@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 +363 -79
- package/index.js +364 -80
- package/package.json +5 -1
- package/templates/config/config.mustache +180 -0
- package/templates/config/generation-algorithm.mustache +73 -0
- package/templates/config/init-config.mustache +0 -1
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$
|
|
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$
|
|
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
|
|
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.
|
|
1760
|
-
"Configuration file already exists. Please remove the existing configuration file before creating a new
|
|
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
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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
|
|
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.
|
|
1740
|
-
"Configuration file already exists. Please remove the existing configuration file before creating a new
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
-
|