@lang-tag/cli 0.11.1 → 0.12.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.
@@ -0,0 +1,47 @@
1
+ import { LangTagCLIConfigGenerationEvent } from '../../config.ts';
2
+ export type ConfigKeeperMode = 'namespace' | 'path' | 'both';
3
+ export interface ConfigKeeperOptions {
4
+ /**
5
+ * The name of the property in the tag configuration that indicates what should be kept.
6
+ * @default 'keep'
7
+ * @example
8
+ * ```tsx
9
+ * lang({ click: "Click" }, { namespace: 'common', path: 'button', keep: 'namespace' })
10
+ * ```
11
+ */
12
+ propertyName?: string;
13
+ }
14
+ /**
15
+ * Creates a config keeper algorithm that preserves original configuration values
16
+ * when they are marked to be kept using a special property (default: 'keep').
17
+ *
18
+ * This algorithm should be applied AFTER other generation algorithms to prevent
19
+ * them from overwriting values that should be preserved.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const pathAlgorithm = pathBasedConfigGenerator({ ... });
24
+ * const keeper = configKeeper();
25
+ *
26
+ * onConfigGeneration: async (event) => {
27
+ * // First, apply path-based generation
28
+ * await pathAlgorithm(event);
29
+ *
30
+ * // Then, restore any values marked to be kept
31
+ * await keeper(event);
32
+ * }
33
+ * ```
34
+ *
35
+ * @example Usage in tag:
36
+ * ```tsx
37
+ * // This will keep the namespace even if path-based algorithm tries to change it
38
+ * lang({ click: "Click" }, { namespace: 'common', path: 'button', keep: 'namespace' })
39
+ *
40
+ * // This will keep the path even if path-based algorithm tries to change it
41
+ * lang({ click: "Click" }, { namespace: 'common', path: 'old.path', keep: 'path' })
42
+ *
43
+ * // This will keep both namespace and path
44
+ * lang({ click: "Click" }, { namespace: 'common', path: 'button', keep: 'both' })
45
+ * ```
46
+ */
47
+ export declare function configKeeper(options?: ConfigKeeperOptions): (event: LangTagCLIConfigGenerationEvent) => Promise<void>;
@@ -5,3 +5,4 @@
5
5
  * during collection and regeneration.
6
6
  */
7
7
  export { pathBasedConfigGenerator, type PathBasedConfigGeneratorOptions } from './path-based-config-generator.ts';
8
+ export { configKeeper, type ConfigKeeperOptions, type ConfigKeeperMode } from './config-keeper.ts';
@@ -19,6 +19,7 @@ function _interopNamespaceDefault(e) {
19
19
  return Object.freeze(n);
20
20
  }
21
21
  const caseLib__namespace = /* @__PURE__ */ _interopNamespaceDefault(caseLib);
22
+ const TRIGGER_NAME$1 = "path-based-config-generator";
22
23
  function pathBasedConfigGenerator(options = {}) {
23
24
  const {
24
25
  includeFileName = false,
@@ -88,7 +89,7 @@ function pathBasedConfigGenerator(options = {}) {
88
89
  if (path$1) {
89
90
  newConfig.path = path$1;
90
91
  } else {
91
- event.save(void 0);
92
+ event.save(null, TRIGGER_NAME$1);
92
93
  return;
93
94
  }
94
95
  } else {
@@ -100,7 +101,7 @@ function pathBasedConfigGenerator(options = {}) {
100
101
  }
101
102
  }
102
103
  if (Object.keys(newConfig).length > 0) {
103
- event.save(newConfig);
104
+ event.save(newConfig, TRIGGER_NAME$1);
104
105
  }
105
106
  };
106
107
  }
@@ -156,4 +157,40 @@ function extractRootDirectoriesFromIncludes(includes) {
156
157
  }
157
158
  return Array.from(directories);
158
159
  }
160
+ const TRIGGER_NAME = "config-keeper";
161
+ function configKeeper(options = {}) {
162
+ const propertyName = options.propertyName ?? "keep";
163
+ return async (event) => {
164
+ if (!event.isSaved) {
165
+ return;
166
+ }
167
+ if (!event.config) {
168
+ return;
169
+ }
170
+ const keepMode = event.config[propertyName];
171
+ if (!keepMode) {
172
+ return;
173
+ }
174
+ if (keepMode !== "namespace" && keepMode !== "path" && keepMode !== "both") {
175
+ return;
176
+ }
177
+ let restoredConfig;
178
+ if (event.savedConfig === null) {
179
+ restoredConfig = { ...event.config };
180
+ delete restoredConfig.namespace;
181
+ delete restoredConfig.path;
182
+ } else {
183
+ restoredConfig = { ...event.savedConfig };
184
+ }
185
+ if ((keepMode === "namespace" || keepMode === "both") && event.config.namespace !== void 0) {
186
+ restoredConfig.namespace = event.config.namespace;
187
+ }
188
+ if ((keepMode === "path" || keepMode === "both") && event.config.path !== void 0) {
189
+ restoredConfig.path = event.config.path;
190
+ }
191
+ restoredConfig[propertyName] = keepMode;
192
+ event.save(restoredConfig, TRIGGER_NAME);
193
+ };
194
+ }
195
+ exports.configKeeper = configKeeper;
159
196
  exports.pathBasedConfigGenerator = pathBasedConfigGenerator;
@@ -4,4 +4,4 @@
4
4
  * These algorithms can be used in your lang-tag-cli config file
5
5
  * to customize how tags are processed during collection and regeneration.
6
6
  */
7
- export { pathBasedConfigGenerator, type PathBasedConfigGeneratorOptions } from './config-generation/index.ts';
7
+ export * from './config-generation/index.ts';
@@ -1,5 +1,6 @@
1
1
  import { sep } from "pathe";
2
2
  import * as caseLib from "case";
3
+ const TRIGGER_NAME$1 = "path-based-config-generator";
3
4
  function pathBasedConfigGenerator(options = {}) {
4
5
  const {
5
6
  includeFileName = false,
@@ -69,7 +70,7 @@ function pathBasedConfigGenerator(options = {}) {
69
70
  if (path) {
70
71
  newConfig.path = path;
71
72
  } else {
72
- event.save(void 0);
73
+ event.save(null, TRIGGER_NAME$1);
73
74
  return;
74
75
  }
75
76
  } else {
@@ -81,7 +82,7 @@ function pathBasedConfigGenerator(options = {}) {
81
82
  }
82
83
  }
83
84
  if (Object.keys(newConfig).length > 0) {
84
- event.save(newConfig);
85
+ event.save(newConfig, TRIGGER_NAME$1);
85
86
  }
86
87
  };
87
88
  }
@@ -137,6 +138,42 @@ function extractRootDirectoriesFromIncludes(includes) {
137
138
  }
138
139
  return Array.from(directories);
139
140
  }
141
+ const TRIGGER_NAME = "config-keeper";
142
+ function configKeeper(options = {}) {
143
+ const propertyName = options.propertyName ?? "keep";
144
+ return async (event) => {
145
+ if (!event.isSaved) {
146
+ return;
147
+ }
148
+ if (!event.config) {
149
+ return;
150
+ }
151
+ const keepMode = event.config[propertyName];
152
+ if (!keepMode) {
153
+ return;
154
+ }
155
+ if (keepMode !== "namespace" && keepMode !== "path" && keepMode !== "both") {
156
+ return;
157
+ }
158
+ let restoredConfig;
159
+ if (event.savedConfig === null) {
160
+ restoredConfig = { ...event.config };
161
+ delete restoredConfig.namespace;
162
+ delete restoredConfig.path;
163
+ } else {
164
+ restoredConfig = { ...event.savedConfig };
165
+ }
166
+ if ((keepMode === "namespace" || keepMode === "both") && event.config.namespace !== void 0) {
167
+ restoredConfig.namespace = event.config.namespace;
168
+ }
169
+ if ((keepMode === "path" || keepMode === "both") && event.config.path !== void 0) {
170
+ restoredConfig.path = event.config.path;
171
+ }
172
+ restoredConfig[propertyName] = keepMode;
173
+ event.save(restoredConfig, TRIGGER_NAME);
174
+ };
175
+ }
140
176
  export {
177
+ configKeeper,
141
178
  pathBasedConfigGenerator
142
179
  };
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,19 +182,35 @@ 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;
198
+ /**
199
+ * Indicates whether the `save()` method has been called during this event.
200
+ */
201
+ readonly isSaved: boolean;
202
+ /**
203
+ * The updated configuration that was passed to the `save()` method.
204
+ * - `undefined` if `save()` has not been called yet
205
+ * - `null` if `save(null)` was called to remove the configuration
206
+ * - `LangTagTranslationsConfig` object if a new configuration was saved
207
+ */
208
+ readonly savedConfig: LangTagTranslationsConfig | null | undefined;
174
209
  /**
175
210
  * Tells CLI to replace tag configuration
176
- * undefined = means configuration will be removed
211
+ * null = means configuration will be removed
177
212
  **/
178
- save(config: LangTagTranslationsConfig | undefined): void;
213
+ save(config: LangTagTranslationsConfig | null, triggerName?: string): void;
179
214
  }
180
215
  export interface LangTagCLICollectConfigFixEvent {
181
216
  config: LangTagTranslationsConfig;
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;
@@ -437,34 +447,50 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
437
447
  return false;
438
448
  }
439
449
  const replacements = [];
450
+ let lastUpdatedLine = 0;
440
451
  for (let tag of tags) {
441
452
  let newConfig = void 0;
442
453
  let shouldUpdate = false;
443
- await config.onConfigGeneration({
454
+ const frozenConfig = tag.parameterConfig ? deepFreezeObject(tag.parameterConfig) : tag.parameterConfig;
455
+ const event = {
444
456
  langTagConfig: config,
445
- config: tag.parameterConfig,
457
+ config: frozenConfig,
446
458
  absolutePath: file,
447
459
  relativePath: path$12,
448
460
  isImportedLibrary: path$12.startsWith(libraryImportsDir),
449
- save: (updatedConfig) => {
461
+ isSaved: false,
462
+ savedConfig: void 0,
463
+ save: (updatedConfig, triggerName) => {
464
+ if (!updatedConfig && updatedConfig !== null) throw new Error("Wrong config data");
450
465
  newConfig = updatedConfig;
451
466
  shouldUpdate = true;
467
+ event.isSaved = true;
468
+ event.savedConfig = updatedConfig;
469
+ logger.debug('Called save for "{path}" with config "{config}" triggered by: ("{trigger}")', { path: path$12, config: JSON.stringify(updatedConfig), trigger: triggerName || "-" });
452
470
  }
453
- });
471
+ };
472
+ await config.onConfigGeneration(event);
454
473
  if (!shouldUpdate) {
455
474
  continue;
456
475
  }
457
- if (JSON5.stringify(tag.parameterConfig) !== JSON5.stringify(newConfig)) {
476
+ lastUpdatedLine = tag.line;
477
+ if (!isConfigSame(tag.parameterConfig, newConfig)) {
458
478
  replacements.push({ tag, config: newConfig });
459
479
  }
460
480
  }
461
481
  if (replacements.length) {
462
482
  const newContent = processor.replaceTags(fileContent, replacements);
463
483
  await promises.writeFile(file, newContent, "utf-8");
484
+ logger.info('Lang tag configurations written for file "{path}" (file://{file}:{line})', { path: path$12, file, line: lastUpdatedLine });
464
485
  return true;
465
486
  }
466
487
  return false;
467
488
  }
489
+ function isConfigSame(c1, c2) {
490
+ if (!c1 && !c2) return true;
491
+ if (c1 && typeof c1 === "object" && c2 && typeof c2 === "object" && JSON5.stringify(c1) === JSON5.stringify(c2)) return true;
492
+ return false;
493
+ }
468
494
  const CONFIG_FILE_NAME = ".lang-tag.config.js";
469
495
  const EXPORTS_FILE_NAME = ".lang-tag.exports.json";
470
496
  const LANG_TAG_DEFAULT_CONFIG = {
@@ -983,7 +1009,6 @@ async function $LT_CMD_RegenerateTags() {
983
1009
  const path2 = file.substring(charactersToSkip);
984
1010
  const localDirty = await checkAndRegenerateFileLangTags(config, logger, file, path2);
985
1011
  if (localDirty) {
986
- logger.info('Lang tag configurations written for file "{path}"', { path: path2 });
987
1012
  dirty = true;
988
1013
  }
989
1014
  }
@@ -1384,10 +1409,7 @@ async function handleFile(config, logger, cwdRelativeFilePath, event) {
1384
1409
  }
1385
1410
  const cwd = process.cwd();
1386
1411
  const absoluteFilePath = path.join(cwd, cwdRelativeFilePath);
1387
- const dirty = await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
1388
- if (dirty) {
1389
- logger.info(`Lang tag configurations written for file "{filePath}"`, { filePath: cwdRelativeFilePath });
1390
- }
1412
+ await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
1391
1413
  const files = await $LT_CollectCandidateFilesWithTags({ filesToScan: [cwdRelativeFilePath], config, logger });
1392
1414
  const namespaces = await $LT_GroupTagsToNamespaces({ logger, files, config });
1393
1415
  const changedNamespaces = await $LT_WriteToNamespaces({ config, namespaces, logger });
@@ -1417,7 +1439,7 @@ async function detectModuleSystem() {
1417
1439
  }
1418
1440
  async function generateDefaultConfig() {
1419
1441
  const moduleSystem = await detectModuleSystem();
1420
- const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator } = require('@lang-tag/cli/algorithms');`;
1442
+ const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator, configKeeper } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator, configKeeper } = require('@lang-tag/cli/algorithms');`;
1421
1443
  const exportStatement = moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
1422
1444
  return `${importStatement}
1423
1445
 
@@ -1429,6 +1451,7 @@ const generationAlgorithm = pathBasedConfigGenerator({
1429
1451
  clearOnDefaultNamespace: true,
1430
1452
  ignoreDirectories: ['core', 'utils', 'helpers']
1431
1453
  });
1454
+ const keeper = configKeeper();
1432
1455
 
1433
1456
  /** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
1434
1457
  const config = {
@@ -1441,9 +1464,11 @@ const config = {
1441
1464
  // We do not modify imported configurations
1442
1465
  if (event.isImportedLibrary) return;
1443
1466
 
1444
- if (event.config?.manual) return;
1467
+ if (event.config?.keep === 'both') return;
1445
1468
 
1446
1469
  await generationAlgorithm(event);
1470
+
1471
+ await keeper(event);
1447
1472
  },
1448
1473
  collect: {
1449
1474
  defaultNamespace: 'common',
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;
@@ -417,34 +427,50 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
417
427
  return false;
418
428
  }
419
429
  const replacements = [];
430
+ let lastUpdatedLine = 0;
420
431
  for (let tag of tags) {
421
432
  let newConfig = void 0;
422
433
  let shouldUpdate = false;
423
- await config.onConfigGeneration({
434
+ const frozenConfig = tag.parameterConfig ? deepFreezeObject(tag.parameterConfig) : tag.parameterConfig;
435
+ const event = {
424
436
  langTagConfig: config,
425
- config: tag.parameterConfig,
437
+ config: frozenConfig,
426
438
  absolutePath: file,
427
439
  relativePath: path2,
428
440
  isImportedLibrary: path2.startsWith(libraryImportsDir),
429
- save: (updatedConfig) => {
441
+ isSaved: false,
442
+ savedConfig: void 0,
443
+ save: (updatedConfig, triggerName) => {
444
+ if (!updatedConfig && updatedConfig !== null) throw new Error("Wrong config data");
430
445
  newConfig = updatedConfig;
431
446
  shouldUpdate = true;
447
+ event.isSaved = true;
448
+ event.savedConfig = updatedConfig;
449
+ logger.debug('Called save for "{path}" with config "{config}" triggered by: ("{trigger}")', { path: path2, config: JSON.stringify(updatedConfig), trigger: triggerName || "-" });
432
450
  }
433
- });
451
+ };
452
+ await config.onConfigGeneration(event);
434
453
  if (!shouldUpdate) {
435
454
  continue;
436
455
  }
437
- if (JSON5.stringify(tag.parameterConfig) !== JSON5.stringify(newConfig)) {
456
+ lastUpdatedLine = tag.line;
457
+ if (!isConfigSame(tag.parameterConfig, newConfig)) {
438
458
  replacements.push({ tag, config: newConfig });
439
459
  }
440
460
  }
441
461
  if (replacements.length) {
442
462
  const newContent = processor.replaceTags(fileContent, replacements);
443
463
  await writeFile(file, newContent, "utf-8");
464
+ logger.info('Lang tag configurations written for file "{path}" (file://{file}:{line})', { path: path2, file, line: lastUpdatedLine });
444
465
  return true;
445
466
  }
446
467
  return false;
447
468
  }
469
+ function isConfigSame(c1, c2) {
470
+ if (!c1 && !c2) return true;
471
+ if (c1 && typeof c1 === "object" && c2 && typeof c2 === "object" && JSON5.stringify(c1) === JSON5.stringify(c2)) return true;
472
+ return false;
473
+ }
448
474
  const CONFIG_FILE_NAME = ".lang-tag.config.js";
449
475
  const EXPORTS_FILE_NAME = ".lang-tag.exports.json";
450
476
  const LANG_TAG_DEFAULT_CONFIG = {
@@ -963,7 +989,6 @@ async function $LT_CMD_RegenerateTags() {
963
989
  const path2 = file.substring(charactersToSkip);
964
990
  const localDirty = await checkAndRegenerateFileLangTags(config, logger, file, path2);
965
991
  if (localDirty) {
966
- logger.info('Lang tag configurations written for file "{path}"', { path: path2 });
967
992
  dirty = true;
968
993
  }
969
994
  }
@@ -1364,10 +1389,7 @@ async function handleFile(config, logger, cwdRelativeFilePath, event) {
1364
1389
  }
1365
1390
  const cwd = process.cwd();
1366
1391
  const absoluteFilePath = path__default.join(cwd, cwdRelativeFilePath);
1367
- const dirty = await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
1368
- if (dirty) {
1369
- logger.info(`Lang tag configurations written for file "{filePath}"`, { filePath: cwdRelativeFilePath });
1370
- }
1392
+ await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
1371
1393
  const files = await $LT_CollectCandidateFilesWithTags({ filesToScan: [cwdRelativeFilePath], config, logger });
1372
1394
  const namespaces = await $LT_GroupTagsToNamespaces({ logger, files, config });
1373
1395
  const changedNamespaces = await $LT_WriteToNamespaces({ config, namespaces, logger });
@@ -1397,7 +1419,7 @@ async function detectModuleSystem() {
1397
1419
  }
1398
1420
  async function generateDefaultConfig() {
1399
1421
  const moduleSystem = await detectModuleSystem();
1400
- const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator } = require('@lang-tag/cli/algorithms');`;
1422
+ const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator, configKeeper } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator, configKeeper } = require('@lang-tag/cli/algorithms');`;
1401
1423
  const exportStatement = moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
1402
1424
  return `${importStatement}
1403
1425
 
@@ -1409,6 +1431,7 @@ const generationAlgorithm = pathBasedConfigGenerator({
1409
1431
  clearOnDefaultNamespace: true,
1410
1432
  ignoreDirectories: ['core', 'utils', 'helpers']
1411
1433
  });
1434
+ const keeper = configKeeper();
1412
1435
 
1413
1436
  /** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
1414
1437
  const config = {
@@ -1421,9 +1444,11 @@ const config = {
1421
1444
  // We do not modify imported configurations
1422
1445
  if (event.isImportedLibrary) return;
1423
1446
 
1424
- if (event.config?.manual) return;
1447
+ if (event.config?.keep === 'both') return;
1425
1448
 
1426
1449
  await generationAlgorithm(event);
1450
+
1451
+ await keeper(event);
1427
1452
  },
1428
1453
  collect: {
1429
1454
  defaultNamespace: 'common',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lang-tag/cli",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -13,9 +13,14 @@ import { createCallableTranslations } from 'lang-tag';
13
13
  import { ReactNode, useMemo } from 'react';
14
14
  {{/isReact}}
15
15
 
16
+ {{#isTypeScript}}
17
+ interface TagConfig extends LangTagTranslationsConfig {
18
+ keep?: 'namespace' | 'path' | 'both'
19
+ }
20
+ {{/isTypeScript}}
16
21
  export function {{tagName}}{{#isTypeScript}}<T extends LangTagTranslations>{{/isTypeScript}}(
17
22
  baseTranslations{{#isTypeScript}}: T{{/isTypeScript}},
18
- config{{#isTypeScript}}?: LangTagTranslationsConfig{{/isTypeScript}},
23
+ config{{#isTypeScript}}?: TagConfig{{/isTypeScript}},
19
24
  ) {
20
25
  // Example integration with react-i18next:
21
26
  // const namespace = config?.namespace || '';
@@ -21,9 +21,14 @@ import {
21
21
  } from 'react';
22
22
  {{/isReact}}
23
23
 
24
+ {{#isTypeScript}}
25
+ interface TagConfig extends LangTagTranslationsConfig {
26
+ keep?: 'namespace' | 'path' | 'both'
27
+ }
28
+ {{/isTypeScript}}
24
29
  export function {{tagName}}<T extends LangTagTranslations>(
25
30
  baseTranslations: T,
26
- config?: LangTagTranslationsConfig,
31
+ config?: TagConfig,
27
32
  ) {
28
33
  const createTranslationHelper = (normalized{{#isTypeScript}}: CallableTranslations<T> | null{{/isTypeScript}}) =>
29
34
  createCallableTranslations(baseTranslations, config, {