@lang-tag/cli 0.11.0 → 0.11.2

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
@@ -14,7 +14,7 @@ The core is optimized for performance, with a bundle size of just **~1KB** ([che
14
14
 
15
15
  ### Effortless translation structure
16
16
 
17
- Instead of manually managing centralized translation files, `lang-tag` lets you colocate keys within components and automatically organizes them into namespaces based on your project structure. For example, all components in `components/orders` or pages in `pages/order` share the `orders` namespace. You define a simple folder-to-namespace mapping once, and `lang-tag` handles merging and file organization—while you retain full control over how namespaces are merged.
17
+ Instead of manually managing centralized translation files, `lang-tag` lets you colocate keys within components and automatically organizes them into namespaces based on your project structure. For example, all components in `components/orders` or pages in `pages/order` share the `orders` namespace. You define a simple directory-to-namespace mapping once, and `lang-tag` handles merging and file organization—while you retain full control over how namespaces are merged.
18
18
 
19
19
  > Set your rules, then let `lang-tag` do the rest.
20
20
 
@@ -23,7 +23,7 @@ Instead of manually managing centralized translation files, `lang-tag` lets you
23
23
  Full functionality is available through an advanced CLI that keeps your application bundle size untouched:
24
24
 
25
25
  - **Automatic translation collection** – `lang-tag collect` scans your project for translation tags and aggregates them into organized JSON files (e.g., `public/locales/en/common.json`), based on your configuration
26
- - **Dynamic configuration updates** – `lang-tag regenerate-tags` automatically refreshes translation settings in your code, using rules defined in your configuration (e.g., mapping namespaces based on folder structure)
26
+ - **Dynamic configuration updates** – `lang-tag regenerate-tags` automatically refreshes translation settings in your code, using rules defined in your configuration (e.g., mapping namespaces based on directory structure)
27
27
  - **Third-party translation import** – `lang-tag import` detects and integrates translations from external libraries, adapting them to your project’s translation system
28
28
  - **Watch mode** – `lang-tag watch` monitors your source files for changes and automatically re-collects/re-generates translations when needed
29
29
 
@@ -6,26 +6,26 @@ export interface PathBasedConfigGeneratorOptions {
6
6
  */
7
7
  includeFileName?: boolean;
8
8
  /**
9
- * Whether to completely remove folders wrapped in brackets () or [].
10
- * If false, only the brackets are removed from folder names.
9
+ * Whether to completely remove directories wrapped in brackets () or [].
10
+ * If false, only the brackets are removed from directory names.
11
11
  * @default true
12
12
  *
13
13
  * @example
14
14
  * - true: 'app/(admin)/users' -> 'app/users'
15
15
  * - false: 'app/(admin)/users' -> 'app/admin/users'
16
16
  */
17
- removeBracketedFolders?: boolean;
17
+ removeBracketedDirectories?: boolean;
18
18
  /**
19
- * List of folder names to completely ignore globally.
19
+ * List of directory names to completely ignore globally.
20
20
  * These will be removed from all paths regardless of their position.
21
21
  * @default []
22
22
  *
23
23
  * @example ['src', 'app', 'components']
24
24
  */
25
- ignoreFolders?: string[];
25
+ ignoreDirectories?: string[];
26
26
  /**
27
- * When true, automatically extracts root folder names from the config.includes patterns
28
- * and adds them to the ignoreFolders list.
27
+ * When true, automatically extracts root directory names from the config.includes patterns
28
+ * and adds them to the ignoreDirectories list.
29
29
  *
30
30
  * @default false
31
31
  *
@@ -36,9 +36,9 @@ export interface PathBasedConfigGeneratorOptions {
36
36
  * // With includes: ['(src|app)/**\/*.{js,ts,jsx,tsx}', 'components/**\/*.{jsx,tsx}']
37
37
  * // Automatically ignores: ['src', 'app', 'components']
38
38
  */
39
- ignoreIncludesRootFolders?: boolean;
39
+ ignoreIncludesRootDirectories?: boolean;
40
40
  /**
41
- * Hierarchical structure for ignoring specific folder patterns.
41
+ * Hierarchical structure for ignoring specific directory patterns.
42
42
  * Keys represent path segments to match, values indicate what to ignore at that level.
43
43
  *
44
44
  * @example
@@ -98,9 +98,9 @@ export interface PathBasedConfigGeneratorOptions {
98
98
  * export default {
99
99
  * onConfigGeneration: pathBasedConfigGenerator({
100
100
  * includeFileName: false,
101
- * removeBracketedFolders: true,
102
- * ignoreFolders: ['lib', 'utils'],
103
- * ignoreIncludesRootFolders: true, // Auto-ignores root folders from includes
101
+ * removeBracketedDirectories: true,
102
+ * ignoreDirectories: ['lib', 'utils'],
103
+ * ignoreIncludesRootDirectories: true, // Auto-ignores root directories from includes
104
104
  * lowercaseNamespace: true,
105
105
  * fallbackNamespace: 'common'
106
106
  * })
@@ -22,9 +22,9 @@ const caseLib__namespace = /* @__PURE__ */ _interopNamespaceDefault(caseLib);
22
22
  function pathBasedConfigGenerator(options = {}) {
23
23
  const {
24
24
  includeFileName = false,
25
- removeBracketedFolders = true,
26
- ignoreFolders = [],
27
- ignoreIncludesRootFolders = false,
25
+ removeBracketedDirectories = true,
26
+ ignoreDirectories = [],
27
+ ignoreIncludesRootDirectories = false,
28
28
  ignoreStructured = {},
29
29
  lowercaseNamespace = false,
30
30
  namespaceCase,
@@ -35,10 +35,10 @@ function pathBasedConfigGenerator(options = {}) {
35
35
  return async (event) => {
36
36
  const { relativePath, langTagConfig } = event;
37
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])];
38
+ let finalIgnoreDirectories = [...ignoreDirectories];
39
+ if (ignoreIncludesRootDirectories && langTagConfig.includes) {
40
+ const extractedDirectories = extractRootDirectoriesFromIncludes(langTagConfig.includes);
41
+ finalIgnoreDirectories = [.../* @__PURE__ */ new Set([...finalIgnoreDirectories, ...extractedDirectories])];
42
42
  }
43
43
  let pathSegments = relativePath.split(path.sep).filter(Boolean);
44
44
  if (pathSegments.length === 0) {
@@ -54,12 +54,12 @@ function pathBasedConfigGenerator(options = {}) {
54
54
  pathSegments = pathSegments.map((segment) => {
55
55
  const bracketMatch = segment.match(/^[\(\[](.+)[\)\]]$/);
56
56
  if (bracketMatch) {
57
- return removeBracketedFolders ? null : bracketMatch[1];
57
+ return removeBracketedDirectories ? null : bracketMatch[1];
58
58
  }
59
59
  return segment;
60
60
  }).filter((seg) => seg !== null);
61
61
  pathSegments = applyStructuredIgnore(pathSegments, ignoreStructured);
62
- pathSegments = pathSegments.filter((seg) => !finalIgnoreFolders.includes(seg));
62
+ pathSegments = pathSegments.filter((seg) => !finalIgnoreDirectories.includes(seg));
63
63
  let namespace;
64
64
  let path$1;
65
65
  if (pathSegments.length >= 1) {
@@ -139,8 +139,8 @@ function applyCaseTransform(str, caseType) {
139
139
  }
140
140
  return str;
141
141
  }
142
- function extractRootFoldersFromIncludes(includes) {
143
- const folders = /* @__PURE__ */ new Set();
142
+ function extractRootDirectoriesFromIncludes(includes) {
143
+ const directories = /* @__PURE__ */ new Set();
144
144
  for (const pattern of includes) {
145
145
  let cleanPattern = pattern.replace(/^\.\//, "");
146
146
  const match = cleanPattern.match(/^([^/]+)/);
@@ -148,12 +148,12 @@ function extractRootFoldersFromIncludes(includes) {
148
148
  const firstSegment = match[1];
149
149
  const groupMatch = firstSegment.match(/^[\(\[]([^\)\]]+)[\)\]]$/);
150
150
  if (groupMatch) {
151
- const groupFolders = groupMatch[1].split("|").map((f) => f.trim());
152
- groupFolders.forEach((folder) => folders.add(folder));
151
+ const groupDirectories = groupMatch[1].split("|").map((f) => f.trim());
152
+ groupDirectories.forEach((directory) => directories.add(directory));
153
153
  } else {
154
- folders.add(firstSegment);
154
+ directories.add(firstSegment);
155
155
  }
156
156
  }
157
- return Array.from(folders);
157
+ return Array.from(directories);
158
158
  }
159
159
  exports.pathBasedConfigGenerator = pathBasedConfigGenerator;
@@ -3,9 +3,9 @@ import * as caseLib from "case";
3
3
  function pathBasedConfigGenerator(options = {}) {
4
4
  const {
5
5
  includeFileName = false,
6
- removeBracketedFolders = true,
7
- ignoreFolders = [],
8
- ignoreIncludesRootFolders = false,
6
+ removeBracketedDirectories = true,
7
+ ignoreDirectories = [],
8
+ ignoreIncludesRootDirectories = false,
9
9
  ignoreStructured = {},
10
10
  lowercaseNamespace = false,
11
11
  namespaceCase,
@@ -16,10 +16,10 @@ function pathBasedConfigGenerator(options = {}) {
16
16
  return async (event) => {
17
17
  const { relativePath, langTagConfig } = event;
18
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])];
19
+ let finalIgnoreDirectories = [...ignoreDirectories];
20
+ if (ignoreIncludesRootDirectories && langTagConfig.includes) {
21
+ const extractedDirectories = extractRootDirectoriesFromIncludes(langTagConfig.includes);
22
+ finalIgnoreDirectories = [.../* @__PURE__ */ new Set([...finalIgnoreDirectories, ...extractedDirectories])];
23
23
  }
24
24
  let pathSegments = relativePath.split(sep).filter(Boolean);
25
25
  if (pathSegments.length === 0) {
@@ -35,12 +35,12 @@ function pathBasedConfigGenerator(options = {}) {
35
35
  pathSegments = pathSegments.map((segment) => {
36
36
  const bracketMatch = segment.match(/^[\(\[](.+)[\)\]]$/);
37
37
  if (bracketMatch) {
38
- return removeBracketedFolders ? null : bracketMatch[1];
38
+ return removeBracketedDirectories ? null : bracketMatch[1];
39
39
  }
40
40
  return segment;
41
41
  }).filter((seg) => seg !== null);
42
42
  pathSegments = applyStructuredIgnore(pathSegments, ignoreStructured);
43
- pathSegments = pathSegments.filter((seg) => !finalIgnoreFolders.includes(seg));
43
+ pathSegments = pathSegments.filter((seg) => !finalIgnoreDirectories.includes(seg));
44
44
  let namespace;
45
45
  let path;
46
46
  if (pathSegments.length >= 1) {
@@ -120,8 +120,8 @@ function applyCaseTransform(str, caseType) {
120
120
  }
121
121
  return str;
122
122
  }
123
- function extractRootFoldersFromIncludes(includes) {
124
- const folders = /* @__PURE__ */ new Set();
123
+ function extractRootDirectoriesFromIncludes(includes) {
124
+ const directories = /* @__PURE__ */ new Set();
125
125
  for (const pattern of includes) {
126
126
  let cleanPattern = pattern.replace(/^\.\//, "");
127
127
  const match = cleanPattern.match(/^([^/]+)/);
@@ -129,13 +129,13 @@ function extractRootFoldersFromIncludes(includes) {
129
129
  const firstSegment = match[1];
130
130
  const groupMatch = firstSegment.match(/^[\(\[]([^\)\]]+)[\)\]]$/);
131
131
  if (groupMatch) {
132
- const groupFolders = groupMatch[1].split("|").map((f) => f.trim());
133
- groupFolders.forEach((folder) => folders.add(folder));
132
+ const groupDirectories = groupMatch[1].split("|").map((f) => f.trim());
133
+ groupDirectories.forEach((directory) => directories.add(directory));
134
134
  } else {
135
- folders.add(firstSegment);
135
+ directories.add(firstSegment);
136
136
  }
137
137
  }
138
- return Array.from(folders);
138
+ return Array.from(directories);
139
139
  }
140
140
  export {
141
141
  pathBasedConfigGenerator
package/config.d.ts CHANGED
@@ -99,9 +99,28 @@ export interface LangTagCLIConfig {
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
+ * **IMPORTANT:** The `event.config` object is deeply frozen and immutable. Any attempt
103
+ * to directly modify it will throw an error. To update the configuration, you must
104
+ * use `event.save(newConfig)` with a new configuration object.
105
+ *
102
106
  * Changes made inside this function are **applied only if you explicitly call**
103
107
  * `event.save(configuration)`. Returning a value or modifying the event object
104
108
  * without calling `save()` will **not** update the configuration.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * onConfigGeneration: async (event) => {
113
+ * // ❌ This will throw an error:
114
+ * // event.config.namespace = "new-namespace";
115
+ *
116
+ * // ✅ Correct way to update:
117
+ * event.save({
118
+ * ...event.config,
119
+ * namespace: "new-namespace",
120
+ * path: "new.path"
121
+ * });
122
+ * }
123
+ * ```
105
124
  */
106
125
  onConfigGeneration: (event: LangTagCLIConfigGenerationEvent) => Promise<void>;
107
126
  debug?: boolean;
@@ -163,14 +182,19 @@ export interface LangTagCLIConflict {
163
182
  }
164
183
  export interface LangTagCLIConfigGenerationEvent {
165
184
  /** The absolute path to the source file being processed. */
166
- absolutePath: string;
185
+ readonly absolutePath: string;
167
186
  /** The path of the source file relative to the project root (where the command was invoked). */
168
- relativePath: string;
187
+ readonly relativePath: string;
169
188
  /** 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;
189
+ readonly isImportedLibrary: boolean;
190
+ /**
191
+ * The configuration object extracted from the lang tag's options argument (e.g., `{ namespace: 'common', path: 'my.path' }`).
192
+ *
193
+ * **This object is deeply frozen and immutable.** Any attempt to modify it will throw an error in strict mode.
194
+ * To update the configuration, use the `save()` method with a new configuration object.
195
+ */
196
+ readonly config: Readonly<LangTagTranslationsConfig> | undefined;
197
+ readonly langTagConfig: LangTagCLIConfig;
174
198
  /**
175
199
  * Tells CLI to replace tag configuration
176
200
  * undefined = means configuration will be removed
package/index.cjs CHANGED
@@ -168,7 +168,7 @@ class $LT_TagProcessor {
168
168
  replaceTags(fileContent, replacements) {
169
169
  const replaceMap = /* @__PURE__ */ new Map();
170
170
  replacements.forEach((R) => {
171
- if (!R.translations && !R.config) {
171
+ if (!R.translations && !R.config && R.config !== null) {
172
172
  throw new Error("Replacement data is required!");
173
173
  }
174
174
  const tag = R.tag;
@@ -182,7 +182,7 @@ class $LT_TagProcessor {
182
182
  throw new Error(`Tag translations are invalid object! Translations: ${newTranslationsString}`);
183
183
  }
184
184
  let newConfigString = R.config;
185
- if (!newConfigString) newConfigString = tag.parameterConfig;
185
+ if (!newConfigString && newConfigString !== null) newConfigString = tag.parameterConfig;
186
186
  if (newConfigString) {
187
187
  try {
188
188
  if (typeof newConfigString === "string") JSON5.parse(newConfigString);
@@ -426,6 +426,16 @@ function $LT_FilterEmptyNamespaceTags(tags, logger) {
426
426
  return true;
427
427
  });
428
428
  }
429
+ function deepFreezeObject(obj) {
430
+ const propNames = Object.getOwnPropertyNames(obj);
431
+ for (const name of propNames) {
432
+ const value = obj[name];
433
+ if (value && typeof value === "object") {
434
+ deepFreezeObject(value);
435
+ }
436
+ }
437
+ return Object.freeze(obj);
438
+ }
429
439
  async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
430
440
  let libraryImportsDir = config.import.dir;
431
441
  if (!libraryImportsDir.endsWith(path.sep)) libraryImportsDir += path.sep;
@@ -440,14 +450,15 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
440
450
  for (let tag of tags) {
441
451
  let newConfig = void 0;
442
452
  let shouldUpdate = false;
453
+ const frozenConfig = tag.parameterConfig ? deepFreezeObject(tag.parameterConfig) : tag.parameterConfig;
443
454
  await config.onConfigGeneration({
444
455
  langTagConfig: config,
445
- config: tag.parameterConfig,
456
+ config: frozenConfig,
446
457
  absolutePath: file,
447
458
  relativePath: path$12,
448
459
  isImportedLibrary: path$12.startsWith(libraryImportsDir),
449
460
  save: (updatedConfig) => {
450
- newConfig = updatedConfig;
461
+ newConfig = updatedConfig || null;
451
462
  shouldUpdate = true;
452
463
  }
453
464
  });
@@ -1422,12 +1433,12 @@ async function generateDefaultConfig() {
1422
1433
  return `${importStatement}
1423
1434
 
1424
1435
  const generationAlgorithm = pathBasedConfigGenerator({
1425
- ignoreIncludesRootFolders: true,
1426
- removeBracketedFolders: true,
1436
+ ignoreIncludesRootDirectories: true,
1437
+ removeBracketedDirectories: true,
1427
1438
  namespaceCase: 'kebab',
1428
1439
  pathCase: 'camel',
1429
1440
  clearOnDefaultNamespace: true,
1430
- ignoreFolders: ['core', 'utils', 'helpers']
1441
+ ignoreDirectories: ['core', 'utils', 'helpers']
1431
1442
  });
1432
1443
 
1433
1444
  /** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
package/index.js CHANGED
@@ -148,7 +148,7 @@ class $LT_TagProcessor {
148
148
  replaceTags(fileContent, replacements) {
149
149
  const replaceMap = /* @__PURE__ */ new Map();
150
150
  replacements.forEach((R) => {
151
- if (!R.translations && !R.config) {
151
+ if (!R.translations && !R.config && R.config !== null) {
152
152
  throw new Error("Replacement data is required!");
153
153
  }
154
154
  const tag = R.tag;
@@ -162,7 +162,7 @@ class $LT_TagProcessor {
162
162
  throw new Error(`Tag translations are invalid object! Translations: ${newTranslationsString}`);
163
163
  }
164
164
  let newConfigString = R.config;
165
- if (!newConfigString) newConfigString = tag.parameterConfig;
165
+ if (!newConfigString && newConfigString !== null) newConfigString = tag.parameterConfig;
166
166
  if (newConfigString) {
167
167
  try {
168
168
  if (typeof newConfigString === "string") JSON5.parse(newConfigString);
@@ -406,6 +406,16 @@ function $LT_FilterEmptyNamespaceTags(tags, logger) {
406
406
  return true;
407
407
  });
408
408
  }
409
+ function deepFreezeObject(obj) {
410
+ const propNames = Object.getOwnPropertyNames(obj);
411
+ for (const name of propNames) {
412
+ const value = obj[name];
413
+ if (value && typeof value === "object") {
414
+ deepFreezeObject(value);
415
+ }
416
+ }
417
+ return Object.freeze(obj);
418
+ }
409
419
  async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
410
420
  let libraryImportsDir = config.import.dir;
411
421
  if (!libraryImportsDir.endsWith(sep)) libraryImportsDir += sep;
@@ -420,14 +430,15 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
420
430
  for (let tag of tags) {
421
431
  let newConfig = void 0;
422
432
  let shouldUpdate = false;
433
+ const frozenConfig = tag.parameterConfig ? deepFreezeObject(tag.parameterConfig) : tag.parameterConfig;
423
434
  await config.onConfigGeneration({
424
435
  langTagConfig: config,
425
- config: tag.parameterConfig,
436
+ config: frozenConfig,
426
437
  absolutePath: file,
427
438
  relativePath: path2,
428
439
  isImportedLibrary: path2.startsWith(libraryImportsDir),
429
440
  save: (updatedConfig) => {
430
- newConfig = updatedConfig;
441
+ newConfig = updatedConfig || null;
431
442
  shouldUpdate = true;
432
443
  }
433
444
  });
@@ -1402,12 +1413,12 @@ async function generateDefaultConfig() {
1402
1413
  return `${importStatement}
1403
1414
 
1404
1415
  const generationAlgorithm = pathBasedConfigGenerator({
1405
- ignoreIncludesRootFolders: true,
1406
- removeBracketedFolders: true,
1416
+ ignoreIncludesRootDirectories: true,
1417
+ removeBracketedDirectories: true,
1407
1418
  namespaceCase: 'kebab',
1408
1419
  pathCase: 'camel',
1409
1420
  clearOnDefaultNamespace: true,
1410
- ignoreFolders: ['core', 'utils', 'helpers']
1421
+ ignoreDirectories: ['core', 'utils', 'helpers']
1411
1422
  });
1412
1423
 
1413
1424
  /** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lang-tag/cli",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"