@lang-tag/cli 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/algorithms/config-generation/index.d.ts +7 -0
- package/algorithms/config-generation/path-based-config-generator.d.ts +110 -0
- package/algorithms/import/index.d.ts +7 -0
- package/algorithms/index.cjs +159 -0
- package/algorithms/index.d.ts +7 -0
- package/algorithms/index.js +142 -0
- package/config.d.ts +21 -15
- package/index.cjs +231 -21
- package/index.js +231 -21
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Lang-tag
|
|
1
|
+
# Lang-tag CLI
|
|
2
2
|
|
|
3
3
|
A professional solution for managing translations in modern JavaScript/TypeScript projects, especially those using component-based architectures. `lang-tag` simplifies internationalization by allowing you to define translation keys directly within the components where they are used. Translations become local, callable function objects with full TypeScript support, IntelliSense, and compile-time safety.
|
|
4
4
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Predefined algorithms for onConfigGeneration hook.
|
|
3
|
+
*
|
|
4
|
+
* These algorithms customize how translation tag configurations are generated
|
|
5
|
+
* during collection and regeneration.
|
|
6
|
+
*/
|
|
7
|
+
export { pathBasedConfigGenerator, type PathBasedConfigGeneratorOptions } from './path-based-config-generator.ts';
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { LangTagCLIConfigGenerationEvent } from '../../config.ts';
|
|
2
|
+
export interface PathBasedConfigGeneratorOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Whether to include the filename (without extension) as part of the path segments.
|
|
5
|
+
* @default false
|
|
6
|
+
*/
|
|
7
|
+
includeFileName?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Whether to completely remove folders wrapped in brackets () or [].
|
|
10
|
+
* If false, only the brackets are removed from folder names.
|
|
11
|
+
* @default true
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* - true: 'app/(admin)/users' -> 'app/users'
|
|
15
|
+
* - false: 'app/(admin)/users' -> 'app/admin/users'
|
|
16
|
+
*/
|
|
17
|
+
removeBracketedFolders?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* List of folder names to completely ignore globally.
|
|
20
|
+
* These will be removed from all paths regardless of their position.
|
|
21
|
+
* @default []
|
|
22
|
+
*
|
|
23
|
+
* @example ['src', 'app', 'components']
|
|
24
|
+
*/
|
|
25
|
+
ignoreFolders?: string[];
|
|
26
|
+
/**
|
|
27
|
+
* When true, automatically extracts root folder names from the config.includes patterns
|
|
28
|
+
* and adds them to the ignoreFolders list.
|
|
29
|
+
*
|
|
30
|
+
* @default false
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // With includes: ['src/**\/*.{js,ts,jsx,tsx}']
|
|
34
|
+
* // Automatically ignores: ['src']
|
|
35
|
+
*
|
|
36
|
+
* // With includes: ['(src|app)/**\/*.{js,ts,jsx,tsx}', 'components/**\/*.{jsx,tsx}']
|
|
37
|
+
* // Automatically ignores: ['src', 'app', 'components']
|
|
38
|
+
*/
|
|
39
|
+
ignoreIncludesRootFolders?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Hierarchical structure for ignoring specific folder patterns.
|
|
42
|
+
* Keys represent path segments to match, values indicate what to ignore at that level.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* {
|
|
46
|
+
* 'src': {
|
|
47
|
+
* 'app': true, // ignore 'app' when under 'src'
|
|
48
|
+
* 'features': ['auth', 'admin'] // ignore 'auth' and 'admin' under 'src/features'
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
*/
|
|
52
|
+
ignoreStructured?: Record<string, any>;
|
|
53
|
+
/**
|
|
54
|
+
* Convert the final namespace to lowercase.
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
lowercaseNamespace?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Case transformation to apply to the namespace.
|
|
60
|
+
* Available options: 'camel', 'capital', 'constant', 'dot', 'header', 'kebab',
|
|
61
|
+
* 'lower', 'no', 'param', 'pascal', 'path', 'sentence', 'snake', 'swap', 'title', 'upper'
|
|
62
|
+
* @default undefined (no transformation)
|
|
63
|
+
*/
|
|
64
|
+
namespaceCase?: 'camel' | 'capital' | 'constant' | 'dot' | 'header' | 'kebab' | 'lower' | 'no' | 'param' | 'pascal' | 'path' | 'sentence' | 'snake' | 'swap' | 'title' | 'upper';
|
|
65
|
+
/**
|
|
66
|
+
* Case transformation to apply to the path segments.
|
|
67
|
+
* Available options: 'camel', 'capital', 'constant', 'dot', 'header', 'kebab',
|
|
68
|
+
* 'lower', 'no', 'param', 'pascal', 'path', 'sentence', 'snake', 'swap', 'title', 'upper'
|
|
69
|
+
* @default undefined (no transformation)
|
|
70
|
+
*/
|
|
71
|
+
pathCase?: 'camel' | 'capital' | 'constant' | 'dot' | 'header' | 'kebab' | 'lower' | 'no' | 'param' | 'pascal' | 'path' | 'sentence' | 'snake' | 'swap' | 'title' | 'upper';
|
|
72
|
+
/**
|
|
73
|
+
* Fallback namespace to use when no segments remain after filtering.
|
|
74
|
+
* Defaults to the defaultNamespace from langTagConfig.collect.defaultNamespace if not provided.
|
|
75
|
+
* @default undefined
|
|
76
|
+
*/
|
|
77
|
+
fallbackNamespace?: string;
|
|
78
|
+
/**
|
|
79
|
+
* When true and the generated namespace equals the fallback/default namespace,
|
|
80
|
+
* the namespace will be omitted from the configuration as it's redundant.
|
|
81
|
+
* @default true
|
|
82
|
+
*/
|
|
83
|
+
clearOnDefaultNamespace?: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Automatically generates namespace and path configuration based on file path structure.
|
|
87
|
+
*
|
|
88
|
+
* This algorithm analyzes the relative file path and intelligently extracts namespace
|
|
89
|
+
* and path segments according to configurable rules.
|
|
90
|
+
*
|
|
91
|
+
* @param options - Configuration options for path-based generation
|
|
92
|
+
* @returns A function compatible with LangTagCLIConfig.onConfigGeneration
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* import { pathBasedConfigGenerator } from '@lang-tag/cli/algorithms';
|
|
97
|
+
*
|
|
98
|
+
* export default {
|
|
99
|
+
* onConfigGeneration: pathBasedConfigGenerator({
|
|
100
|
+
* includeFileName: false,
|
|
101
|
+
* removeBracketedFolders: true,
|
|
102
|
+
* ignoreFolders: ['lib', 'utils'],
|
|
103
|
+
* ignoreIncludesRootFolders: true, // Auto-ignores root folders from includes
|
|
104
|
+
* lowercaseNamespace: true,
|
|
105
|
+
* fallbackNamespace: 'common'
|
|
106
|
+
* })
|
|
107
|
+
* };
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare function pathBasedConfigGenerator(options?: PathBasedConfigGeneratorOptions): (event: LangTagCLIConfigGenerationEvent) => Promise<void>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const path = require("pathe");
|
|
4
|
+
const caseLib = require("case");
|
|
5
|
+
function _interopNamespaceDefault(e) {
|
|
6
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
7
|
+
if (e) {
|
|
8
|
+
for (const k in e) {
|
|
9
|
+
if (k !== "default") {
|
|
10
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
11
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: () => e[k]
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
n.default = e;
|
|
19
|
+
return Object.freeze(n);
|
|
20
|
+
}
|
|
21
|
+
const caseLib__namespace = /* @__PURE__ */ _interopNamespaceDefault(caseLib);
|
|
22
|
+
function pathBasedConfigGenerator(options = {}) {
|
|
23
|
+
const {
|
|
24
|
+
includeFileName = false,
|
|
25
|
+
removeBracketedFolders = true,
|
|
26
|
+
ignoreFolders = [],
|
|
27
|
+
ignoreIncludesRootFolders = false,
|
|
28
|
+
ignoreStructured = {},
|
|
29
|
+
lowercaseNamespace = false,
|
|
30
|
+
namespaceCase,
|
|
31
|
+
pathCase,
|
|
32
|
+
fallbackNamespace,
|
|
33
|
+
clearOnDefaultNamespace = true
|
|
34
|
+
} = options;
|
|
35
|
+
return async (event) => {
|
|
36
|
+
const { relativePath, langTagConfig } = event;
|
|
37
|
+
const actualFallbackNamespace = fallbackNamespace ?? langTagConfig.collect?.defaultNamespace;
|
|
38
|
+
let finalIgnoreFolders = [...ignoreFolders];
|
|
39
|
+
if (ignoreIncludesRootFolders && langTagConfig.includes) {
|
|
40
|
+
const extractedFolders = extractRootFoldersFromIncludes(langTagConfig.includes);
|
|
41
|
+
finalIgnoreFolders = [.../* @__PURE__ */ new Set([...finalIgnoreFolders, ...extractedFolders])];
|
|
42
|
+
}
|
|
43
|
+
let pathSegments = relativePath.split(path.sep).filter(Boolean);
|
|
44
|
+
if (pathSegments.length === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const fileName = pathSegments[pathSegments.length - 1];
|
|
48
|
+
const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, "");
|
|
49
|
+
if (includeFileName) {
|
|
50
|
+
pathSegments[pathSegments.length - 1] = fileNameWithoutExt;
|
|
51
|
+
} else {
|
|
52
|
+
pathSegments = pathSegments.slice(0, -1);
|
|
53
|
+
}
|
|
54
|
+
pathSegments = pathSegments.map((segment) => {
|
|
55
|
+
const bracketMatch = segment.match(/^[\(\[](.+)[\)\]]$/);
|
|
56
|
+
if (bracketMatch) {
|
|
57
|
+
return removeBracketedFolders ? null : bracketMatch[1];
|
|
58
|
+
}
|
|
59
|
+
return segment;
|
|
60
|
+
}).filter((seg) => seg !== null);
|
|
61
|
+
pathSegments = applyStructuredIgnore(pathSegments, ignoreStructured);
|
|
62
|
+
pathSegments = pathSegments.filter((seg) => !finalIgnoreFolders.includes(seg));
|
|
63
|
+
let namespace;
|
|
64
|
+
let path$1;
|
|
65
|
+
if (pathSegments.length >= 1) {
|
|
66
|
+
namespace = pathSegments[0];
|
|
67
|
+
if (pathSegments.length > 1) {
|
|
68
|
+
path$1 = pathSegments.slice(1).join(".");
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
namespace = actualFallbackNamespace;
|
|
72
|
+
}
|
|
73
|
+
if (namespace) {
|
|
74
|
+
if (lowercaseNamespace) {
|
|
75
|
+
namespace = namespace.toLowerCase();
|
|
76
|
+
}
|
|
77
|
+
if (namespaceCase) {
|
|
78
|
+
namespace = applyCaseTransform(namespace, namespaceCase);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (path$1 && pathCase) {
|
|
82
|
+
const pathParts = path$1.split(".");
|
|
83
|
+
const transformedParts = pathParts.map((part) => applyCaseTransform(part, pathCase));
|
|
84
|
+
path$1 = transformedParts.join(".");
|
|
85
|
+
}
|
|
86
|
+
const newConfig = {};
|
|
87
|
+
if (clearOnDefaultNamespace && namespace === actualFallbackNamespace) {
|
|
88
|
+
if (path$1) {
|
|
89
|
+
newConfig.path = path$1;
|
|
90
|
+
} else {
|
|
91
|
+
event.save(void 0);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
if (namespace) {
|
|
96
|
+
newConfig.namespace = namespace;
|
|
97
|
+
}
|
|
98
|
+
if (path$1) {
|
|
99
|
+
newConfig.path = path$1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (Object.keys(newConfig).length > 0) {
|
|
103
|
+
event.save(newConfig);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function applyStructuredIgnore(segments, structure) {
|
|
108
|
+
const result = [];
|
|
109
|
+
let currentStructure = structure;
|
|
110
|
+
for (let i = 0; i < segments.length; i++) {
|
|
111
|
+
const segment = segments[i];
|
|
112
|
+
if (segment in currentStructure) {
|
|
113
|
+
const rule = currentStructure[segment];
|
|
114
|
+
if (rule === true) {
|
|
115
|
+
currentStructure = structure;
|
|
116
|
+
continue;
|
|
117
|
+
} else if (Array.isArray(rule)) {
|
|
118
|
+
result.push(segment);
|
|
119
|
+
if (i + 1 < segments.length && rule.includes(segments[i + 1])) {
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
currentStructure = structure;
|
|
123
|
+
continue;
|
|
124
|
+
} else if (typeof rule === "object" && rule !== null) {
|
|
125
|
+
result.push(segment);
|
|
126
|
+
currentStructure = rule;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
result.push(segment);
|
|
131
|
+
currentStructure = structure;
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
function applyCaseTransform(str, caseType) {
|
|
136
|
+
const caseFunction = caseLib__namespace[caseType];
|
|
137
|
+
if (typeof caseFunction === "function") {
|
|
138
|
+
return caseFunction(str);
|
|
139
|
+
}
|
|
140
|
+
return str;
|
|
141
|
+
}
|
|
142
|
+
function extractRootFoldersFromIncludes(includes) {
|
|
143
|
+
const folders = /* @__PURE__ */ new Set();
|
|
144
|
+
for (const pattern of includes) {
|
|
145
|
+
let cleanPattern = pattern.replace(/^\.\//, "");
|
|
146
|
+
const match = cleanPattern.match(/^([^/]+)/);
|
|
147
|
+
if (!match) continue;
|
|
148
|
+
const firstSegment = match[1];
|
|
149
|
+
const groupMatch = firstSegment.match(/^[\(\[]([^\)\]]+)[\)\]]$/);
|
|
150
|
+
if (groupMatch) {
|
|
151
|
+
const groupFolders = groupMatch[1].split("|").map((f) => f.trim());
|
|
152
|
+
groupFolders.forEach((folder) => folders.add(folder));
|
|
153
|
+
} else {
|
|
154
|
+
folders.add(firstSegment);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return Array.from(folders);
|
|
158
|
+
}
|
|
159
|
+
exports.pathBasedConfigGenerator = pathBasedConfigGenerator;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Predefined algorithms for lang-tag-cli configuration.
|
|
3
|
+
*
|
|
4
|
+
* These algorithms can be used in your lang-tag-cli config file
|
|
5
|
+
* to customize how tags are processed during collection and regeneration.
|
|
6
|
+
*/
|
|
7
|
+
export { pathBasedConfigGenerator, type PathBasedConfigGeneratorOptions } from './config-generation/index.ts';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { sep } from "pathe";
|
|
2
|
+
import * as caseLib from "case";
|
|
3
|
+
function pathBasedConfigGenerator(options = {}) {
|
|
4
|
+
const {
|
|
5
|
+
includeFileName = false,
|
|
6
|
+
removeBracketedFolders = true,
|
|
7
|
+
ignoreFolders = [],
|
|
8
|
+
ignoreIncludesRootFolders = false,
|
|
9
|
+
ignoreStructured = {},
|
|
10
|
+
lowercaseNamespace = false,
|
|
11
|
+
namespaceCase,
|
|
12
|
+
pathCase,
|
|
13
|
+
fallbackNamespace,
|
|
14
|
+
clearOnDefaultNamespace = true
|
|
15
|
+
} = options;
|
|
16
|
+
return async (event) => {
|
|
17
|
+
const { relativePath, langTagConfig } = event;
|
|
18
|
+
const actualFallbackNamespace = fallbackNamespace ?? langTagConfig.collect?.defaultNamespace;
|
|
19
|
+
let finalIgnoreFolders = [...ignoreFolders];
|
|
20
|
+
if (ignoreIncludesRootFolders && langTagConfig.includes) {
|
|
21
|
+
const extractedFolders = extractRootFoldersFromIncludes(langTagConfig.includes);
|
|
22
|
+
finalIgnoreFolders = [.../* @__PURE__ */ new Set([...finalIgnoreFolders, ...extractedFolders])];
|
|
23
|
+
}
|
|
24
|
+
let pathSegments = relativePath.split(sep).filter(Boolean);
|
|
25
|
+
if (pathSegments.length === 0) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const fileName = pathSegments[pathSegments.length - 1];
|
|
29
|
+
const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, "");
|
|
30
|
+
if (includeFileName) {
|
|
31
|
+
pathSegments[pathSegments.length - 1] = fileNameWithoutExt;
|
|
32
|
+
} else {
|
|
33
|
+
pathSegments = pathSegments.slice(0, -1);
|
|
34
|
+
}
|
|
35
|
+
pathSegments = pathSegments.map((segment) => {
|
|
36
|
+
const bracketMatch = segment.match(/^[\(\[](.+)[\)\]]$/);
|
|
37
|
+
if (bracketMatch) {
|
|
38
|
+
return removeBracketedFolders ? null : bracketMatch[1];
|
|
39
|
+
}
|
|
40
|
+
return segment;
|
|
41
|
+
}).filter((seg) => seg !== null);
|
|
42
|
+
pathSegments = applyStructuredIgnore(pathSegments, ignoreStructured);
|
|
43
|
+
pathSegments = pathSegments.filter((seg) => !finalIgnoreFolders.includes(seg));
|
|
44
|
+
let namespace;
|
|
45
|
+
let path;
|
|
46
|
+
if (pathSegments.length >= 1) {
|
|
47
|
+
namespace = pathSegments[0];
|
|
48
|
+
if (pathSegments.length > 1) {
|
|
49
|
+
path = pathSegments.slice(1).join(".");
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
namespace = actualFallbackNamespace;
|
|
53
|
+
}
|
|
54
|
+
if (namespace) {
|
|
55
|
+
if (lowercaseNamespace) {
|
|
56
|
+
namespace = namespace.toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
if (namespaceCase) {
|
|
59
|
+
namespace = applyCaseTransform(namespace, namespaceCase);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (path && pathCase) {
|
|
63
|
+
const pathParts = path.split(".");
|
|
64
|
+
const transformedParts = pathParts.map((part) => applyCaseTransform(part, pathCase));
|
|
65
|
+
path = transformedParts.join(".");
|
|
66
|
+
}
|
|
67
|
+
const newConfig = {};
|
|
68
|
+
if (clearOnDefaultNamespace && namespace === actualFallbackNamespace) {
|
|
69
|
+
if (path) {
|
|
70
|
+
newConfig.path = path;
|
|
71
|
+
} else {
|
|
72
|
+
event.save(void 0);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
if (namespace) {
|
|
77
|
+
newConfig.namespace = namespace;
|
|
78
|
+
}
|
|
79
|
+
if (path) {
|
|
80
|
+
newConfig.path = path;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (Object.keys(newConfig).length > 0) {
|
|
84
|
+
event.save(newConfig);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function applyStructuredIgnore(segments, structure) {
|
|
89
|
+
const result = [];
|
|
90
|
+
let currentStructure = structure;
|
|
91
|
+
for (let i = 0; i < segments.length; i++) {
|
|
92
|
+
const segment = segments[i];
|
|
93
|
+
if (segment in currentStructure) {
|
|
94
|
+
const rule = currentStructure[segment];
|
|
95
|
+
if (rule === true) {
|
|
96
|
+
currentStructure = structure;
|
|
97
|
+
continue;
|
|
98
|
+
} else if (Array.isArray(rule)) {
|
|
99
|
+
result.push(segment);
|
|
100
|
+
if (i + 1 < segments.length && rule.includes(segments[i + 1])) {
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
currentStructure = structure;
|
|
104
|
+
continue;
|
|
105
|
+
} else if (typeof rule === "object" && rule !== null) {
|
|
106
|
+
result.push(segment);
|
|
107
|
+
currentStructure = rule;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
result.push(segment);
|
|
112
|
+
currentStructure = structure;
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
function applyCaseTransform(str, caseType) {
|
|
117
|
+
const caseFunction = caseLib[caseType];
|
|
118
|
+
if (typeof caseFunction === "function") {
|
|
119
|
+
return caseFunction(str);
|
|
120
|
+
}
|
|
121
|
+
return str;
|
|
122
|
+
}
|
|
123
|
+
function extractRootFoldersFromIncludes(includes) {
|
|
124
|
+
const folders = /* @__PURE__ */ new Set();
|
|
125
|
+
for (const pattern of includes) {
|
|
126
|
+
let cleanPattern = pattern.replace(/^\.\//, "");
|
|
127
|
+
const match = cleanPattern.match(/^([^/]+)/);
|
|
128
|
+
if (!match) continue;
|
|
129
|
+
const firstSegment = match[1];
|
|
130
|
+
const groupMatch = firstSegment.match(/^[\(\[]([^\)\]]+)[\)\]]$/);
|
|
131
|
+
if (groupMatch) {
|
|
132
|
+
const groupFolders = groupMatch[1].split("|").map((f) => f.trim());
|
|
133
|
+
groupFolders.forEach((folder) => folders.add(folder));
|
|
134
|
+
} else {
|
|
135
|
+
folders.add(firstSegment);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return Array.from(folders);
|
|
139
|
+
}
|
|
140
|
+
export {
|
|
141
|
+
pathBasedConfigGenerator
|
|
142
|
+
};
|
package/config.d.ts
CHANGED
|
@@ -98,9 +98,12 @@ export interface LangTagCLIConfig {
|
|
|
98
98
|
* A function called for each found lang tag before processing.
|
|
99
99
|
* Allows dynamic modification of the tag's configuration (namespace, path, etc.)
|
|
100
100
|
* based on the file path or other context.
|
|
101
|
-
*
|
|
101
|
+
*
|
|
102
|
+
* Changes made inside this function are **applied only if you explicitly call**
|
|
103
|
+
* `event.save(configuration)`. Returning a value or modifying the event object
|
|
104
|
+
* without calling `save()` will **not** update the configuration.
|
|
102
105
|
*/
|
|
103
|
-
onConfigGeneration: (
|
|
106
|
+
onConfigGeneration: (event: LangTagCLIConfigGenerationEvent) => Promise<void>;
|
|
104
107
|
debug?: boolean;
|
|
105
108
|
}
|
|
106
109
|
/**
|
|
@@ -131,19 +134,6 @@ export interface LangTagCLIOnImportActions {
|
|
|
131
134
|
/** Sets the configuration for the currently imported tag. */
|
|
132
135
|
setConfig: (config: LangTagTranslationsConfig) => void;
|
|
133
136
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Parameters passed to the `onConfigGeneration` configuration function.
|
|
136
|
-
*/
|
|
137
|
-
export interface LangTagCLIOnConfigGenerationParams {
|
|
138
|
-
/** The absolute path to the source file being processed. */
|
|
139
|
-
fullPath: string;
|
|
140
|
-
/** The path of the source file relative to the project root (where the command was invoked). */
|
|
141
|
-
path: string;
|
|
142
|
-
/** True if the file being processed is located within the configured library import directory (`config.import.dir`). */
|
|
143
|
-
isImportedLibrary: boolean;
|
|
144
|
-
/** The configuration object extracted from the lang tag's options argument (e.g., `{ namespace: 'common', path: 'my.path' }`). */
|
|
145
|
-
config: LangTagTranslationsConfig;
|
|
146
|
-
}
|
|
147
137
|
type Validity = 'ok' | 'invalid-param-1' | 'invalid-param-2' | 'translations-not-found';
|
|
148
138
|
export interface LangTagCLIProcessedTag {
|
|
149
139
|
fullMatch: string;
|
|
@@ -171,6 +161,22 @@ export interface LangTagCLIConflict {
|
|
|
171
161
|
tagB: LangTagCLITagConflictInfo;
|
|
172
162
|
conflictType: 'path_overwrite' | 'type_mismatch';
|
|
173
163
|
}
|
|
164
|
+
export interface LangTagCLIConfigGenerationEvent {
|
|
165
|
+
/** The absolute path to the source file being processed. */
|
|
166
|
+
absolutePath: string;
|
|
167
|
+
/** The path of the source file relative to the project root (where the command was invoked). */
|
|
168
|
+
relativePath: string;
|
|
169
|
+
/** True if the file being processed is located within the configured library import directory (`config.import.dir`). */
|
|
170
|
+
isImportedLibrary: boolean;
|
|
171
|
+
/** The configuration object extracted from the lang tag's options argument (e.g., `{ namespace: 'common', path: 'my.path' }`). */
|
|
172
|
+
config: LangTagTranslationsConfig | undefined;
|
|
173
|
+
langTagConfig: LangTagCLIConfig;
|
|
174
|
+
/**
|
|
175
|
+
* Tells CLI to replace tag configuration
|
|
176
|
+
* undefined = means configuration will be removed
|
|
177
|
+
**/
|
|
178
|
+
save(config: LangTagTranslationsConfig | undefined): void;
|
|
179
|
+
}
|
|
174
180
|
export interface LangTagCLICollectConfigFixEvent {
|
|
175
181
|
config: LangTagTranslationsConfig;
|
|
176
182
|
langTagConfig: LangTagCLIConfig;
|
package/index.cjs
CHANGED
|
@@ -43,6 +43,7 @@ class $LT_TagProcessor {
|
|
|
43
43
|
const optionalVariableAssignment = `(?:\\s*(\\w+)\\s*=\\s*)?`;
|
|
44
44
|
const matches = [];
|
|
45
45
|
let currentIndex = 0;
|
|
46
|
+
const skipRanges = this.buildSkipRanges(fileContent);
|
|
46
47
|
const startPattern = new RegExp(`${optionalVariableAssignment}${tagName}\\(\\s*\\{`, "g");
|
|
47
48
|
while (true) {
|
|
48
49
|
startPattern.lastIndex = currentIndex;
|
|
@@ -50,6 +51,10 @@ class $LT_TagProcessor {
|
|
|
50
51
|
if (!startMatch) break;
|
|
51
52
|
const matchStartIndex = startMatch.index;
|
|
52
53
|
const variableName = startMatch[1] || void 0;
|
|
54
|
+
if (this.isInSkipRange(matchStartIndex, skipRanges)) {
|
|
55
|
+
currentIndex = matchStartIndex + 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
53
58
|
let braceCount = 1;
|
|
54
59
|
let i = matchStartIndex + startMatch[0].length;
|
|
55
60
|
while (i < fileContent.length && braceCount > 0) {
|
|
@@ -115,17 +120,29 @@ class $LT_TagProcessor {
|
|
|
115
120
|
let validity = "ok";
|
|
116
121
|
let parameter1 = void 0;
|
|
117
122
|
let parameter2 = void 0;
|
|
123
|
+
if (this.hasTemplateInterpolation(parameter1Text) || parameter2Text && this.hasTemplateInterpolation(parameter2Text)) {
|
|
124
|
+
currentIndex = matchStartIndex + 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
118
127
|
try {
|
|
119
128
|
parameter1 = JSON5.parse(parameter1Text);
|
|
120
129
|
if (parameter2Text) {
|
|
121
130
|
try {
|
|
122
131
|
parameter2 = JSON5.parse(parameter2Text);
|
|
123
132
|
} catch (error) {
|
|
124
|
-
|
|
133
|
+
try {
|
|
134
|
+
parameter2 = JSON5.parse(this.escapeNewlinesInStrings(parameter2Text));
|
|
135
|
+
} catch {
|
|
136
|
+
validity = "invalid-param-2";
|
|
137
|
+
}
|
|
125
138
|
}
|
|
126
139
|
}
|
|
127
140
|
} catch (error) {
|
|
128
|
-
|
|
141
|
+
try {
|
|
142
|
+
parameter1 = JSON5.parse(this.escapeNewlinesInStrings(parameter1Text));
|
|
143
|
+
} catch {
|
|
144
|
+
validity = "invalid-param-1";
|
|
145
|
+
}
|
|
129
146
|
}
|
|
130
147
|
let parameterTranslations = this.config.translationArgPosition === 1 ? parameter1 : parameter2;
|
|
131
148
|
let parameterConfig = this.config.translationArgPosition === 1 ? parameter2 : parameter1;
|
|
@@ -191,6 +208,180 @@ class $LT_TagProcessor {
|
|
|
191
208
|
});
|
|
192
209
|
return fileContent;
|
|
193
210
|
}
|
|
211
|
+
buildSkipRanges(fileContent) {
|
|
212
|
+
const ranges = [];
|
|
213
|
+
let i = 0;
|
|
214
|
+
while (i < fileContent.length) {
|
|
215
|
+
const char = fileContent[i];
|
|
216
|
+
const nextChar = fileContent[i + 1];
|
|
217
|
+
if (char === "/" && nextChar === "/") {
|
|
218
|
+
const start = i;
|
|
219
|
+
i += 2;
|
|
220
|
+
while (i < fileContent.length && fileContent[i] !== "\n") {
|
|
221
|
+
i++;
|
|
222
|
+
}
|
|
223
|
+
ranges.push([start, i]);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (char === "/" && nextChar === "*") {
|
|
227
|
+
const start = i;
|
|
228
|
+
i += 2;
|
|
229
|
+
while (i < fileContent.length - 1) {
|
|
230
|
+
if (fileContent[i] === "*" && fileContent[i + 1] === "/") {
|
|
231
|
+
i += 2;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
i++;
|
|
235
|
+
}
|
|
236
|
+
ranges.push([start, i]);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (char === "'") {
|
|
240
|
+
const start = i;
|
|
241
|
+
i++;
|
|
242
|
+
while (i < fileContent.length) {
|
|
243
|
+
if (fileContent[i] === "\\") {
|
|
244
|
+
i += 2;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (fileContent[i] === "'") {
|
|
248
|
+
i++;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
i++;
|
|
252
|
+
}
|
|
253
|
+
ranges.push([start, i]);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (char === '"') {
|
|
257
|
+
const start = i;
|
|
258
|
+
i++;
|
|
259
|
+
while (i < fileContent.length) {
|
|
260
|
+
if (fileContent[i] === "\\") {
|
|
261
|
+
i += 2;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (fileContent[i] === '"') {
|
|
265
|
+
i++;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
i++;
|
|
269
|
+
}
|
|
270
|
+
ranges.push([start, i]);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (char === "`") {
|
|
274
|
+
const start = i;
|
|
275
|
+
i++;
|
|
276
|
+
while (i < fileContent.length) {
|
|
277
|
+
if (fileContent[i] === "\\") {
|
|
278
|
+
i += 2;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (fileContent[i] === "`") {
|
|
282
|
+
i++;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
i++;
|
|
286
|
+
}
|
|
287
|
+
ranges.push([start, i]);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
i++;
|
|
291
|
+
}
|
|
292
|
+
return ranges;
|
|
293
|
+
}
|
|
294
|
+
isInSkipRange(position, skipRanges) {
|
|
295
|
+
for (const [start, end] of skipRanges) {
|
|
296
|
+
if (position >= start && position < end) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
hasTemplateInterpolation(text) {
|
|
303
|
+
let i = 0;
|
|
304
|
+
while (i < text.length) {
|
|
305
|
+
if (text[i] === "`") {
|
|
306
|
+
i++;
|
|
307
|
+
while (i < text.length) {
|
|
308
|
+
if (text[i] === "\\") {
|
|
309
|
+
i += 2;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (text[i] === "$" && text[i + 1] === "{") {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (text[i] === "`") {
|
|
316
|
+
i++;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
i++;
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
i++;
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
escapeNewlinesInStrings(text) {
|
|
328
|
+
let result = "";
|
|
329
|
+
let i = 0;
|
|
330
|
+
while (i < text.length) {
|
|
331
|
+
const char = text[i];
|
|
332
|
+
if (char === '"') {
|
|
333
|
+
result += char;
|
|
334
|
+
i++;
|
|
335
|
+
while (i < text.length) {
|
|
336
|
+
if (text[i] === "\\") {
|
|
337
|
+
result += text[i] + text[i + 1];
|
|
338
|
+
i += 2;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (text[i] === "\n") {
|
|
342
|
+
result += "\\n";
|
|
343
|
+
i++;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (text[i] === '"') {
|
|
347
|
+
result += text[i];
|
|
348
|
+
i++;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
result += text[i];
|
|
352
|
+
i++;
|
|
353
|
+
}
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (char === "'") {
|
|
357
|
+
result += char;
|
|
358
|
+
i++;
|
|
359
|
+
while (i < text.length) {
|
|
360
|
+
if (text[i] === "\\") {
|
|
361
|
+
result += text[i] + text[i + 1];
|
|
362
|
+
i += 2;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (text[i] === "\n") {
|
|
366
|
+
result += "\\n";
|
|
367
|
+
i++;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (text[i] === "'") {
|
|
371
|
+
result += text[i];
|
|
372
|
+
i++;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
result += text[i];
|
|
376
|
+
i++;
|
|
377
|
+
}
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
result += char;
|
|
381
|
+
i++;
|
|
382
|
+
}
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
194
385
|
}
|
|
195
386
|
function getLineAndColumn(text, matchIndex) {
|
|
196
387
|
const lines = text.slice(0, matchIndex).split("\n");
|
|
@@ -247,13 +438,20 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
|
|
|
247
438
|
}
|
|
248
439
|
const replacements = [];
|
|
249
440
|
for (let tag of tags) {
|
|
250
|
-
|
|
441
|
+
let newConfig = void 0;
|
|
442
|
+
let shouldUpdate = false;
|
|
443
|
+
await config.onConfigGeneration({
|
|
444
|
+
langTagConfig: config,
|
|
251
445
|
config: tag.parameterConfig,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
isImportedLibrary: path$12.startsWith(libraryImportsDir)
|
|
446
|
+
absolutePath: file,
|
|
447
|
+
relativePath: path$12,
|
|
448
|
+
isImportedLibrary: path$12.startsWith(libraryImportsDir),
|
|
449
|
+
save: (updatedConfig) => {
|
|
450
|
+
newConfig = updatedConfig;
|
|
451
|
+
shouldUpdate = true;
|
|
452
|
+
}
|
|
255
453
|
});
|
|
256
|
-
if (
|
|
454
|
+
if (!shouldUpdate) {
|
|
257
455
|
continue;
|
|
258
456
|
}
|
|
259
457
|
if (JSON5.stringify(tag.parameterConfig) !== JSON5.stringify(newConfig)) {
|
|
@@ -304,7 +502,8 @@ const LANG_TAG_DEFAULT_CONFIG = {
|
|
|
304
502
|
isLibrary: false,
|
|
305
503
|
language: "en",
|
|
306
504
|
translationArgPosition: 1,
|
|
307
|
-
onConfigGeneration: (
|
|
505
|
+
onConfigGeneration: async (event) => {
|
|
506
|
+
}
|
|
308
507
|
};
|
|
309
508
|
async function $LT_ReadConfig(projectPath) {
|
|
310
509
|
const configPath = path$1.resolve(projectPath, CONFIG_FILE_NAME);
|
|
@@ -1099,6 +1298,11 @@ async function $LT_CMD_Collect(options) {
|
|
|
1099
1298
|
const { config, logger } = await $LT_GetCommandEssentials();
|
|
1100
1299
|
logger.info("Collecting translations from source files...");
|
|
1101
1300
|
const files = await $LT_CollectCandidateFilesWithTags({ config, logger });
|
|
1301
|
+
if (config.debug) {
|
|
1302
|
+
for (let file of files) {
|
|
1303
|
+
logger.debug("Found {count} translations tags inside: {file}", { count: file.tags.length, file: file.relativeFilePath });
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1102
1306
|
if (config.isLibrary) {
|
|
1103
1307
|
await $LT_WriteAsExportFile({ config, logger, files });
|
|
1104
1308
|
return;
|
|
@@ -1211,29 +1415,35 @@ async function detectModuleSystem() {
|
|
|
1211
1415
|
return "cjs";
|
|
1212
1416
|
}
|
|
1213
1417
|
}
|
|
1214
|
-
function getExportStatement(moduleSystem) {
|
|
1215
|
-
return moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
|
|
1216
|
-
}
|
|
1217
1418
|
async function generateDefaultConfig() {
|
|
1218
1419
|
const moduleSystem = await detectModuleSystem();
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1420
|
+
const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator } = require('@lang-tag/cli/algorithms');`;
|
|
1421
|
+
const exportStatement = moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
|
|
1422
|
+
return `${importStatement}
|
|
1423
|
+
|
|
1424
|
+
const generationAlgorithm = pathBasedConfigGenerator({
|
|
1425
|
+
ignoreIncludesRootFolders: true,
|
|
1426
|
+
removeBracketedFolders: true,
|
|
1427
|
+
namespaceCase: 'kebab',
|
|
1428
|
+
pathCase: 'camel',
|
|
1429
|
+
clearOnDefaultNamespace: true,
|
|
1430
|
+
ignoreFolders: ['core', 'utils', 'helpers']
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
/** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
|
|
1221
1434
|
const config = {
|
|
1222
1435
|
tagName: 'lang',
|
|
1223
1436
|
isLibrary: false,
|
|
1224
1437
|
includes: ['src/**/*.{js,ts,jsx,tsx}'],
|
|
1225
1438
|
excludes: ['node_modules', 'dist', 'build', '**/*.test.ts'],
|
|
1226
1439
|
outputDir: 'public/locales/en',
|
|
1227
|
-
onConfigGeneration:
|
|
1440
|
+
onConfigGeneration: async event => {
|
|
1228
1441
|
// We do not modify imported configurations
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1231
|
-
//if (!params.config.path) {
|
|
1232
|
-
// params.config.path = 'test';
|
|
1233
|
-
// params.config.namespace = 'testNamespace';
|
|
1234
|
-
//}
|
|
1442
|
+
if (event.isImportedLibrary) return;
|
|
1235
1443
|
|
|
1236
|
-
return
|
|
1444
|
+
if (event.config?.manual) return;
|
|
1445
|
+
|
|
1446
|
+
await generationAlgorithm(event);
|
|
1237
1447
|
},
|
|
1238
1448
|
collect: {
|
|
1239
1449
|
defaultNamespace: 'common',
|
package/index.js
CHANGED
|
@@ -23,6 +23,7 @@ class $LT_TagProcessor {
|
|
|
23
23
|
const optionalVariableAssignment = `(?:\\s*(\\w+)\\s*=\\s*)?`;
|
|
24
24
|
const matches = [];
|
|
25
25
|
let currentIndex = 0;
|
|
26
|
+
const skipRanges = this.buildSkipRanges(fileContent);
|
|
26
27
|
const startPattern = new RegExp(`${optionalVariableAssignment}${tagName}\\(\\s*\\{`, "g");
|
|
27
28
|
while (true) {
|
|
28
29
|
startPattern.lastIndex = currentIndex;
|
|
@@ -30,6 +31,10 @@ class $LT_TagProcessor {
|
|
|
30
31
|
if (!startMatch) break;
|
|
31
32
|
const matchStartIndex = startMatch.index;
|
|
32
33
|
const variableName = startMatch[1] || void 0;
|
|
34
|
+
if (this.isInSkipRange(matchStartIndex, skipRanges)) {
|
|
35
|
+
currentIndex = matchStartIndex + 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
33
38
|
let braceCount = 1;
|
|
34
39
|
let i = matchStartIndex + startMatch[0].length;
|
|
35
40
|
while (i < fileContent.length && braceCount > 0) {
|
|
@@ -95,17 +100,29 @@ class $LT_TagProcessor {
|
|
|
95
100
|
let validity = "ok";
|
|
96
101
|
let parameter1 = void 0;
|
|
97
102
|
let parameter2 = void 0;
|
|
103
|
+
if (this.hasTemplateInterpolation(parameter1Text) || parameter2Text && this.hasTemplateInterpolation(parameter2Text)) {
|
|
104
|
+
currentIndex = matchStartIndex + 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
98
107
|
try {
|
|
99
108
|
parameter1 = JSON5.parse(parameter1Text);
|
|
100
109
|
if (parameter2Text) {
|
|
101
110
|
try {
|
|
102
111
|
parameter2 = JSON5.parse(parameter2Text);
|
|
103
112
|
} catch (error) {
|
|
104
|
-
|
|
113
|
+
try {
|
|
114
|
+
parameter2 = JSON5.parse(this.escapeNewlinesInStrings(parameter2Text));
|
|
115
|
+
} catch {
|
|
116
|
+
validity = "invalid-param-2";
|
|
117
|
+
}
|
|
105
118
|
}
|
|
106
119
|
}
|
|
107
120
|
} catch (error) {
|
|
108
|
-
|
|
121
|
+
try {
|
|
122
|
+
parameter1 = JSON5.parse(this.escapeNewlinesInStrings(parameter1Text));
|
|
123
|
+
} catch {
|
|
124
|
+
validity = "invalid-param-1";
|
|
125
|
+
}
|
|
109
126
|
}
|
|
110
127
|
let parameterTranslations = this.config.translationArgPosition === 1 ? parameter1 : parameter2;
|
|
111
128
|
let parameterConfig = this.config.translationArgPosition === 1 ? parameter2 : parameter1;
|
|
@@ -171,6 +188,180 @@ class $LT_TagProcessor {
|
|
|
171
188
|
});
|
|
172
189
|
return fileContent;
|
|
173
190
|
}
|
|
191
|
+
buildSkipRanges(fileContent) {
|
|
192
|
+
const ranges = [];
|
|
193
|
+
let i = 0;
|
|
194
|
+
while (i < fileContent.length) {
|
|
195
|
+
const char = fileContent[i];
|
|
196
|
+
const nextChar = fileContent[i + 1];
|
|
197
|
+
if (char === "/" && nextChar === "/") {
|
|
198
|
+
const start = i;
|
|
199
|
+
i += 2;
|
|
200
|
+
while (i < fileContent.length && fileContent[i] !== "\n") {
|
|
201
|
+
i++;
|
|
202
|
+
}
|
|
203
|
+
ranges.push([start, i]);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (char === "/" && nextChar === "*") {
|
|
207
|
+
const start = i;
|
|
208
|
+
i += 2;
|
|
209
|
+
while (i < fileContent.length - 1) {
|
|
210
|
+
if (fileContent[i] === "*" && fileContent[i + 1] === "/") {
|
|
211
|
+
i += 2;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
i++;
|
|
215
|
+
}
|
|
216
|
+
ranges.push([start, i]);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (char === "'") {
|
|
220
|
+
const start = i;
|
|
221
|
+
i++;
|
|
222
|
+
while (i < fileContent.length) {
|
|
223
|
+
if (fileContent[i] === "\\") {
|
|
224
|
+
i += 2;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (fileContent[i] === "'") {
|
|
228
|
+
i++;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
i++;
|
|
232
|
+
}
|
|
233
|
+
ranges.push([start, i]);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (char === '"') {
|
|
237
|
+
const start = i;
|
|
238
|
+
i++;
|
|
239
|
+
while (i < fileContent.length) {
|
|
240
|
+
if (fileContent[i] === "\\") {
|
|
241
|
+
i += 2;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (fileContent[i] === '"') {
|
|
245
|
+
i++;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
i++;
|
|
249
|
+
}
|
|
250
|
+
ranges.push([start, i]);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (char === "`") {
|
|
254
|
+
const start = i;
|
|
255
|
+
i++;
|
|
256
|
+
while (i < fileContent.length) {
|
|
257
|
+
if (fileContent[i] === "\\") {
|
|
258
|
+
i += 2;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (fileContent[i] === "`") {
|
|
262
|
+
i++;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
i++;
|
|
266
|
+
}
|
|
267
|
+
ranges.push([start, i]);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
i++;
|
|
271
|
+
}
|
|
272
|
+
return ranges;
|
|
273
|
+
}
|
|
274
|
+
isInSkipRange(position, skipRanges) {
|
|
275
|
+
for (const [start, end] of skipRanges) {
|
|
276
|
+
if (position >= start && position < end) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
hasTemplateInterpolation(text) {
|
|
283
|
+
let i = 0;
|
|
284
|
+
while (i < text.length) {
|
|
285
|
+
if (text[i] === "`") {
|
|
286
|
+
i++;
|
|
287
|
+
while (i < text.length) {
|
|
288
|
+
if (text[i] === "\\") {
|
|
289
|
+
i += 2;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (text[i] === "$" && text[i + 1] === "{") {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
if (text[i] === "`") {
|
|
296
|
+
i++;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
i++;
|
|
300
|
+
}
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
i++;
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
escapeNewlinesInStrings(text) {
|
|
308
|
+
let result = "";
|
|
309
|
+
let i = 0;
|
|
310
|
+
while (i < text.length) {
|
|
311
|
+
const char = text[i];
|
|
312
|
+
if (char === '"') {
|
|
313
|
+
result += char;
|
|
314
|
+
i++;
|
|
315
|
+
while (i < text.length) {
|
|
316
|
+
if (text[i] === "\\") {
|
|
317
|
+
result += text[i] + text[i + 1];
|
|
318
|
+
i += 2;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (text[i] === "\n") {
|
|
322
|
+
result += "\\n";
|
|
323
|
+
i++;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (text[i] === '"') {
|
|
327
|
+
result += text[i];
|
|
328
|
+
i++;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
result += text[i];
|
|
332
|
+
i++;
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (char === "'") {
|
|
337
|
+
result += char;
|
|
338
|
+
i++;
|
|
339
|
+
while (i < text.length) {
|
|
340
|
+
if (text[i] === "\\") {
|
|
341
|
+
result += text[i] + text[i + 1];
|
|
342
|
+
i += 2;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (text[i] === "\n") {
|
|
346
|
+
result += "\\n";
|
|
347
|
+
i++;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (text[i] === "'") {
|
|
351
|
+
result += text[i];
|
|
352
|
+
i++;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
result += text[i];
|
|
356
|
+
i++;
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
result += char;
|
|
361
|
+
i++;
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
174
365
|
}
|
|
175
366
|
function getLineAndColumn(text, matchIndex) {
|
|
176
367
|
const lines = text.slice(0, matchIndex).split("\n");
|
|
@@ -227,13 +418,20 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
|
|
|
227
418
|
}
|
|
228
419
|
const replacements = [];
|
|
229
420
|
for (let tag of tags) {
|
|
230
|
-
|
|
421
|
+
let newConfig = void 0;
|
|
422
|
+
let shouldUpdate = false;
|
|
423
|
+
await config.onConfigGeneration({
|
|
424
|
+
langTagConfig: config,
|
|
231
425
|
config: tag.parameterConfig,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
isImportedLibrary: path2.startsWith(libraryImportsDir)
|
|
426
|
+
absolutePath: file,
|
|
427
|
+
relativePath: path2,
|
|
428
|
+
isImportedLibrary: path2.startsWith(libraryImportsDir),
|
|
429
|
+
save: (updatedConfig) => {
|
|
430
|
+
newConfig = updatedConfig;
|
|
431
|
+
shouldUpdate = true;
|
|
432
|
+
}
|
|
235
433
|
});
|
|
236
|
-
if (
|
|
434
|
+
if (!shouldUpdate) {
|
|
237
435
|
continue;
|
|
238
436
|
}
|
|
239
437
|
if (JSON5.stringify(tag.parameterConfig) !== JSON5.stringify(newConfig)) {
|
|
@@ -284,7 +482,8 @@ const LANG_TAG_DEFAULT_CONFIG = {
|
|
|
284
482
|
isLibrary: false,
|
|
285
483
|
language: "en",
|
|
286
484
|
translationArgPosition: 1,
|
|
287
|
-
onConfigGeneration: (
|
|
485
|
+
onConfigGeneration: async (event) => {
|
|
486
|
+
}
|
|
288
487
|
};
|
|
289
488
|
async function $LT_ReadConfig(projectPath) {
|
|
290
489
|
const configPath = resolve(projectPath, CONFIG_FILE_NAME);
|
|
@@ -1079,6 +1278,11 @@ async function $LT_CMD_Collect(options) {
|
|
|
1079
1278
|
const { config, logger } = await $LT_GetCommandEssentials();
|
|
1080
1279
|
logger.info("Collecting translations from source files...");
|
|
1081
1280
|
const files = await $LT_CollectCandidateFilesWithTags({ config, logger });
|
|
1281
|
+
if (config.debug) {
|
|
1282
|
+
for (let file of files) {
|
|
1283
|
+
logger.debug("Found {count} translations tags inside: {file}", { count: file.tags.length, file: file.relativeFilePath });
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1082
1286
|
if (config.isLibrary) {
|
|
1083
1287
|
await $LT_WriteAsExportFile({ config, logger, files });
|
|
1084
1288
|
return;
|
|
@@ -1191,29 +1395,35 @@ async function detectModuleSystem() {
|
|
|
1191
1395
|
return "cjs";
|
|
1192
1396
|
}
|
|
1193
1397
|
}
|
|
1194
|
-
function getExportStatement(moduleSystem) {
|
|
1195
|
-
return moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
|
|
1196
|
-
}
|
|
1197
1398
|
async function generateDefaultConfig() {
|
|
1198
1399
|
const moduleSystem = await detectModuleSystem();
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1400
|
+
const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator } = require('@lang-tag/cli/algorithms');`;
|
|
1401
|
+
const exportStatement = moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
|
|
1402
|
+
return `${importStatement}
|
|
1403
|
+
|
|
1404
|
+
const generationAlgorithm = pathBasedConfigGenerator({
|
|
1405
|
+
ignoreIncludesRootFolders: true,
|
|
1406
|
+
removeBracketedFolders: true,
|
|
1407
|
+
namespaceCase: 'kebab',
|
|
1408
|
+
pathCase: 'camel',
|
|
1409
|
+
clearOnDefaultNamespace: true,
|
|
1410
|
+
ignoreFolders: ['core', 'utils', 'helpers']
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
/** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
|
|
1201
1414
|
const config = {
|
|
1202
1415
|
tagName: 'lang',
|
|
1203
1416
|
isLibrary: false,
|
|
1204
1417
|
includes: ['src/**/*.{js,ts,jsx,tsx}'],
|
|
1205
1418
|
excludes: ['node_modules', 'dist', 'build', '**/*.test.ts'],
|
|
1206
1419
|
outputDir: 'public/locales/en',
|
|
1207
|
-
onConfigGeneration:
|
|
1420
|
+
onConfigGeneration: async event => {
|
|
1208
1421
|
// We do not modify imported configurations
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1211
|
-
//if (!params.config.path) {
|
|
1212
|
-
// params.config.path = 'test';
|
|
1213
|
-
// params.config.namespace = 'testNamespace';
|
|
1214
|
-
//}
|
|
1422
|
+
if (event.isImportedLibrary) return;
|
|
1215
1423
|
|
|
1216
|
-
return
|
|
1424
|
+
if (event.config?.manual) return;
|
|
1425
|
+
|
|
1426
|
+
await generationAlgorithm(event);
|
|
1217
1427
|
},
|
|
1218
1428
|
collect: {
|
|
1219
1429
|
defaultNamespace: 'common',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lang-tag/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
".": {
|
|
23
23
|
"import": "./index.js",
|
|
24
24
|
"require": "./index.cjs"
|
|
25
|
+
},
|
|
26
|
+
"./algorithms": {
|
|
27
|
+
"types": "./algorithms/index.d.ts",
|
|
28
|
+
"import": "./algorithms/index.js",
|
|
29
|
+
"require": "./algorithms/index.cjs"
|
|
25
30
|
}
|
|
26
31
|
},
|
|
27
32
|
"peerDependencies": {
|
|
@@ -29,6 +34,7 @@
|
|
|
29
34
|
},
|
|
30
35
|
"dependencies": {
|
|
31
36
|
"acorn": "^8.15.0",
|
|
37
|
+
"case": "^1.6.3",
|
|
32
38
|
"chokidar": "^4.0.3",
|
|
33
39
|
"commander": "^13.1.0",
|
|
34
40
|
"globby": "^14.1.0",
|