@lang-tag/cli 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs CHANGED
@@ -15,6 +15,10 @@ const namespaceCollector = require("./chunks/namespace-collector.cjs");
15
15
  const micromatch = require("micromatch");
16
16
  const acorn = require("acorn");
17
17
  const mustache = require("mustache");
18
+ const checkbox = require("@inquirer/checkbox");
19
+ const confirm = require("@inquirer/confirm");
20
+ const input = require("@inquirer/input");
21
+ const select = require("@inquirer/select");
18
22
  const chokidar = require("chokidar");
19
23
  var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
20
24
  function _interopNamespaceDefault(e) {
@@ -1545,7 +1549,7 @@ const templatePath = path.join(
1545
1549
  "imported-tag.mustache"
1546
1550
  );
1547
1551
  const template = fs.readFileSync(templatePath, "utf-8");
1548
- function renderTemplate$1(data) {
1552
+ function renderTemplate$2(data) {
1549
1553
  return mustache.render(template, data, {}, { escape: (text) => text });
1550
1554
  }
1551
1555
  async function generateImportFiles(config, logger, importManager) {
@@ -1574,7 +1578,7 @@ async function generateImportFiles(config, logger, importManager) {
1574
1578
  tagImportPath: config.import.tagImportPath,
1575
1579
  exports: processedExports
1576
1580
  };
1577
- const content = renderTemplate$1(templateData);
1581
+ const content = renderTemplate$2(templateData);
1578
1582
  await $LT_EnsureDirectoryExists(path.dirname(filePath));
1579
1583
  await promises.writeFile(filePath, content, "utf-8");
1580
1584
  logger.success('Created tag file: "{file}"', {
@@ -1667,6 +1671,316 @@ async function $LT_ImportTranslations() {
1667
1671
  await $LT_ImportLibraries(config, logger);
1668
1672
  logger.success("Successfully imported translations from libraries.");
1669
1673
  }
1674
+ function parseGitignore(cwd) {
1675
+ const gitignorePath = path.join(cwd, ".gitignore");
1676
+ try {
1677
+ const content = fs.readFileSync(gitignorePath, "utf-8");
1678
+ return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).filter((line) => !line.startsWith("!")).map((line) => {
1679
+ if (line.endsWith("/")) line = line.slice(0, -1);
1680
+ if (line.startsWith("/")) line = line.slice(1);
1681
+ return line;
1682
+ }).filter((line) => {
1683
+ if (line.startsWith("*.")) return false;
1684
+ if (line.includes(".") && line.split(".")[1].length <= 4)
1685
+ return false;
1686
+ return true;
1687
+ });
1688
+ } catch {
1689
+ return [];
1690
+ }
1691
+ }
1692
+ function isIgnored(entry, ignorePatterns) {
1693
+ if (entry.startsWith(".")) {
1694
+ return true;
1695
+ }
1696
+ if (ignorePatterns.length === 0) {
1697
+ return false;
1698
+ }
1699
+ return micromatch.isMatch(entry, ignorePatterns);
1700
+ }
1701
+ function detectProjectDirectories() {
1702
+ const cwd = process.cwd();
1703
+ const ignorePatterns = parseGitignore(cwd);
1704
+ const detectedFolders = [];
1705
+ try {
1706
+ const entries = fs.readdirSync(cwd);
1707
+ for (const entry of entries) {
1708
+ if (isIgnored(entry, ignorePatterns)) {
1709
+ continue;
1710
+ }
1711
+ try {
1712
+ const fullPath = path.join(cwd, entry);
1713
+ const stat = fs.statSync(fullPath);
1714
+ if (stat.isDirectory()) {
1715
+ detectedFolders.push(entry);
1716
+ }
1717
+ } catch {
1718
+ }
1719
+ }
1720
+ } catch {
1721
+ return ["src", "app"];
1722
+ }
1723
+ return detectedFolders.length > 0 ? detectedFolders.sort() : ["src", "app"];
1724
+ }
1725
+ function getDefaultAnswers() {
1726
+ return {
1727
+ projectType: "project",
1728
+ tagName: "lang",
1729
+ collectorType: "namespace",
1730
+ namespaceOptions: {
1731
+ modifyNamespaceOptions: false,
1732
+ defaultNamespace: "common"
1733
+ },
1734
+ localesDirectory: "public/locales",
1735
+ configGeneration: {
1736
+ enabled: true,
1737
+ useAlgorithm: "path-based",
1738
+ keepVariables: true
1739
+ },
1740
+ importLibraries: true,
1741
+ interfereWithCollection: false,
1742
+ includeDirectories: ["src"],
1743
+ baseLanguageCode: "en",
1744
+ addCommentGuides: false
1745
+ };
1746
+ }
1747
+ async function askProjectSetupQuestions() {
1748
+ const projectType = await select({
1749
+ message: "Is this a project or a library?",
1750
+ choices: [
1751
+ {
1752
+ name: "Project (application that consumes translations)",
1753
+ value: "project",
1754
+ description: "For applications that use translations"
1755
+ },
1756
+ {
1757
+ name: "Library (exports translations for other projects)",
1758
+ value: "library",
1759
+ description: "For packages that provide translations"
1760
+ }
1761
+ ]
1762
+ });
1763
+ const tagName = await input({
1764
+ message: "What name would you like for your translation tag function?",
1765
+ default: "lang"
1766
+ });
1767
+ let collectorType = "namespace";
1768
+ let namespaceOptions;
1769
+ let localesDirectory = "locales";
1770
+ const modifyNamespaceOptions = false;
1771
+ if (projectType === "project") {
1772
+ collectorType = await select({
1773
+ message: "How would you like to collect translations?",
1774
+ choices: [
1775
+ {
1776
+ name: "Namespace (organized by modules/features)",
1777
+ value: "namespace",
1778
+ description: "Organized structure with namespaces"
1779
+ },
1780
+ {
1781
+ name: "Dictionary (flat structure, all translations in one file)",
1782
+ value: "dictionary",
1783
+ description: "Simple flat dictionary structure"
1784
+ }
1785
+ ]
1786
+ });
1787
+ localesDirectory = await input({
1788
+ message: "Where should the translation files be stored?",
1789
+ default: "public/locales"
1790
+ });
1791
+ }
1792
+ const defaultNamespace = await input({
1793
+ message: "Default namespace for tags without explicit namespace:",
1794
+ default: "common"
1795
+ });
1796
+ namespaceOptions = {
1797
+ modifyNamespaceOptions,
1798
+ defaultNamespace
1799
+ };
1800
+ const enableConfigGeneration = await confirm({
1801
+ message: "Do you want to script config generation for tags?",
1802
+ default: projectType === "project"
1803
+ });
1804
+ let configGeneration = {
1805
+ enabled: enableConfigGeneration
1806
+ };
1807
+ if (enableConfigGeneration) {
1808
+ const algorithmChoice = await select({
1809
+ message: "Which config generation approach would you like?",
1810
+ choices: [
1811
+ {
1812
+ name: "Path-based (automatic based on file structure)",
1813
+ value: "path-based",
1814
+ description: "Generates namespace and path from file location"
1815
+ },
1816
+ {
1817
+ name: "Custom (write your own algorithm)",
1818
+ value: "custom",
1819
+ description: "Implement custom config generation logic"
1820
+ }
1821
+ ]
1822
+ });
1823
+ const keepVariables = await confirm({
1824
+ message: "Add a keeper mechanism that locks parts of the configuration from being overwritten?",
1825
+ default: true
1826
+ });
1827
+ configGeneration = {
1828
+ enabled: true,
1829
+ useAlgorithm: algorithmChoice,
1830
+ keepVariables
1831
+ };
1832
+ }
1833
+ const importLibraries = await confirm({
1834
+ message: "Do you plan to import translation tags from external libraries?",
1835
+ default: projectType === "project"
1836
+ });
1837
+ const interfereWithCollection = await confirm({
1838
+ message: "Do you want to interfere with collection mechanisms (conflict resolution, collection finish)?",
1839
+ default: false
1840
+ });
1841
+ const detectedDirectories = detectProjectDirectories();
1842
+ const includeDirectories = await checkbox({
1843
+ message: "Select directories where lang tags will be used (you can add more later):",
1844
+ choices: detectedDirectories.map((directory) => ({
1845
+ name: directory,
1846
+ value: directory,
1847
+ checked: directory === "src" || detectedDirectories.length === 1
1848
+ })),
1849
+ required: true
1850
+ });
1851
+ const baseLanguageCode = await input({
1852
+ message: "Base language code:",
1853
+ default: "en",
1854
+ validate: (value) => {
1855
+ if (!value || value.length < 2) {
1856
+ return "Please enter a valid language code (e.g., en, pl, fr, de, es)";
1857
+ }
1858
+ return true;
1859
+ }
1860
+ });
1861
+ const addCommentGuides = await confirm({
1862
+ message: "Would you like guides in comments?",
1863
+ default: true
1864
+ });
1865
+ return {
1866
+ projectType,
1867
+ tagName,
1868
+ collectorType,
1869
+ namespaceOptions,
1870
+ localesDirectory,
1871
+ configGeneration,
1872
+ importLibraries,
1873
+ interfereWithCollection,
1874
+ includeDirectories,
1875
+ baseLanguageCode,
1876
+ addCommentGuides
1877
+ };
1878
+ }
1879
+ function renderTemplate$1(template2, data, partials) {
1880
+ return mustache.render(template2, data, partials, {
1881
+ escape: (text) => text
1882
+ });
1883
+ }
1884
+ function loadTemplateFile(filename, required = true) {
1885
+ const __filename2 = url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename2).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href);
1886
+ const __dirname = path.dirname(__filename2);
1887
+ let templatePath2 = path.join(__dirname, "templates", "config", filename);
1888
+ try {
1889
+ return fs.readFileSync(templatePath2, "utf-8");
1890
+ } catch {
1891
+ templatePath2 = path.join(
1892
+ __dirname,
1893
+ "..",
1894
+ "..",
1895
+ "templates",
1896
+ "config",
1897
+ filename
1898
+ );
1899
+ try {
1900
+ return fs.readFileSync(templatePath2, "utf-8");
1901
+ } catch (error) {
1902
+ if (required) {
1903
+ throw new Error(
1904
+ `Failed to load template "${filename}": ${error}`
1905
+ );
1906
+ }
1907
+ return null;
1908
+ }
1909
+ }
1910
+ }
1911
+ function loadTemplate$1() {
1912
+ return loadTemplateFile("config.mustache", true);
1913
+ }
1914
+ function loadPartials() {
1915
+ const partials = {};
1916
+ const generationAlgorithm = loadTemplateFile(
1917
+ "generation-algorithm.mustache",
1918
+ false
1919
+ );
1920
+ if (generationAlgorithm) {
1921
+ partials["generation-algorithm"] = generationAlgorithm;
1922
+ }
1923
+ return partials;
1924
+ }
1925
+ function buildIncludesPattern(directories) {
1926
+ return directories.map((directory) => `'${directory}/**/*.{js,ts,jsx,tsx}'`).join(", ");
1927
+ }
1928
+ function buildExcludesPattern() {
1929
+ const excludes = [
1930
+ "node_modules",
1931
+ "dist",
1932
+ "build",
1933
+ "**/*.test.ts",
1934
+ "**/*.spec.ts"
1935
+ ];
1936
+ return excludes.map((e) => `'${e}'`).join(", ");
1937
+ }
1938
+ function prepareTemplateData$1(options) {
1939
+ const { answers, moduleSystem } = options;
1940
+ const needsPathBasedImport = answers.configGeneration.enabled && answers.configGeneration.useAlgorithm === "path-based";
1941
+ const hasConfigGeneration = answers.configGeneration.enabled;
1942
+ const usePathBased = hasConfigGeneration && answers.configGeneration.useAlgorithm === "path-based";
1943
+ const useCustom = hasConfigGeneration && answers.configGeneration.useAlgorithm === "custom";
1944
+ const needsTagName = answers.tagName !== "lang";
1945
+ const useKeeper = answers.configGeneration.keepVariables || false;
1946
+ const isDictionaryCollector = answers.collectorType === "dictionary";
1947
+ const isModifiedNamespaceCollector = answers.collectorType === "namespace" && !!answers.namespaceOptions?.modifyNamespaceOptions;
1948
+ const importLibraries = answers.importLibraries;
1949
+ const defaultNamespace = answers.namespaceOptions?.defaultNamespace || "common";
1950
+ const isDefaultNamespace = defaultNamespace === "common";
1951
+ const hasCollectContent = isDictionaryCollector || isModifiedNamespaceCollector || answers.interfereWithCollection || !isDefaultNamespace;
1952
+ const needsAlgorithms = needsPathBasedImport || useKeeper || isDictionaryCollector || isModifiedNamespaceCollector || importLibraries;
1953
+ return {
1954
+ isCJS: moduleSystem === "cjs",
1955
+ addComments: answers.addCommentGuides,
1956
+ needsAlgorithms,
1957
+ needsPathBasedImport,
1958
+ useKeeper,
1959
+ isDictionaryCollector,
1960
+ isModifiedNamespaceCollector,
1961
+ importLibraries,
1962
+ needsTagName,
1963
+ tagName: answers.tagName,
1964
+ isLibrary: answers.projectType === "library",
1965
+ includes: buildIncludesPattern(answers.includeDirectories),
1966
+ excludes: buildExcludesPattern(),
1967
+ localesDirectory: answers.localesDirectory,
1968
+ baseLanguageCode: answers.baseLanguageCode,
1969
+ hasConfigGeneration,
1970
+ usePathBased,
1971
+ useCustom,
1972
+ defaultNamespace,
1973
+ isDefaultNamespace,
1974
+ interfereWithCollection: answers.interfereWithCollection,
1975
+ hasCollectContent
1976
+ };
1977
+ }
1978
+ function renderConfigTemplate(options) {
1979
+ const template2 = loadTemplate$1();
1980
+ const templateData = prepareTemplateData$1(options);
1981
+ const partials = loadPartials();
1982
+ return renderTemplate$1(template2, templateData, partials);
1983
+ }
1670
1984
  async function detectModuleSystem() {
1671
1985
  const packageJsonPath = path.join(process.cwd(), "package.json");
1672
1986
  if (!fs.existsSync(packageJsonPath)) {
@@ -1683,90 +1997,60 @@ async function detectModuleSystem() {
1683
1997
  return "cjs";
1684
1998
  }
1685
1999
  }
1686
- async function generateDefaultConfig() {
1687
- const moduleSystem = await detectModuleSystem();
1688
- const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator, configKeeper } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator, configKeeper } = require('@lang-tag/cli/algorithms');`;
1689
- const exportStatement = moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
1690
- return `${importStatement}
1691
-
1692
- const generationAlgorithm = pathBasedConfigGenerator({
1693
- ignoreIncludesRootDirectories: true,
1694
- removeBracketedDirectories: true,
1695
- namespaceCase: 'kebab',
1696
- pathCase: 'camel',
1697
- clearOnDefaultNamespace: true,
1698
- ignoreDirectories: ['core', 'utils', 'helpers'],
1699
- // Advanced: Use pathRules for hierarchical transformations with ignore and rename
1700
- // pathRules: {
1701
- // app: {
1702
- // dashboard: {
1703
- // _: false, // ignore "dashboard" but continue with nested rules
1704
- // modules: false // also ignore "modules"
1705
- // },
1706
- // admin: {
1707
- // '>': 'management', // rename "admin" to "management"
1708
- // users: false // ignore "users",
1709
- // ui: {
1710
- // '>>': { // 'redirect' - ignore everything, jump to 'ui' namespace and prefix all paths with 'admin'
1711
- // namespace: 'ui',
1712
- // pathPrefix: 'admin'
1713
- // }
1714
- // }
1715
- // }
1716
- // }
1717
- // }
1718
- });
1719
- const keeper = configKeeper({ propertyName: 'keep' });
1720
-
1721
- /** @type {import('@lang-tag/cli/type').LangTagCLIConfig} */
1722
- const config = {
1723
- tagName: 'lang',
1724
- isLibrary: false,
1725
- includes: ['src/**/*.{js,ts,jsx,tsx}'],
1726
- excludes: ['node_modules', 'dist', 'build', '**/*.test.ts'],
1727
- localesDirectory: 'public/locales',
1728
- baseLanguageCode: 'en',
1729
- onConfigGeneration: async event => {
1730
- // We do not modify imported configurations
1731
- if (event.isImportedLibrary) return;
1732
-
1733
- if (event.config?.keep === 'both') return;
1734
-
1735
- await generationAlgorithm(event);
1736
-
1737
- await keeper(event);
1738
- },
1739
- collect: {
1740
- defaultNamespace: 'common',
1741
- onConflictResolution: async event => {
1742
- await event.logger.conflict(event.conflict, true);
1743
- // By default, continue processing even if conflicts occur
1744
- // Call event.exit(); to terminate the process upon the first conflict
1745
- },
1746
- onCollectFinish: event => {
1747
- if (event.conflicts.length) event.exit(); // Stop the process to avoid merging on conflict
1748
- }
1749
- },
1750
- translationArgPosition: 1,
1751
- debug: false,
1752
- };
1753
-
1754
- ${exportStatement}`;
1755
- }
1756
- async function $LT_CMD_InitConfig() {
2000
+ async function $LT_CMD_InitConfig(options = {}) {
1757
2001
  const logger = $LT_CreateDefaultLogger();
1758
2002
  if (fs.existsSync(CONFIG_FILE_NAME)) {
1759
- logger.success(
1760
- "Configuration file already exists. Please remove the existing configuration file before creating a new default one"
2003
+ logger.error(
2004
+ "Configuration file already exists. Please remove the existing configuration file before creating a new one"
1761
2005
  );
1762
2006
  return;
1763
2007
  }
2008
+ console.log("");
2009
+ logger.info("Welcome to Lang Tag CLI Setup!");
2010
+ console.log("");
1764
2011
  try {
1765
- const configContent = await generateDefaultConfig();
2012
+ const answers = options.yes ? getDefaultAnswers() : await askProjectSetupQuestions();
2013
+ if (options.yes) {
2014
+ logger.info("Using default configuration (--yes flag detected)...");
2015
+ }
2016
+ const moduleSystem = await detectModuleSystem();
2017
+ const configContent = renderConfigTemplate({
2018
+ answers,
2019
+ moduleSystem
2020
+ });
1766
2021
  await promises.writeFile(CONFIG_FILE_NAME, configContent, "utf-8");
1767
- logger.success("Configuration file created successfully");
2022
+ logger.success("Configuration file created successfully!");
2023
+ logger.success("Created {configFile}", {
2024
+ configFile: CONFIG_FILE_NAME
2025
+ });
2026
+ logger.info("Next steps:");
2027
+ logger.info(" 1. Review and customize {configFile}", {
2028
+ configFile: CONFIG_FILE_NAME
2029
+ });
2030
+ logger.info(
2031
+ " 2. Ensure all dependencies are installed (TypeScript, React, etc.),"
2032
+ );
2033
+ logger.info(
2034
+ ' then run "npx lang-tag init-tag" to generate an initial tag'
2035
+ );
2036
+ logger.info(
2037
+ " (the tag will be based on what libraries you have in your project)"
2038
+ );
2039
+ logger.info(
2040
+ " 3. Use your tag in the project under the include directories"
2041
+ );
2042
+ logger.info(' 4. Run "npx lang-tag collect" to collect translations');
2043
+ if (answers.projectType === "project") {
2044
+ logger.info(" 5. Your translations will be in {dir}", {
2045
+ dir: answers.localesDirectory
2046
+ });
2047
+ }
1768
2048
  } catch (error) {
1769
- logger.error(error?.message);
2049
+ if (error.name === "ExitPromptError") {
2050
+ logger.warn("Setup cancelled");
2051
+ return;
2052
+ }
2053
+ logger.error(error?.message || "An error occurred during setup");
1770
2054
  }
1771
2055
  }
1772
2056
  async function readPackageJson() {
@@ -2099,7 +2383,7 @@ function createCli() {
2099
2383
  commander.program.command("watch").alias("w").description(
2100
2384
  "Watch for changes in source files and automatically collect translations"
2101
2385
  ).action($LT_WatchTranslations);
2102
- commander.program.command("init").description("Initialize project with default configuration").action($LT_CMD_InitConfig);
2386
+ commander.program.command("init").description("Initialize project with default configuration").option("-y, --yes", "Skip prompts and use default configuration").action($LT_CMD_InitConfig);
2103
2387
  commander.program.command("init-tag").description("Initialize a new lang-tag function file").option(
2104
2388
  "-n, --name <name>",
2105
2389
  "Name of the tag function (default: from config)"
package/index.js CHANGED
@@ -2,7 +2,7 @@
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 } from "fs";
5
+ import fs, { readFileSync, existsSync, 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";
@@ -15,6 +15,10 @@ import { N as NamespaceCollector } from "./chunks/namespace-collector.js";
15
15
  import micromatch from "micromatch";
16
16
  import * as acorn from "acorn";
17
17
  import mustache from "mustache";
18
+ import checkbox from "@inquirer/checkbox";
19
+ import confirm from "@inquirer/confirm";
20
+ import input from "@inquirer/input";
21
+ import select from "@inquirer/select";
18
22
  import chokidar from "chokidar";
19
23
  function $LT_FilterInvalidTags(tags, config, logger) {
20
24
  return tags.filter((tag) => {
@@ -1525,7 +1529,7 @@ const templatePath = join(
1525
1529
  "imported-tag.mustache"
1526
1530
  );
1527
1531
  const template = readFileSync(templatePath, "utf-8");
1528
- function renderTemplate$1(data) {
1532
+ function renderTemplate$2(data) {
1529
1533
  return mustache.render(template, data, {}, { escape: (text) => text });
1530
1534
  }
1531
1535
  async function generateImportFiles(config, logger, importManager) {
@@ -1554,7 +1558,7 @@ async function generateImportFiles(config, logger, importManager) {
1554
1558
  tagImportPath: config.import.tagImportPath,
1555
1559
  exports: processedExports
1556
1560
  };
1557
- const content = renderTemplate$1(templateData);
1561
+ const content = renderTemplate$2(templateData);
1558
1562
  await $LT_EnsureDirectoryExists(dirname(filePath));
1559
1563
  await writeFile(filePath, content, "utf-8");
1560
1564
  logger.success('Created tag file: "{file}"', {
@@ -1647,6 +1651,316 @@ async function $LT_ImportTranslations() {
1647
1651
  await $LT_ImportLibraries(config, logger);
1648
1652
  logger.success("Successfully imported translations from libraries.");
1649
1653
  }
1654
+ function parseGitignore(cwd) {
1655
+ const gitignorePath = join(cwd, ".gitignore");
1656
+ try {
1657
+ const content = readFileSync(gitignorePath, "utf-8");
1658
+ return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).filter((line) => !line.startsWith("!")).map((line) => {
1659
+ if (line.endsWith("/")) line = line.slice(0, -1);
1660
+ if (line.startsWith("/")) line = line.slice(1);
1661
+ return line;
1662
+ }).filter((line) => {
1663
+ if (line.startsWith("*.")) return false;
1664
+ if (line.includes(".") && line.split(".")[1].length <= 4)
1665
+ return false;
1666
+ return true;
1667
+ });
1668
+ } catch {
1669
+ return [];
1670
+ }
1671
+ }
1672
+ function isIgnored(entry, ignorePatterns) {
1673
+ if (entry.startsWith(".")) {
1674
+ return true;
1675
+ }
1676
+ if (ignorePatterns.length === 0) {
1677
+ return false;
1678
+ }
1679
+ return micromatch.isMatch(entry, ignorePatterns);
1680
+ }
1681
+ function detectProjectDirectories() {
1682
+ const cwd = process.cwd();
1683
+ const ignorePatterns = parseGitignore(cwd);
1684
+ const detectedFolders = [];
1685
+ try {
1686
+ const entries = readdirSync(cwd);
1687
+ for (const entry of entries) {
1688
+ if (isIgnored(entry, ignorePatterns)) {
1689
+ continue;
1690
+ }
1691
+ try {
1692
+ const fullPath = join(cwd, entry);
1693
+ const stat = statSync(fullPath);
1694
+ if (stat.isDirectory()) {
1695
+ detectedFolders.push(entry);
1696
+ }
1697
+ } catch {
1698
+ }
1699
+ }
1700
+ } catch {
1701
+ return ["src", "app"];
1702
+ }
1703
+ return detectedFolders.length > 0 ? detectedFolders.sort() : ["src", "app"];
1704
+ }
1705
+ function getDefaultAnswers() {
1706
+ return {
1707
+ projectType: "project",
1708
+ tagName: "lang",
1709
+ collectorType: "namespace",
1710
+ namespaceOptions: {
1711
+ modifyNamespaceOptions: false,
1712
+ defaultNamespace: "common"
1713
+ },
1714
+ localesDirectory: "public/locales",
1715
+ configGeneration: {
1716
+ enabled: true,
1717
+ useAlgorithm: "path-based",
1718
+ keepVariables: true
1719
+ },
1720
+ importLibraries: true,
1721
+ interfereWithCollection: false,
1722
+ includeDirectories: ["src"],
1723
+ baseLanguageCode: "en",
1724
+ addCommentGuides: false
1725
+ };
1726
+ }
1727
+ async function askProjectSetupQuestions() {
1728
+ const projectType = await select({
1729
+ message: "Is this a project or a library?",
1730
+ choices: [
1731
+ {
1732
+ name: "Project (application that consumes translations)",
1733
+ value: "project",
1734
+ description: "For applications that use translations"
1735
+ },
1736
+ {
1737
+ name: "Library (exports translations for other projects)",
1738
+ value: "library",
1739
+ description: "For packages that provide translations"
1740
+ }
1741
+ ]
1742
+ });
1743
+ const tagName = await input({
1744
+ message: "What name would you like for your translation tag function?",
1745
+ default: "lang"
1746
+ });
1747
+ let collectorType = "namespace";
1748
+ let namespaceOptions;
1749
+ let localesDirectory = "locales";
1750
+ const modifyNamespaceOptions = false;
1751
+ if (projectType === "project") {
1752
+ collectorType = await select({
1753
+ message: "How would you like to collect translations?",
1754
+ choices: [
1755
+ {
1756
+ name: "Namespace (organized by modules/features)",
1757
+ value: "namespace",
1758
+ description: "Organized structure with namespaces"
1759
+ },
1760
+ {
1761
+ name: "Dictionary (flat structure, all translations in one file)",
1762
+ value: "dictionary",
1763
+ description: "Simple flat dictionary structure"
1764
+ }
1765
+ ]
1766
+ });
1767
+ localesDirectory = await input({
1768
+ message: "Where should the translation files be stored?",
1769
+ default: "public/locales"
1770
+ });
1771
+ }
1772
+ const defaultNamespace = await input({
1773
+ message: "Default namespace for tags without explicit namespace:",
1774
+ default: "common"
1775
+ });
1776
+ namespaceOptions = {
1777
+ modifyNamespaceOptions,
1778
+ defaultNamespace
1779
+ };
1780
+ const enableConfigGeneration = await confirm({
1781
+ message: "Do you want to script config generation for tags?",
1782
+ default: projectType === "project"
1783
+ });
1784
+ let configGeneration = {
1785
+ enabled: enableConfigGeneration
1786
+ };
1787
+ if (enableConfigGeneration) {
1788
+ const algorithmChoice = await select({
1789
+ message: "Which config generation approach would you like?",
1790
+ choices: [
1791
+ {
1792
+ name: "Path-based (automatic based on file structure)",
1793
+ value: "path-based",
1794
+ description: "Generates namespace and path from file location"
1795
+ },
1796
+ {
1797
+ name: "Custom (write your own algorithm)",
1798
+ value: "custom",
1799
+ description: "Implement custom config generation logic"
1800
+ }
1801
+ ]
1802
+ });
1803
+ const keepVariables = await confirm({
1804
+ message: "Add a keeper mechanism that locks parts of the configuration from being overwritten?",
1805
+ default: true
1806
+ });
1807
+ configGeneration = {
1808
+ enabled: true,
1809
+ useAlgorithm: algorithmChoice,
1810
+ keepVariables
1811
+ };
1812
+ }
1813
+ const importLibraries = await confirm({
1814
+ message: "Do you plan to import translation tags from external libraries?",
1815
+ default: projectType === "project"
1816
+ });
1817
+ const interfereWithCollection = await confirm({
1818
+ message: "Do you want to interfere with collection mechanisms (conflict resolution, collection finish)?",
1819
+ default: false
1820
+ });
1821
+ const detectedDirectories = detectProjectDirectories();
1822
+ const includeDirectories = await checkbox({
1823
+ message: "Select directories where lang tags will be used (you can add more later):",
1824
+ choices: detectedDirectories.map((directory) => ({
1825
+ name: directory,
1826
+ value: directory,
1827
+ checked: directory === "src" || detectedDirectories.length === 1
1828
+ })),
1829
+ required: true
1830
+ });
1831
+ const baseLanguageCode = await input({
1832
+ message: "Base language code:",
1833
+ default: "en",
1834
+ validate: (value) => {
1835
+ if (!value || value.length < 2) {
1836
+ return "Please enter a valid language code (e.g., en, pl, fr, de, es)";
1837
+ }
1838
+ return true;
1839
+ }
1840
+ });
1841
+ const addCommentGuides = await confirm({
1842
+ message: "Would you like guides in comments?",
1843
+ default: true
1844
+ });
1845
+ return {
1846
+ projectType,
1847
+ tagName,
1848
+ collectorType,
1849
+ namespaceOptions,
1850
+ localesDirectory,
1851
+ configGeneration,
1852
+ importLibraries,
1853
+ interfereWithCollection,
1854
+ includeDirectories,
1855
+ baseLanguageCode,
1856
+ addCommentGuides
1857
+ };
1858
+ }
1859
+ function renderTemplate$1(template2, data, partials) {
1860
+ return mustache.render(template2, data, partials, {
1861
+ escape: (text) => text
1862
+ });
1863
+ }
1864
+ function loadTemplateFile(filename, required = true) {
1865
+ const __filename2 = fileURLToPath(import.meta.url);
1866
+ const __dirname2 = dirname(__filename2);
1867
+ let templatePath2 = join(__dirname2, "templates", "config", filename);
1868
+ try {
1869
+ return readFileSync(templatePath2, "utf-8");
1870
+ } catch {
1871
+ templatePath2 = join(
1872
+ __dirname2,
1873
+ "..",
1874
+ "..",
1875
+ "templates",
1876
+ "config",
1877
+ filename
1878
+ );
1879
+ try {
1880
+ return readFileSync(templatePath2, "utf-8");
1881
+ } catch (error) {
1882
+ if (required) {
1883
+ throw new Error(
1884
+ `Failed to load template "${filename}": ${error}`
1885
+ );
1886
+ }
1887
+ return null;
1888
+ }
1889
+ }
1890
+ }
1891
+ function loadTemplate$1() {
1892
+ return loadTemplateFile("config.mustache", true);
1893
+ }
1894
+ function loadPartials() {
1895
+ const partials = {};
1896
+ const generationAlgorithm = loadTemplateFile(
1897
+ "generation-algorithm.mustache",
1898
+ false
1899
+ );
1900
+ if (generationAlgorithm) {
1901
+ partials["generation-algorithm"] = generationAlgorithm;
1902
+ }
1903
+ return partials;
1904
+ }
1905
+ function buildIncludesPattern(directories) {
1906
+ return directories.map((directory) => `'${directory}/**/*.{js,ts,jsx,tsx}'`).join(", ");
1907
+ }
1908
+ function buildExcludesPattern() {
1909
+ const excludes = [
1910
+ "node_modules",
1911
+ "dist",
1912
+ "build",
1913
+ "**/*.test.ts",
1914
+ "**/*.spec.ts"
1915
+ ];
1916
+ return excludes.map((e) => `'${e}'`).join(", ");
1917
+ }
1918
+ function prepareTemplateData$1(options) {
1919
+ const { answers, moduleSystem } = options;
1920
+ const needsPathBasedImport = answers.configGeneration.enabled && answers.configGeneration.useAlgorithm === "path-based";
1921
+ const hasConfigGeneration = answers.configGeneration.enabled;
1922
+ const usePathBased = hasConfigGeneration && answers.configGeneration.useAlgorithm === "path-based";
1923
+ const useCustom = hasConfigGeneration && answers.configGeneration.useAlgorithm === "custom";
1924
+ const needsTagName = answers.tagName !== "lang";
1925
+ const useKeeper = answers.configGeneration.keepVariables || false;
1926
+ const isDictionaryCollector = answers.collectorType === "dictionary";
1927
+ const isModifiedNamespaceCollector = answers.collectorType === "namespace" && !!answers.namespaceOptions?.modifyNamespaceOptions;
1928
+ const importLibraries = answers.importLibraries;
1929
+ const defaultNamespace = answers.namespaceOptions?.defaultNamespace || "common";
1930
+ const isDefaultNamespace = defaultNamespace === "common";
1931
+ const hasCollectContent = isDictionaryCollector || isModifiedNamespaceCollector || answers.interfereWithCollection || !isDefaultNamespace;
1932
+ const needsAlgorithms = needsPathBasedImport || useKeeper || isDictionaryCollector || isModifiedNamespaceCollector || importLibraries;
1933
+ return {
1934
+ isCJS: moduleSystem === "cjs",
1935
+ addComments: answers.addCommentGuides,
1936
+ needsAlgorithms,
1937
+ needsPathBasedImport,
1938
+ useKeeper,
1939
+ isDictionaryCollector,
1940
+ isModifiedNamespaceCollector,
1941
+ importLibraries,
1942
+ needsTagName,
1943
+ tagName: answers.tagName,
1944
+ isLibrary: answers.projectType === "library",
1945
+ includes: buildIncludesPattern(answers.includeDirectories),
1946
+ excludes: buildExcludesPattern(),
1947
+ localesDirectory: answers.localesDirectory,
1948
+ baseLanguageCode: answers.baseLanguageCode,
1949
+ hasConfigGeneration,
1950
+ usePathBased,
1951
+ useCustom,
1952
+ defaultNamespace,
1953
+ isDefaultNamespace,
1954
+ interfereWithCollection: answers.interfereWithCollection,
1955
+ hasCollectContent
1956
+ };
1957
+ }
1958
+ function renderConfigTemplate(options) {
1959
+ const template2 = loadTemplate$1();
1960
+ const templateData = prepareTemplateData$1(options);
1961
+ const partials = loadPartials();
1962
+ return renderTemplate$1(template2, templateData, partials);
1963
+ }
1650
1964
  async function detectModuleSystem() {
1651
1965
  const packageJsonPath = join(process.cwd(), "package.json");
1652
1966
  if (!existsSync(packageJsonPath)) {
@@ -1663,90 +1977,60 @@ async function detectModuleSystem() {
1663
1977
  return "cjs";
1664
1978
  }
1665
1979
  }
1666
- async function generateDefaultConfig() {
1667
- const moduleSystem = await detectModuleSystem();
1668
- const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator, configKeeper } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator, configKeeper } = require('@lang-tag/cli/algorithms');`;
1669
- const exportStatement = moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
1670
- return `${importStatement}
1671
-
1672
- const generationAlgorithm = pathBasedConfigGenerator({
1673
- ignoreIncludesRootDirectories: true,
1674
- removeBracketedDirectories: true,
1675
- namespaceCase: 'kebab',
1676
- pathCase: 'camel',
1677
- clearOnDefaultNamespace: true,
1678
- ignoreDirectories: ['core', 'utils', 'helpers'],
1679
- // Advanced: Use pathRules for hierarchical transformations with ignore and rename
1680
- // pathRules: {
1681
- // app: {
1682
- // dashboard: {
1683
- // _: false, // ignore "dashboard" but continue with nested rules
1684
- // modules: false // also ignore "modules"
1685
- // },
1686
- // admin: {
1687
- // '>': 'management', // rename "admin" to "management"
1688
- // users: false // ignore "users",
1689
- // ui: {
1690
- // '>>': { // 'redirect' - ignore everything, jump to 'ui' namespace and prefix all paths with 'admin'
1691
- // namespace: 'ui',
1692
- // pathPrefix: 'admin'
1693
- // }
1694
- // }
1695
- // }
1696
- // }
1697
- // }
1698
- });
1699
- const keeper = configKeeper({ propertyName: 'keep' });
1700
-
1701
- /** @type {import('@lang-tag/cli/type').LangTagCLIConfig} */
1702
- const config = {
1703
- tagName: 'lang',
1704
- isLibrary: false,
1705
- includes: ['src/**/*.{js,ts,jsx,tsx}'],
1706
- excludes: ['node_modules', 'dist', 'build', '**/*.test.ts'],
1707
- localesDirectory: 'public/locales',
1708
- baseLanguageCode: 'en',
1709
- onConfigGeneration: async event => {
1710
- // We do not modify imported configurations
1711
- if (event.isImportedLibrary) return;
1712
-
1713
- if (event.config?.keep === 'both') return;
1714
-
1715
- await generationAlgorithm(event);
1716
-
1717
- await keeper(event);
1718
- },
1719
- collect: {
1720
- defaultNamespace: 'common',
1721
- onConflictResolution: async event => {
1722
- await event.logger.conflict(event.conflict, true);
1723
- // By default, continue processing even if conflicts occur
1724
- // Call event.exit(); to terminate the process upon the first conflict
1725
- },
1726
- onCollectFinish: event => {
1727
- if (event.conflicts.length) event.exit(); // Stop the process to avoid merging on conflict
1728
- }
1729
- },
1730
- translationArgPosition: 1,
1731
- debug: false,
1732
- };
1733
-
1734
- ${exportStatement}`;
1735
- }
1736
- async function $LT_CMD_InitConfig() {
1980
+ async function $LT_CMD_InitConfig(options = {}) {
1737
1981
  const logger = $LT_CreateDefaultLogger();
1738
1982
  if (existsSync(CONFIG_FILE_NAME)) {
1739
- logger.success(
1740
- "Configuration file already exists. Please remove the existing configuration file before creating a new default one"
1983
+ logger.error(
1984
+ "Configuration file already exists. Please remove the existing configuration file before creating a new one"
1741
1985
  );
1742
1986
  return;
1743
1987
  }
1988
+ console.log("");
1989
+ logger.info("Welcome to Lang Tag CLI Setup!");
1990
+ console.log("");
1744
1991
  try {
1745
- const configContent = await generateDefaultConfig();
1992
+ const answers = options.yes ? getDefaultAnswers() : await askProjectSetupQuestions();
1993
+ if (options.yes) {
1994
+ logger.info("Using default configuration (--yes flag detected)...");
1995
+ }
1996
+ const moduleSystem = await detectModuleSystem();
1997
+ const configContent = renderConfigTemplate({
1998
+ answers,
1999
+ moduleSystem
2000
+ });
1746
2001
  await writeFile(CONFIG_FILE_NAME, configContent, "utf-8");
1747
- logger.success("Configuration file created successfully");
2002
+ logger.success("Configuration file created successfully!");
2003
+ logger.success("Created {configFile}", {
2004
+ configFile: CONFIG_FILE_NAME
2005
+ });
2006
+ logger.info("Next steps:");
2007
+ logger.info(" 1. Review and customize {configFile}", {
2008
+ configFile: CONFIG_FILE_NAME
2009
+ });
2010
+ logger.info(
2011
+ " 2. Ensure all dependencies are installed (TypeScript, React, etc.),"
2012
+ );
2013
+ logger.info(
2014
+ ' then run "npx lang-tag init-tag" to generate an initial tag'
2015
+ );
2016
+ logger.info(
2017
+ " (the tag will be based on what libraries you have in your project)"
2018
+ );
2019
+ logger.info(
2020
+ " 3. Use your tag in the project under the include directories"
2021
+ );
2022
+ logger.info(' 4. Run "npx lang-tag collect" to collect translations');
2023
+ if (answers.projectType === "project") {
2024
+ logger.info(" 5. Your translations will be in {dir}", {
2025
+ dir: answers.localesDirectory
2026
+ });
2027
+ }
1748
2028
  } catch (error) {
1749
- logger.error(error?.message);
2029
+ if (error.name === "ExitPromptError") {
2030
+ logger.warn("Setup cancelled");
2031
+ return;
2032
+ }
2033
+ logger.error(error?.message || "An error occurred during setup");
1750
2034
  }
1751
2035
  }
1752
2036
  async function readPackageJson() {
@@ -2079,7 +2363,7 @@ function createCli() {
2079
2363
  program.command("watch").alias("w").description(
2080
2364
  "Watch for changes in source files and automatically collect translations"
2081
2365
  ).action($LT_WatchTranslations);
2082
- program.command("init").description("Initialize project with default configuration").action($LT_CMD_InitConfig);
2366
+ program.command("init").description("Initialize project with default configuration").option("-y, --yes", "Skip prompts and use default configuration").action($LT_CMD_InitConfig);
2083
2367
  program.command("init-tag").description("Initialize a new lang-tag function file").option(
2084
2368
  "-n, --name <name>",
2085
2369
  "Name of the tag function (default: from config)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lang-tag/cli",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -33,6 +33,10 @@
33
33
  "lang-tag": "*"
34
34
  },
35
35
  "dependencies": {
36
+ "@inquirer/checkbox": "^4.3.0",
37
+ "@inquirer/confirm": "^5.1.19",
38
+ "@inquirer/prompts": "^7.9.0",
39
+ "@inquirer/select": "^4.4.0",
36
40
  "acorn": "^8.15.0",
37
41
  "case": "^1.6.3",
38
42
  "chokidar": "^4.0.3",
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Lang Tag CLI Configuration
3
+ *
4
+ * This file was generated. You can modify it to customize your translation workflow.
5
+ * Documentation: https://github.com/TheTonsOfCode/lang-tag-cli
6
+ */
7
+
8
+ {{#needsAlgorithms}}
9
+ {{#isCJS}}const{{/isCJS}}{{^isCJS}}import{{/isCJS}} {
10
+ {{#needsPathBasedImport}}
11
+ pathBasedConfigGenerator,
12
+ {{/needsPathBasedImport}}
13
+ {{#useKeeper}}
14
+ configKeeper,
15
+ {{/useKeeper}}
16
+ {{#isDictionaryCollector}}
17
+ DictionaryCollector,
18
+ {{/isDictionaryCollector}}
19
+ {{#isModifiedNamespaceCollector}}
20
+ NamespaceCollector,
21
+ {{/isModifiedNamespaceCollector}}
22
+ {{#importLibraries}}
23
+ flexibleImportAlgorithm,
24
+ {{/importLibraries}}
25
+ } {{#isCJS}}= require('@lang-tag/cli/algorithms');{{/isCJS}}{{^isCJS}}from '@lang-tag/cli/algorithms';{{/isCJS}}
26
+ {{/needsAlgorithms}}
27
+
28
+ {{#needsPathBasedImport}}
29
+ {{>generation-algorithm}}
30
+ {{/needsPathBasedImport}}
31
+ {{#useKeeper}}
32
+ {{#addComments}}
33
+ // Preserves tags marked with keep property. Example: {{tagName}}({ text: 'text'}, { namespace: 'custom', keep: 'namespace' })
34
+ {{/addComments}}
35
+ const keeper = configKeeper({ propertyName: 'keep' });
36
+
37
+ {{/useKeeper}}
38
+ /** @type {import('@lang-tag/cli/type').LangTagCLIConfig} */
39
+ const config = {
40
+ {{#needsTagName}}
41
+ tagName: '{{tagName}}',
42
+ {{/needsTagName}}
43
+ {{#isLibrary}}
44
+ isLibrary: true,
45
+ {{/isLibrary}}
46
+ {{#addComments}}
47
+ // Directories where we’re going to look for language tags
48
+ {{/addComments}}
49
+ includes: [{{includes}}],
50
+ excludes: [{{excludes}}],
51
+ {{^isLibrary}}
52
+ localesDirectory: '{{localesDirectory}}',
53
+ {{/isLibrary}}
54
+ {{#addComments}}
55
+ // The source translations language used in your code (e.g., 'en', 'pl')
56
+ {{/addComments}}
57
+ baseLanguageCode: '{{baseLanguageCode}}',
58
+ {{#hasConfigGeneration}}
59
+ {{#usePathBased}}
60
+ {{#addComments}}
61
+ /**
62
+ * Automatically generates namespace/path from file location.
63
+ * Example: src/features/auth/LoginForm.tsx → { namespace: 'auth', path: 'loginForm' }
64
+ *
65
+ * To customize, modify generationAlgorithm options above or add custom logic:
66
+ * - Change case formatting (namespaceCase, pathCase)
67
+ * - Ignore specific directories (ignoreDirectories)
68
+ * - Add path transformation rules (pathRules)
69
+ */
70
+ {{/addComments}}
71
+ onConfigGeneration: async event => {
72
+ // We do not modify imported configurations
73
+ if (event.isImportedLibrary) return;
74
+ {{#useKeeper}}
75
+ if (event.config?.keep === 'both') return;
76
+ {{/useKeeper}}
77
+
78
+ await generationAlgorithm(event);
79
+ {{#useKeeper}}
80
+ await keeper(event);
81
+ {{/useKeeper}}
82
+ },
83
+ {{/usePathBased}}
84
+ {{#useCustom}}
85
+ {{#addComments}}
86
+ /**
87
+ * Custom hook to generate namespace/path for each tag based on file location.
88
+ *
89
+ * Sample properties:
90
+ * - event.relativePath: File path relative to project root (e.g., 'src/auth/Login.tsx')
91
+ * - event.config: Current tag config (may be undefined or contain user-provided values)
92
+ * - event.save(newConfig): Save new config. Example: event.save({ namespace: 'auth', path: 'login' })
93
+ *
94
+ * Example - generate from directory structure:
95
+ * const segments = event.relativePath.split('/').slice(1, -1);
96
+ * event.save({ namespace: segments[0], path: segments.slice(1).join('.') });
97
+ */
98
+ {{/addComments}}
99
+ onConfigGeneration: async event => {
100
+ // We do not modify imported configurations
101
+ if (event.isImportedLibrary) return;
102
+
103
+ {{#useKeeper}}
104
+ if (event.config?.keep === 'both') return;
105
+ {{/useKeeper}}
106
+ // TODO: Implement your custom config generation logic here
107
+ // event.save({
108
+ // namespace: 'your-namespace',
109
+ // path: 'your.path'
110
+ // });
111
+ {{#useKeeper}}
112
+ await keeper(event);
113
+ {{/useKeeper}}
114
+ },
115
+ {{/useCustom}}
116
+ {{/hasConfigGeneration}}
117
+ {{#hasCollectContent}}
118
+ collect: {
119
+ {{#isDictionaryCollector}}
120
+ {{#addComments}}
121
+ // All translations in one file per language. Change to NamespaceCollector() for separate files per namespace.
122
+ {{/addComments}}
123
+ collector: new DictionaryCollector(),
124
+ {{/isDictionaryCollector}}
125
+ {{#isModifiedNamespaceCollector}}
126
+ {{#addComments}}
127
+ // Separate file per namespace (e.g., locales/en/auth.json, locales/en/dashboard.json)
128
+ {{/addComments}}
129
+ collector: new NamespaceCollector(),
130
+ {{/isModifiedNamespaceCollector}}
131
+ {{^isDictionary}}
132
+ {{^isDefaultNamespace}}
133
+ {{#addComments}}
134
+ // Tags without config or namespace in config will use this
135
+ {{/addComments}}
136
+ defaultNamespace: '{{defaultNamespace}}',
137
+ {{/isDefaultNamespace}}
138
+ {{/isDictionary}}
139
+ {{#interfereWithCollection}}
140
+ {{#addComments}}
141
+ /**
142
+ * Called for each duplicate key conflict (same namespace + path, different values).
143
+ * To stop on first conflict, call event.exit() here instead of in onCollectFinish.
144
+ */
145
+ {{/addComments}}
146
+ onConflictResolution: async event => {
147
+ await event.logger.conflict(event.conflict, true);
148
+ // By default, continue processing even if conflicts occur
149
+ // Call event.exit(); to terminate the process upon the first conflict
150
+ },
151
+ {{#addComments}}
152
+ /**
153
+ * Called after collection completes. Check event.conflicts array to handle all conflicts.
154
+ * Remove this hook to allow merging despite conflicts.
155
+ */
156
+ {{/addComments}}
157
+ onCollectFinish: event => {
158
+ if (event.conflicts.length) event.exit(); // Stop the process to avoid merging on conflict
159
+ }
160
+ {{/interfereWithCollection}}
161
+ },
162
+ {{/hasCollectContent}}
163
+ {{#importLibraries}}
164
+ {{#addComments}}
165
+ /**
166
+ * Imports translations from external libraries (node modules packages containing exported 'lang-tags.json').
167
+ * - dir: Where to generate import files
168
+ * - tagImportPath: Update path to your tag function
169
+ * - onImport: Controls file naming/structure (see flexibleImportAlgorithm options)
170
+ */
171
+ {{/addComments}}
172
+ import: {
173
+ dir: 'src/lang-libraries',
174
+ tagImportPath: 'import { {{tagName}} } from "@/my-lang-tag-path"',
175
+ onImport: flexibleImportAlgorithm({ filePath: { includePackageInPath: true } })
176
+ },
177
+ {{/importLibraries}}
178
+ };
179
+
180
+ {{#isCJS}}module.exports = config;{{/isCJS}}{{^isCJS}}export default config;{{/isCJS}}
@@ -0,0 +1,73 @@
1
+ {{#addComments}}
2
+ /**
3
+ * Generates namespace/path from directory structure (filename is excluded).
4
+ *
5
+ * Example with current settings (assuming includes: ['src/**']):
6
+ * File: src/(admin)/features/auth/utils/LoginForm.tsx
7
+ * 1. Remove filename → src/(admin)/features/auth/utils
8
+ * 2. Remove root 'src' → (admin)/features/auth/utils
9
+ * 3. Remove bracketed '(admin)' → features/auth/utils
10
+ * 4. Remove 'utils' (in ignoreDirectories) → features/auth
11
+ * 5. First segment = namespace, rest = path → namespace: 'features', path: 'auth'
12
+ */
13
+ {{/addComments}}
14
+ const generationAlgorithm = pathBasedConfigGenerator({
15
+ {{#addComments}}
16
+ // Removes root directory from includes patterns (e.g., 'src' if includes: ['src/**'])
17
+ {{/addComments}}
18
+ ignoreIncludesRootDirectories: true,
19
+ {{#addComments}}
20
+ // Directories with () or [] are NOT used for namespace/path generation
21
+ // true: app/(admin)/[id]/page.tsx → app/page (completely removed)
22
+ // false: app/(admin)/[id]/page.tsx → app/admin/id/page (brackets removed, names kept)
23
+ {{/addComments}}
24
+ removeBracketedDirectories: true,
25
+ {{#addComments}}
26
+ // Case for namespace. Options: 'kebab', 'camel', 'pascal', 'snake'
27
+ {{/addComments}}
28
+ namespaceCase: 'kebab',
29
+ {{#addComments}}
30
+ // Case for path. Options: 'kebab', 'camel', 'pascal', 'snake'
31
+ {{/addComments}}
32
+ pathCase: 'camel',
33
+ {{#addComments}}
34
+ // If namespace equals defaultNamespace, omit it from config (namespace becomes undefined)
35
+ {{/addComments}}
36
+ clearOnDefaultNamespace: true,
37
+ {{#addComments}}
38
+ // Skip these directory names globally when building namespace/path
39
+ {{/addComments}}
40
+ ignoreDirectories: ['core', 'utils', 'helpers'],{{#addComments}}
41
+ /**
42
+ * Advanced: pathRules for hierarchical transformations
43
+ *
44
+ * Special operators:
45
+ * - _: false → ignore current directory, continue with nested rules
46
+ * - '>': 'newName' → rename directory in namespace
47
+ * - '>>': redirect → jump to different namespace
48
+ * - '>>': 'namespace' → use specified namespace, remaining segments become path
49
+ * - '>>': { namespace: 'ui', pathPrefix: 'admin' } → jump to 'ui' namespace with 'admin.' prefix
50
+ * - '>>': '' or null → use current directory as namespace
51
+ *
52
+ * Example usage:
53
+ */
54
+ // pathRules: {
55
+ // app: {
56
+ // dashboard: {
57
+ // _: false, // ignore "dashboard" but continue with nested rules
58
+ // modules: false // also ignore "modules"
59
+ // },
60
+ // admin: {
61
+ // '>': 'management', // rename "admin" to "management"
62
+ // users: false // ignore "users"
63
+ // },
64
+ // components: {
65
+ // '>>': { // jump to 'ui' namespace, prefix all paths with 'components'
66
+ // namespace: 'ui',
67
+ // pathPrefix: 'components'
68
+ // }
69
+ // }
70
+ // }
71
+ // }{{/addComments}}
72
+ });
73
+
@@ -1 +0,0 @@
1
- -