@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 CHANGED
@@ -1,4 +1,4 @@
1
- # Lang-tag: Component-Colocated Translation Management / Translation Engine Proxy
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,7 @@
1
+ /**
2
+ * Predefined algorithms for onImport hook.
3
+ *
4
+ * These algorithms customize how library translations are imported
5
+ * and organized in your project.
6
+ */
7
+ export {};
@@ -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
- * If it returns `undefined`, the tag's configuration is not automatically generated or updated.
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: (params: LangTagCLIOnConfigGenerationParams) => LangTagTranslationsConfig | undefined;
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
- validity = "invalid-param-2";
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
- validity = "invalid-param-1";
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
- const newConfig = config.onConfigGeneration({
441
+ let newConfig = void 0;
442
+ let shouldUpdate = false;
443
+ await config.onConfigGeneration({
444
+ langTagConfig: config,
251
445
  config: tag.parameterConfig,
252
- fullPath: file,
253
- path: path$12,
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 (newConfig === void 0) {
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: (params) => void 0
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 exportStatement = getExportStatement(moduleSystem);
1220
- return `/** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
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: (params) => {
1440
+ onConfigGeneration: async event => {
1228
1441
  // We do not modify imported configurations
1229
- if (params.isImportedLibrary) return undefined;
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 undefined
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
- validity = "invalid-param-2";
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
- validity = "invalid-param-1";
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
- const newConfig = config.onConfigGeneration({
421
+ let newConfig = void 0;
422
+ let shouldUpdate = false;
423
+ await config.onConfigGeneration({
424
+ langTagConfig: config,
231
425
  config: tag.parameterConfig,
232
- fullPath: file,
233
- path: path2,
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 (newConfig === void 0) {
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: (params) => void 0
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 exportStatement = getExportStatement(moduleSystem);
1200
- return `/** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
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: (params) => {
1420
+ onConfigGeneration: async event => {
1208
1421
  // We do not modify imported configurations
1209
- if (params.isImportedLibrary) return undefined;
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 undefined
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.10.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",