@lang-tag/cli 0.12.3 → 0.13.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,5 +1,6 @@
1
1
  # Lang-tag CLI
2
2
 
3
+
3
4
  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
5
 
5
6
  ## Key Benefits
@@ -47,16 +47,43 @@ export interface PathBasedConfigGeneratorOptions {
47
47
  /**
48
48
  * Hierarchical structure for ignoring specific directory patterns.
49
49
  * Keys represent path segments to match, values indicate what to ignore at that level.
50
+ * Supports special key `_` when set to `true` to ignore current segment but continue hierarchy.
50
51
  *
51
52
  * @example
52
53
  * {
53
54
  * 'src': {
54
55
  * 'app': true, // ignore 'app' when under 'src'
55
- * 'features': ['auth', 'admin'] // ignore 'auth' and 'admin' under 'src/features'
56
+ * 'features': ['auth', 'admin'], // ignore 'auth' and 'admin' under 'src/features'
57
+ * 'dashboard': {
58
+ * _: true, // ignore 'dashboard' but continue with nested rules
59
+ * modules: true // also ignore 'modules' under 'dashboard'
60
+ * }
56
61
  * }
57
62
  * }
58
63
  */
59
64
  ignoreStructured?: Record<string, any>;
65
+ /**
66
+ * Advanced hierarchical rules for transforming path segments.
67
+ * Supports ignoring and renaming segments with special keys:
68
+ * - `_`: when `false`, ignores the current segment but continues hierarchy
69
+ * - `>`: renames the current segment to the specified value
70
+ * - Regular keys: nested rules or boolean/string for child segments
71
+ *
72
+ * @example
73
+ * {
74
+ * app: {
75
+ * dashboard: {
76
+ * _: false, // ignore "dashboard" segment
77
+ * modules: false // ignore "modules" when under "dashboard"
78
+ * },
79
+ * admin: {
80
+ * '>': 'management', // rename "admin" to "management"
81
+ * users: true // keep "users" as is (does nothing)
82
+ * }
83
+ * }
84
+ * }
85
+ */
86
+ pathRules?: Record<string, any>;
60
87
  /**
61
88
  * Convert the final namespace to lowercase.
62
89
  * @default false
@@ -27,12 +27,20 @@ function pathBasedConfigGenerator(options = {}) {
27
27
  ignoreDirectories = [],
28
28
  ignoreIncludesRootDirectories = false,
29
29
  ignoreStructured = {},
30
+ pathRules = {},
30
31
  lowercaseNamespace = false,
31
32
  namespaceCase,
32
33
  pathCase,
33
34
  fallbackNamespace,
34
35
  clearOnDefaultNamespace = true
35
36
  } = options;
37
+ const hasPathRules = Object.keys(pathRules).length > 0;
38
+ const hasIgnoreStructured = Object.keys(ignoreStructured).length > 0;
39
+ if (hasPathRules && hasIgnoreStructured) {
40
+ throw new Error(
41
+ 'pathBasedConfigGenerator: Cannot use both "pathRules" and "ignoreStructured" options simultaneously. Please use "pathRules" (recommended) or "ignoreStructured" (legacy), but not both.'
42
+ );
43
+ }
36
44
  return async (event) => {
37
45
  const { relativePath, langTagConfig } = event;
38
46
  const actualFallbackNamespace = fallbackNamespace ?? langTagConfig.collect?.defaultNamespace;
@@ -54,7 +62,11 @@ function pathBasedConfigGenerator(options = {}) {
54
62
  }
55
63
  return segment;
56
64
  }).filter((seg) => seg !== null);
57
- pathSegments = applyStructuredIgnore(pathSegments, ignoreStructured);
65
+ if (hasPathRules) {
66
+ pathSegments = applyPathRules(pathSegments, pathRules);
67
+ } else {
68
+ pathSegments = applyStructuredIgnore(pathSegments, ignoreStructured);
69
+ }
58
70
  if (ignoreIncludesRootDirectories && langTagConfig.includes && pathSegments.length > 0) {
59
71
  const extractedDirectories = extractRootDirectoriesFromIncludes(langTagConfig.includes);
60
72
  if (extractedDirectories.includes(pathSegments[0])) {
@@ -135,7 +147,56 @@ function applyStructuredIgnore(segments, structure) {
135
147
  currentStructure = structure;
136
148
  continue;
137
149
  } else if (typeof rule === "object" && rule !== null) {
150
+ const ignoreSelf = rule["_"] === true;
151
+ if (ignoreSelf) {
152
+ currentStructure = rule;
153
+ continue;
154
+ } else {
155
+ result.push(segment);
156
+ currentStructure = rule;
157
+ continue;
158
+ }
159
+ }
160
+ }
161
+ result.push(segment);
162
+ currentStructure = structure;
163
+ }
164
+ return result;
165
+ }
166
+ function applyPathRules(segments, structure) {
167
+ const result = [];
168
+ let currentStructure = structure;
169
+ for (let i = 0; i < segments.length; i++) {
170
+ const segment = segments[i];
171
+ if (segment in currentStructure) {
172
+ const rule = currentStructure[segment];
173
+ if (rule === true) {
174
+ currentStructure = structure;
175
+ continue;
176
+ } else if (rule === false) {
177
+ currentStructure = structure;
178
+ continue;
179
+ } else if (typeof rule === "string") {
180
+ result.push(rule);
181
+ currentStructure = structure;
182
+ continue;
183
+ } else if (Array.isArray(rule)) {
138
184
  result.push(segment);
185
+ if (i + 1 < segments.length && rule.includes(segments[i + 1])) {
186
+ i++;
187
+ }
188
+ currentStructure = structure;
189
+ continue;
190
+ } else if (typeof rule === "object" && rule !== null) {
191
+ const ignoreSelf = rule["_"] === false;
192
+ const renameTo = rule[">"];
193
+ if (!ignoreSelf) {
194
+ if (typeof renameTo === "string") {
195
+ result.push(renameTo);
196
+ } else {
197
+ result.push(segment);
198
+ }
199
+ }
139
200
  currentStructure = rule;
140
201
  continue;
141
202
  }
@@ -8,12 +8,20 @@ function pathBasedConfigGenerator(options = {}) {
8
8
  ignoreDirectories = [],
9
9
  ignoreIncludesRootDirectories = false,
10
10
  ignoreStructured = {},
11
+ pathRules = {},
11
12
  lowercaseNamespace = false,
12
13
  namespaceCase,
13
14
  pathCase,
14
15
  fallbackNamespace,
15
16
  clearOnDefaultNamespace = true
16
17
  } = options;
18
+ const hasPathRules = Object.keys(pathRules).length > 0;
19
+ const hasIgnoreStructured = Object.keys(ignoreStructured).length > 0;
20
+ if (hasPathRules && hasIgnoreStructured) {
21
+ throw new Error(
22
+ 'pathBasedConfigGenerator: Cannot use both "pathRules" and "ignoreStructured" options simultaneously. Please use "pathRules" (recommended) or "ignoreStructured" (legacy), but not both.'
23
+ );
24
+ }
17
25
  return async (event) => {
18
26
  const { relativePath, langTagConfig } = event;
19
27
  const actualFallbackNamespace = fallbackNamespace ?? langTagConfig.collect?.defaultNamespace;
@@ -35,7 +43,11 @@ function pathBasedConfigGenerator(options = {}) {
35
43
  }
36
44
  return segment;
37
45
  }).filter((seg) => seg !== null);
38
- pathSegments = applyStructuredIgnore(pathSegments, ignoreStructured);
46
+ if (hasPathRules) {
47
+ pathSegments = applyPathRules(pathSegments, pathRules);
48
+ } else {
49
+ pathSegments = applyStructuredIgnore(pathSegments, ignoreStructured);
50
+ }
39
51
  if (ignoreIncludesRootDirectories && langTagConfig.includes && pathSegments.length > 0) {
40
52
  const extractedDirectories = extractRootDirectoriesFromIncludes(langTagConfig.includes);
41
53
  if (extractedDirectories.includes(pathSegments[0])) {
@@ -116,7 +128,56 @@ function applyStructuredIgnore(segments, structure) {
116
128
  currentStructure = structure;
117
129
  continue;
118
130
  } else if (typeof rule === "object" && rule !== null) {
131
+ const ignoreSelf = rule["_"] === true;
132
+ if (ignoreSelf) {
133
+ currentStructure = rule;
134
+ continue;
135
+ } else {
136
+ result.push(segment);
137
+ currentStructure = rule;
138
+ continue;
139
+ }
140
+ }
141
+ }
142
+ result.push(segment);
143
+ currentStructure = structure;
144
+ }
145
+ return result;
146
+ }
147
+ function applyPathRules(segments, structure) {
148
+ const result = [];
149
+ let currentStructure = structure;
150
+ for (let i = 0; i < segments.length; i++) {
151
+ const segment = segments[i];
152
+ if (segment in currentStructure) {
153
+ const rule = currentStructure[segment];
154
+ if (rule === true) {
155
+ currentStructure = structure;
156
+ continue;
157
+ } else if (rule === false) {
158
+ currentStructure = structure;
159
+ continue;
160
+ } else if (typeof rule === "string") {
161
+ result.push(rule);
162
+ currentStructure = structure;
163
+ continue;
164
+ } else if (Array.isArray(rule)) {
119
165
  result.push(segment);
166
+ if (i + 1 < segments.length && rule.includes(segments[i + 1])) {
167
+ i++;
168
+ }
169
+ currentStructure = structure;
170
+ continue;
171
+ } else if (typeof rule === "object" && rule !== null) {
172
+ const ignoreSelf = rule["_"] === false;
173
+ const renameTo = rule[">"];
174
+ if (!ignoreSelf) {
175
+ if (typeof renameTo === "string") {
176
+ result.push(renameTo);
177
+ } else {
178
+ result.push(segment);
179
+ }
180
+ }
120
181
  currentStructure = rule;
121
182
  continue;
122
183
  }
package/index.cjs CHANGED
@@ -484,7 +484,8 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
484
484
  if (replacements.length) {
485
485
  const newContent = processor.replaceTags(fileContent, replacements);
486
486
  await promises.writeFile(file, newContent, "utf-8");
487
- logger.info('Lang tag configurations written for file "{path}" (file://{file}:{line})', { path: path$12, file, line: lastUpdatedLine });
487
+ const encodedFile = encodeURI(file);
488
+ logger.info('Lang tag configurations written for file "{path}" (file://{file}:{line})', { path: path$12, file: encodedFile, line: lastUpdatedLine });
488
489
  return true;
489
490
  }
490
491
  return false;
@@ -904,7 +905,8 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
904
905
  console.error("Failed to colorize config:", error);
905
906
  }
906
907
  }
907
- console.log(`${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${filePath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`);
908
+ const encodedPath = encodeURI(filePath);
909
+ console.log(`${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${encodedPath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`);
908
910
  printLines(colorizedWhole.split("\n"), startLine, errorLines, condense);
909
911
  } catch (error) {
910
912
  console.error("Error displaying conflict:", error);
@@ -1452,7 +1454,20 @@ const generationAlgorithm = pathBasedConfigGenerator({
1452
1454
  namespaceCase: 'kebab',
1453
1455
  pathCase: 'camel',
1454
1456
  clearOnDefaultNamespace: true,
1455
- ignoreDirectories: ['core', 'utils', 'helpers']
1457
+ ignoreDirectories: ['core', 'utils', 'helpers'],
1458
+ // Advanced: Use pathRules for hierarchical transformations with ignore and rename
1459
+ // pathRules: {
1460
+ // app: {
1461
+ // dashboard: {
1462
+ // _: false, // ignore "dashboard" but continue with nested rules
1463
+ // modules: false // also ignore "modules"
1464
+ // },
1465
+ // admin: {
1466
+ // '>': 'management', // rename "admin" to "management"
1467
+ // users: false // ignore "users"
1468
+ // }
1469
+ // }
1470
+ // }
1456
1471
  });
1457
1472
  const keeper = configKeeper();
1458
1473
 
package/index.js CHANGED
@@ -464,7 +464,8 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
464
464
  if (replacements.length) {
465
465
  const newContent = processor.replaceTags(fileContent, replacements);
466
466
  await writeFile(file, newContent, "utf-8");
467
- logger.info('Lang tag configurations written for file "{path}" (file://{file}:{line})', { path: path2, file, line: lastUpdatedLine });
467
+ const encodedFile = encodeURI(file);
468
+ logger.info('Lang tag configurations written for file "{path}" (file://{file}:{line})', { path: path2, file: encodedFile, line: lastUpdatedLine });
468
469
  return true;
469
470
  }
470
471
  return false;
@@ -884,7 +885,8 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
884
885
  console.error("Failed to colorize config:", error);
885
886
  }
886
887
  }
887
- console.log(`${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${filePath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`);
888
+ const encodedPath = encodeURI(filePath);
889
+ console.log(`${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${encodedPath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`);
888
890
  printLines(colorizedWhole.split("\n"), startLine, errorLines, condense);
889
891
  } catch (error) {
890
892
  console.error("Error displaying conflict:", error);
@@ -1432,7 +1434,20 @@ const generationAlgorithm = pathBasedConfigGenerator({
1432
1434
  namespaceCase: 'kebab',
1433
1435
  pathCase: 'camel',
1434
1436
  clearOnDefaultNamespace: true,
1435
- ignoreDirectories: ['core', 'utils', 'helpers']
1437
+ ignoreDirectories: ['core', 'utils', 'helpers'],
1438
+ // Advanced: Use pathRules for hierarchical transformations with ignore and rename
1439
+ // pathRules: {
1440
+ // app: {
1441
+ // dashboard: {
1442
+ // _: false, // ignore "dashboard" but continue with nested rules
1443
+ // modules: false // also ignore "modules"
1444
+ // },
1445
+ // admin: {
1446
+ // '>': 'management', // rename "admin" to "management"
1447
+ // users: false // ignore "users"
1448
+ // }
1449
+ // }
1450
+ // }
1436
1451
  });
1437
1452
  const keeper = configKeeper();
1438
1453
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lang-tag/cli",
3
- "version": "0.12.3",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"