@lang-tag/cli 0.11.2 → 0.12.1

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,
@@ -83,13 +84,22 @@ function pathBasedConfigGenerator(options = {}) {
83
84
  const transformedParts = pathParts.map((part) => applyCaseTransform(part, pathCase));
84
85
  path$1 = transformedParts.join(".");
85
86
  }
86
- const newConfig = {};
87
+ const newConfig = event.config ? { ...event.config } : {};
87
88
  if (clearOnDefaultNamespace && namespace === actualFallbackNamespace) {
88
89
  if (path$1) {
89
90
  newConfig.path = path$1;
91
+ delete newConfig.namespace;
90
92
  } else {
91
- event.save(void 0);
92
- return;
93
+ const hasOtherProperties = event.config && Object.keys(event.config).some(
94
+ (key) => key !== "namespace" && key !== "path"
95
+ );
96
+ if (!hasOtherProperties) {
97
+ event.save(null, TRIGGER_NAME$1);
98
+ return;
99
+ } else {
100
+ delete newConfig.namespace;
101
+ delete newConfig.path;
102
+ }
93
103
  }
94
104
  } else {
95
105
  if (namespace) {
@@ -100,7 +110,7 @@ function pathBasedConfigGenerator(options = {}) {
100
110
  }
101
111
  }
102
112
  if (Object.keys(newConfig).length > 0) {
103
- event.save(newConfig);
113
+ event.save(newConfig, TRIGGER_NAME$1);
104
114
  }
105
115
  };
106
116
  }
@@ -156,4 +166,40 @@ function extractRootDirectoriesFromIncludes(includes) {
156
166
  }
157
167
  return Array.from(directories);
158
168
  }
169
+ const TRIGGER_NAME = "config-keeper";
170
+ function configKeeper(options = {}) {
171
+ const propertyName = options.propertyName ?? "keep";
172
+ return async (event) => {
173
+ if (!event.isSaved) {
174
+ return;
175
+ }
176
+ if (!event.config) {
177
+ return;
178
+ }
179
+ const keepMode = event.config[propertyName];
180
+ if (!keepMode) {
181
+ return;
182
+ }
183
+ if (keepMode !== "namespace" && keepMode !== "path" && keepMode !== "both") {
184
+ return;
185
+ }
186
+ let restoredConfig;
187
+ if (event.savedConfig === null) {
188
+ restoredConfig = { ...event.config };
189
+ delete restoredConfig.namespace;
190
+ delete restoredConfig.path;
191
+ } else {
192
+ restoredConfig = { ...event.savedConfig };
193
+ }
194
+ if ((keepMode === "namespace" || keepMode === "both") && event.config.namespace !== void 0) {
195
+ restoredConfig.namespace = event.config.namespace;
196
+ }
197
+ if ((keepMode === "path" || keepMode === "both") && event.config.path !== void 0) {
198
+ restoredConfig.path = event.config.path;
199
+ }
200
+ restoredConfig[propertyName] = keepMode;
201
+ event.save(restoredConfig, TRIGGER_NAME);
202
+ };
203
+ }
204
+ exports.configKeeper = configKeeper;
159
205
  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,
@@ -64,13 +65,22 @@ function pathBasedConfigGenerator(options = {}) {
64
65
  const transformedParts = pathParts.map((part) => applyCaseTransform(part, pathCase));
65
66
  path = transformedParts.join(".");
66
67
  }
67
- const newConfig = {};
68
+ const newConfig = event.config ? { ...event.config } : {};
68
69
  if (clearOnDefaultNamespace && namespace === actualFallbackNamespace) {
69
70
  if (path) {
70
71
  newConfig.path = path;
72
+ delete newConfig.namespace;
71
73
  } else {
72
- event.save(void 0);
73
- return;
74
+ const hasOtherProperties = event.config && Object.keys(event.config).some(
75
+ (key) => key !== "namespace" && key !== "path"
76
+ );
77
+ if (!hasOtherProperties) {
78
+ event.save(null, TRIGGER_NAME$1);
79
+ return;
80
+ } else {
81
+ delete newConfig.namespace;
82
+ delete newConfig.path;
83
+ }
74
84
  }
75
85
  } else {
76
86
  if (namespace) {
@@ -81,7 +91,7 @@ function pathBasedConfigGenerator(options = {}) {
81
91
  }
82
92
  }
83
93
  if (Object.keys(newConfig).length > 0) {
84
- event.save(newConfig);
94
+ event.save(newConfig, TRIGGER_NAME$1);
85
95
  }
86
96
  };
87
97
  }
@@ -137,6 +147,42 @@ function extractRootDirectoriesFromIncludes(includes) {
137
147
  }
138
148
  return Array.from(directories);
139
149
  }
150
+ const TRIGGER_NAME = "config-keeper";
151
+ function configKeeper(options = {}) {
152
+ const propertyName = options.propertyName ?? "keep";
153
+ return async (event) => {
154
+ if (!event.isSaved) {
155
+ return;
156
+ }
157
+ if (!event.config) {
158
+ return;
159
+ }
160
+ const keepMode = event.config[propertyName];
161
+ if (!keepMode) {
162
+ return;
163
+ }
164
+ if (keepMode !== "namespace" && keepMode !== "path" && keepMode !== "both") {
165
+ return;
166
+ }
167
+ let restoredConfig;
168
+ if (event.savedConfig === null) {
169
+ restoredConfig = { ...event.config };
170
+ delete restoredConfig.namespace;
171
+ delete restoredConfig.path;
172
+ } else {
173
+ restoredConfig = { ...event.savedConfig };
174
+ }
175
+ if ((keepMode === "namespace" || keepMode === "both") && event.config.namespace !== void 0) {
176
+ restoredConfig.namespace = event.config.namespace;
177
+ }
178
+ if ((keepMode === "path" || keepMode === "both") && event.config.path !== void 0) {
179
+ restoredConfig.path = event.config.path;
180
+ }
181
+ restoredConfig[propertyName] = keepMode;
182
+ event.save(restoredConfig, TRIGGER_NAME);
183
+ };
184
+ }
140
185
  export {
186
+ configKeeper,
141
187
  pathBasedConfigGenerator
142
188
  };
package/config.d.ts CHANGED
@@ -195,11 +195,22 @@ export interface LangTagCLIConfigGenerationEvent {
195
195
  */
196
196
  readonly config: Readonly<LangTagTranslationsConfig> | undefined;
197
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;
198
209
  /**
199
210
  * Tells CLI to replace tag configuration
200
- * undefined = means configuration will be removed
211
+ * null = means configuration will be removed
201
212
  **/
202
- save(config: LangTagTranslationsConfig | undefined): void;
213
+ save(config: LangTagTranslationsConfig | null, triggerName?: string): void;
203
214
  }
204
215
  export interface LangTagCLICollectConfigFixEvent {
205
216
  config: LangTagTranslationsConfig;
package/index.cjs CHANGED
@@ -173,7 +173,7 @@ class $LT_TagProcessor {
173
173
  }
174
174
  const tag = R.tag;
175
175
  let newTranslationsString = R.translations;
176
- if (!newTranslationsString) newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text;
176
+ if (!newTranslationsString) newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text || "{}";
177
177
  else if (typeof newTranslationsString !== "string") newTranslationsString = JSON5.stringify(newTranslationsString);
178
178
  if (!newTranslationsString) throw new Error("Tag must have translations provided!");
179
179
  try {
@@ -191,6 +191,9 @@ class $LT_TagProcessor {
191
191
  throw new Error(`Tag config is invalid object! Config: ${newConfigString}`);
192
192
  }
193
193
  }
194
+ if (newConfigString === null && this.config.translationArgPosition === 2) {
195
+ newConfigString = "{}";
196
+ }
194
197
  const arg1 = this.config.translationArgPosition === 1 ? newTranslationsString : newConfigString;
195
198
  const arg2 = this.config.translationArgPosition === 1 ? newConfigString : newTranslationsString;
196
199
  let tagFunction = `${this.config.tagName}(${arg1}`;
@@ -447,35 +450,50 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
447
450
  return false;
448
451
  }
449
452
  const replacements = [];
453
+ let lastUpdatedLine = 0;
450
454
  for (let tag of tags) {
451
455
  let newConfig = void 0;
452
456
  let shouldUpdate = false;
453
457
  const frozenConfig = tag.parameterConfig ? deepFreezeObject(tag.parameterConfig) : tag.parameterConfig;
454
- await config.onConfigGeneration({
458
+ const event = {
455
459
  langTagConfig: config,
456
460
  config: frozenConfig,
457
461
  absolutePath: file,
458
462
  relativePath: path$12,
459
463
  isImportedLibrary: path$12.startsWith(libraryImportsDir),
460
- save: (updatedConfig) => {
461
- newConfig = updatedConfig || null;
464
+ isSaved: false,
465
+ savedConfig: void 0,
466
+ save: (updatedConfig, triggerName) => {
467
+ if (!updatedConfig && updatedConfig !== null) throw new Error("Wrong config data");
468
+ newConfig = updatedConfig;
462
469
  shouldUpdate = true;
470
+ event.isSaved = true;
471
+ event.savedConfig = updatedConfig;
472
+ logger.debug('Called save for "{path}" with config "{config}" triggered by: ("{trigger}")', { path: path$12, config: JSON.stringify(updatedConfig), trigger: triggerName || "-" });
463
473
  }
464
- });
474
+ };
475
+ await config.onConfigGeneration(event);
465
476
  if (!shouldUpdate) {
466
477
  continue;
467
478
  }
468
- if (JSON5.stringify(tag.parameterConfig) !== JSON5.stringify(newConfig)) {
479
+ lastUpdatedLine = tag.line;
480
+ if (!isConfigSame(tag.parameterConfig, newConfig)) {
469
481
  replacements.push({ tag, config: newConfig });
470
482
  }
471
483
  }
472
484
  if (replacements.length) {
473
485
  const newContent = processor.replaceTags(fileContent, replacements);
474
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 });
475
488
  return true;
476
489
  }
477
490
  return false;
478
491
  }
492
+ function isConfigSame(c1, c2) {
493
+ if (!c1 && !c2) return true;
494
+ if (c1 && typeof c1 === "object" && c2 && typeof c2 === "object" && JSON5.stringify(c1) === JSON5.stringify(c2)) return true;
495
+ return false;
496
+ }
479
497
  const CONFIG_FILE_NAME = ".lang-tag.config.js";
480
498
  const EXPORTS_FILE_NAME = ".lang-tag.exports.json";
481
499
  const LANG_TAG_DEFAULT_CONFIG = {
@@ -497,7 +515,7 @@ const LANG_TAG_DEFAULT_CONFIG = {
497
515
  await event.logger.conflict(event.conflict, true);
498
516
  },
499
517
  onCollectFinish: (event) => {
500
- event.exit();
518
+ if (event.conflicts.length) event.exit();
501
519
  }
502
520
  },
503
521
  import: {
@@ -994,7 +1012,6 @@ async function $LT_CMD_RegenerateTags() {
994
1012
  const path2 = file.substring(charactersToSkip);
995
1013
  const localDirty = await checkAndRegenerateFileLangTags(config, logger, file, path2);
996
1014
  if (localDirty) {
997
- logger.info('Lang tag configurations written for file "{path}"', { path: path2 });
998
1015
  dirty = true;
999
1016
  }
1000
1017
  }
@@ -1201,18 +1218,18 @@ async function $LT_GroupTagsToNamespaces({ logger, files, config }) {
1201
1218
  }
1202
1219
  if (allConflicts.length > 0) {
1203
1220
  logger.warn(`Found ${allConflicts.length} conflicts.`);
1204
- if (config.collect?.onCollectFinish) {
1205
- let shouldContinue = true;
1206
- config.collect.onCollectFinish({
1207
- conflicts: allConflicts,
1208
- logger,
1209
- exit() {
1210
- shouldContinue = false;
1211
- }
1212
- });
1213
- if (!shouldContinue) {
1214
- throw new Error(`LangTagConflictResolution:Processing stopped due to collect finish handler`);
1221
+ }
1222
+ if (config.collect?.onCollectFinish) {
1223
+ let shouldContinue = true;
1224
+ config.collect.onCollectFinish({
1225
+ conflicts: allConflicts,
1226
+ logger,
1227
+ exit() {
1228
+ shouldContinue = false;
1215
1229
  }
1230
+ });
1231
+ if (!shouldContinue) {
1232
+ throw new Error(`LangTagConflictResolution:Processing stopped due to collect finish handler`);
1216
1233
  }
1217
1234
  }
1218
1235
  return namespaces;
@@ -1395,10 +1412,7 @@ async function handleFile(config, logger, cwdRelativeFilePath, event) {
1395
1412
  }
1396
1413
  const cwd = process.cwd();
1397
1414
  const absoluteFilePath = path.join(cwd, cwdRelativeFilePath);
1398
- const dirty = await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
1399
- if (dirty) {
1400
- logger.info(`Lang tag configurations written for file "{filePath}"`, { filePath: cwdRelativeFilePath });
1401
- }
1415
+ await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
1402
1416
  const files = await $LT_CollectCandidateFilesWithTags({ filesToScan: [cwdRelativeFilePath], config, logger });
1403
1417
  const namespaces = await $LT_GroupTagsToNamespaces({ logger, files, config });
1404
1418
  const changedNamespaces = await $LT_WriteToNamespaces({ config, namespaces, logger });
@@ -1428,7 +1442,7 @@ async function detectModuleSystem() {
1428
1442
  }
1429
1443
  async function generateDefaultConfig() {
1430
1444
  const moduleSystem = await detectModuleSystem();
1431
- const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator } = require('@lang-tag/cli/algorithms');`;
1445
+ const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator, configKeeper } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator, configKeeper } = require('@lang-tag/cli/algorithms');`;
1432
1446
  const exportStatement = moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
1433
1447
  return `${importStatement}
1434
1448
 
@@ -1440,6 +1454,7 @@ const generationAlgorithm = pathBasedConfigGenerator({
1440
1454
  clearOnDefaultNamespace: true,
1441
1455
  ignoreDirectories: ['core', 'utils', 'helpers']
1442
1456
  });
1457
+ const keeper = configKeeper();
1443
1458
 
1444
1459
  /** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
1445
1460
  const config = {
@@ -1452,9 +1467,11 @@ const config = {
1452
1467
  // We do not modify imported configurations
1453
1468
  if (event.isImportedLibrary) return;
1454
1469
 
1455
- if (event.config?.manual) return;
1470
+ if (event.config?.keep === 'both') return;
1456
1471
 
1457
1472
  await generationAlgorithm(event);
1473
+
1474
+ await keeper(event);
1458
1475
  },
1459
1476
  collect: {
1460
1477
  defaultNamespace: 'common',
@@ -1464,7 +1481,7 @@ const config = {
1464
1481
  // Call event.exit(); to terminate the process upon the first conflict
1465
1482
  },
1466
1483
  onCollectFinish: event => {
1467
- event.exit(); // Stop the process to avoid merging on conflict
1484
+ if (event.conflicts.length) event.exit(); // Stop the process to avoid merging on conflict
1468
1485
  }
1469
1486
  },
1470
1487
  translationArgPosition: 1,
package/index.js CHANGED
@@ -153,7 +153,7 @@ class $LT_TagProcessor {
153
153
  }
154
154
  const tag = R.tag;
155
155
  let newTranslationsString = R.translations;
156
- if (!newTranslationsString) newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text;
156
+ if (!newTranslationsString) newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text || "{}";
157
157
  else if (typeof newTranslationsString !== "string") newTranslationsString = JSON5.stringify(newTranslationsString);
158
158
  if (!newTranslationsString) throw new Error("Tag must have translations provided!");
159
159
  try {
@@ -171,6 +171,9 @@ class $LT_TagProcessor {
171
171
  throw new Error(`Tag config is invalid object! Config: ${newConfigString}`);
172
172
  }
173
173
  }
174
+ if (newConfigString === null && this.config.translationArgPosition === 2) {
175
+ newConfigString = "{}";
176
+ }
174
177
  const arg1 = this.config.translationArgPosition === 1 ? newTranslationsString : newConfigString;
175
178
  const arg2 = this.config.translationArgPosition === 1 ? newConfigString : newTranslationsString;
176
179
  let tagFunction = `${this.config.tagName}(${arg1}`;
@@ -427,35 +430,50 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
427
430
  return false;
428
431
  }
429
432
  const replacements = [];
433
+ let lastUpdatedLine = 0;
430
434
  for (let tag of tags) {
431
435
  let newConfig = void 0;
432
436
  let shouldUpdate = false;
433
437
  const frozenConfig = tag.parameterConfig ? deepFreezeObject(tag.parameterConfig) : tag.parameterConfig;
434
- await config.onConfigGeneration({
438
+ const event = {
435
439
  langTagConfig: config,
436
440
  config: frozenConfig,
437
441
  absolutePath: file,
438
442
  relativePath: path2,
439
443
  isImportedLibrary: path2.startsWith(libraryImportsDir),
440
- save: (updatedConfig) => {
441
- newConfig = updatedConfig || null;
444
+ isSaved: false,
445
+ savedConfig: void 0,
446
+ save: (updatedConfig, triggerName) => {
447
+ if (!updatedConfig && updatedConfig !== null) throw new Error("Wrong config data");
448
+ newConfig = updatedConfig;
442
449
  shouldUpdate = true;
450
+ event.isSaved = true;
451
+ event.savedConfig = updatedConfig;
452
+ logger.debug('Called save for "{path}" with config "{config}" triggered by: ("{trigger}")', { path: path2, config: JSON.stringify(updatedConfig), trigger: triggerName || "-" });
443
453
  }
444
- });
454
+ };
455
+ await config.onConfigGeneration(event);
445
456
  if (!shouldUpdate) {
446
457
  continue;
447
458
  }
448
- if (JSON5.stringify(tag.parameterConfig) !== JSON5.stringify(newConfig)) {
459
+ lastUpdatedLine = tag.line;
460
+ if (!isConfigSame(tag.parameterConfig, newConfig)) {
449
461
  replacements.push({ tag, config: newConfig });
450
462
  }
451
463
  }
452
464
  if (replacements.length) {
453
465
  const newContent = processor.replaceTags(fileContent, replacements);
454
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 });
455
468
  return true;
456
469
  }
457
470
  return false;
458
471
  }
472
+ function isConfigSame(c1, c2) {
473
+ if (!c1 && !c2) return true;
474
+ if (c1 && typeof c1 === "object" && c2 && typeof c2 === "object" && JSON5.stringify(c1) === JSON5.stringify(c2)) return true;
475
+ return false;
476
+ }
459
477
  const CONFIG_FILE_NAME = ".lang-tag.config.js";
460
478
  const EXPORTS_FILE_NAME = ".lang-tag.exports.json";
461
479
  const LANG_TAG_DEFAULT_CONFIG = {
@@ -477,7 +495,7 @@ const LANG_TAG_DEFAULT_CONFIG = {
477
495
  await event.logger.conflict(event.conflict, true);
478
496
  },
479
497
  onCollectFinish: (event) => {
480
- event.exit();
498
+ if (event.conflicts.length) event.exit();
481
499
  }
482
500
  },
483
501
  import: {
@@ -974,7 +992,6 @@ async function $LT_CMD_RegenerateTags() {
974
992
  const path2 = file.substring(charactersToSkip);
975
993
  const localDirty = await checkAndRegenerateFileLangTags(config, logger, file, path2);
976
994
  if (localDirty) {
977
- logger.info('Lang tag configurations written for file "{path}"', { path: path2 });
978
995
  dirty = true;
979
996
  }
980
997
  }
@@ -1181,18 +1198,18 @@ async function $LT_GroupTagsToNamespaces({ logger, files, config }) {
1181
1198
  }
1182
1199
  if (allConflicts.length > 0) {
1183
1200
  logger.warn(`Found ${allConflicts.length} conflicts.`);
1184
- if (config.collect?.onCollectFinish) {
1185
- let shouldContinue = true;
1186
- config.collect.onCollectFinish({
1187
- conflicts: allConflicts,
1188
- logger,
1189
- exit() {
1190
- shouldContinue = false;
1191
- }
1192
- });
1193
- if (!shouldContinue) {
1194
- throw new Error(`LangTagConflictResolution:Processing stopped due to collect finish handler`);
1201
+ }
1202
+ if (config.collect?.onCollectFinish) {
1203
+ let shouldContinue = true;
1204
+ config.collect.onCollectFinish({
1205
+ conflicts: allConflicts,
1206
+ logger,
1207
+ exit() {
1208
+ shouldContinue = false;
1195
1209
  }
1210
+ });
1211
+ if (!shouldContinue) {
1212
+ throw new Error(`LangTagConflictResolution:Processing stopped due to collect finish handler`);
1196
1213
  }
1197
1214
  }
1198
1215
  return namespaces;
@@ -1375,10 +1392,7 @@ async function handleFile(config, logger, cwdRelativeFilePath, event) {
1375
1392
  }
1376
1393
  const cwd = process.cwd();
1377
1394
  const absoluteFilePath = path__default.join(cwd, cwdRelativeFilePath);
1378
- const dirty = await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
1379
- if (dirty) {
1380
- logger.info(`Lang tag configurations written for file "{filePath}"`, { filePath: cwdRelativeFilePath });
1381
- }
1395
+ await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
1382
1396
  const files = await $LT_CollectCandidateFilesWithTags({ filesToScan: [cwdRelativeFilePath], config, logger });
1383
1397
  const namespaces = await $LT_GroupTagsToNamespaces({ logger, files, config });
1384
1398
  const changedNamespaces = await $LT_WriteToNamespaces({ config, namespaces, logger });
@@ -1408,7 +1422,7 @@ async function detectModuleSystem() {
1408
1422
  }
1409
1423
  async function generateDefaultConfig() {
1410
1424
  const moduleSystem = await detectModuleSystem();
1411
- const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator } = require('@lang-tag/cli/algorithms');`;
1425
+ const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator, configKeeper } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator, configKeeper } = require('@lang-tag/cli/algorithms');`;
1412
1426
  const exportStatement = moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
1413
1427
  return `${importStatement}
1414
1428
 
@@ -1420,6 +1434,7 @@ const generationAlgorithm = pathBasedConfigGenerator({
1420
1434
  clearOnDefaultNamespace: true,
1421
1435
  ignoreDirectories: ['core', 'utils', 'helpers']
1422
1436
  });
1437
+ const keeper = configKeeper();
1423
1438
 
1424
1439
  /** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
1425
1440
  const config = {
@@ -1432,9 +1447,11 @@ const config = {
1432
1447
  // We do not modify imported configurations
1433
1448
  if (event.isImportedLibrary) return;
1434
1449
 
1435
- if (event.config?.manual) return;
1450
+ if (event.config?.keep === 'both') return;
1436
1451
 
1437
1452
  await generationAlgorithm(event);
1453
+
1454
+ await keeper(event);
1438
1455
  },
1439
1456
  collect: {
1440
1457
  defaultNamespace: 'common',
@@ -1444,7 +1461,7 @@ const config = {
1444
1461
  // Call event.exit(); to terminate the process upon the first conflict
1445
1462
  },
1446
1463
  onCollectFinish: event => {
1447
- event.exit(); // Stop the process to avoid merging on conflict
1464
+ if (event.conflicts.length) event.exit(); // Stop the process to avoid merging on conflict
1448
1465
  }
1449
1466
  },
1450
1467
  translationArgPosition: 1,
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@lang-tag/cli",
3
- "version": "0.11.2",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/TheTonsOfCode/lang-tag"
10
+ "url": "https://github.com/TheTonsOfCode/lang-tag-cli"
11
11
  },
12
12
  "author": "TheTonsOfCode",
13
13
  "license": "MIT",
@@ -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, {