@lang-tag/cli 0.11.2 → 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
@@ -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
@@ -447,35 +447,50 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
447
447
  return false;
448
448
  }
449
449
  const replacements = [];
450
+ let lastUpdatedLine = 0;
450
451
  for (let tag of tags) {
451
452
  let newConfig = void 0;
452
453
  let shouldUpdate = false;
453
454
  const frozenConfig = tag.parameterConfig ? deepFreezeObject(tag.parameterConfig) : tag.parameterConfig;
454
- await config.onConfigGeneration({
455
+ const event = {
455
456
  langTagConfig: config,
456
457
  config: frozenConfig,
457
458
  absolutePath: file,
458
459
  relativePath: path$12,
459
460
  isImportedLibrary: path$12.startsWith(libraryImportsDir),
460
- save: (updatedConfig) => {
461
- newConfig = updatedConfig || null;
461
+ isSaved: false,
462
+ savedConfig: void 0,
463
+ save: (updatedConfig, triggerName) => {
464
+ if (!updatedConfig && updatedConfig !== null) throw new Error("Wrong config data");
465
+ newConfig = updatedConfig;
462
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 || "-" });
463
470
  }
464
- });
471
+ };
472
+ await config.onConfigGeneration(event);
465
473
  if (!shouldUpdate) {
466
474
  continue;
467
475
  }
468
- if (JSON5.stringify(tag.parameterConfig) !== JSON5.stringify(newConfig)) {
476
+ lastUpdatedLine = tag.line;
477
+ if (!isConfigSame(tag.parameterConfig, newConfig)) {
469
478
  replacements.push({ tag, config: newConfig });
470
479
  }
471
480
  }
472
481
  if (replacements.length) {
473
482
  const newContent = processor.replaceTags(fileContent, replacements);
474
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 });
475
485
  return true;
476
486
  }
477
487
  return false;
478
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
+ }
479
494
  const CONFIG_FILE_NAME = ".lang-tag.config.js";
480
495
  const EXPORTS_FILE_NAME = ".lang-tag.exports.json";
481
496
  const LANG_TAG_DEFAULT_CONFIG = {
@@ -994,7 +1009,6 @@ async function $LT_CMD_RegenerateTags() {
994
1009
  const path2 = file.substring(charactersToSkip);
995
1010
  const localDirty = await checkAndRegenerateFileLangTags(config, logger, file, path2);
996
1011
  if (localDirty) {
997
- logger.info('Lang tag configurations written for file "{path}"', { path: path2 });
998
1012
  dirty = true;
999
1013
  }
1000
1014
  }
@@ -1395,10 +1409,7 @@ async function handleFile(config, logger, cwdRelativeFilePath, event) {
1395
1409
  }
1396
1410
  const cwd = process.cwd();
1397
1411
  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
- }
1412
+ await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
1402
1413
  const files = await $LT_CollectCandidateFilesWithTags({ filesToScan: [cwdRelativeFilePath], config, logger });
1403
1414
  const namespaces = await $LT_GroupTagsToNamespaces({ logger, files, config });
1404
1415
  const changedNamespaces = await $LT_WriteToNamespaces({ config, namespaces, logger });
@@ -1428,7 +1439,7 @@ async function detectModuleSystem() {
1428
1439
  }
1429
1440
  async function generateDefaultConfig() {
1430
1441
  const moduleSystem = await detectModuleSystem();
1431
- 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');`;
1432
1443
  const exportStatement = moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
1433
1444
  return `${importStatement}
1434
1445
 
@@ -1440,6 +1451,7 @@ const generationAlgorithm = pathBasedConfigGenerator({
1440
1451
  clearOnDefaultNamespace: true,
1441
1452
  ignoreDirectories: ['core', 'utils', 'helpers']
1442
1453
  });
1454
+ const keeper = configKeeper();
1443
1455
 
1444
1456
  /** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
1445
1457
  const config = {
@@ -1452,9 +1464,11 @@ const config = {
1452
1464
  // We do not modify imported configurations
1453
1465
  if (event.isImportedLibrary) return;
1454
1466
 
1455
- if (event.config?.manual) return;
1467
+ if (event.config?.keep === 'both') return;
1456
1468
 
1457
1469
  await generationAlgorithm(event);
1470
+
1471
+ await keeper(event);
1458
1472
  },
1459
1473
  collect: {
1460
1474
  defaultNamespace: 'common',
package/index.js CHANGED
@@ -427,35 +427,50 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
427
427
  return false;
428
428
  }
429
429
  const replacements = [];
430
+ let lastUpdatedLine = 0;
430
431
  for (let tag of tags) {
431
432
  let newConfig = void 0;
432
433
  let shouldUpdate = false;
433
434
  const frozenConfig = tag.parameterConfig ? deepFreezeObject(tag.parameterConfig) : tag.parameterConfig;
434
- await config.onConfigGeneration({
435
+ const event = {
435
436
  langTagConfig: config,
436
437
  config: frozenConfig,
437
438
  absolutePath: file,
438
439
  relativePath: path2,
439
440
  isImportedLibrary: path2.startsWith(libraryImportsDir),
440
- save: (updatedConfig) => {
441
- newConfig = updatedConfig || null;
441
+ isSaved: false,
442
+ savedConfig: void 0,
443
+ save: (updatedConfig, triggerName) => {
444
+ if (!updatedConfig && updatedConfig !== null) throw new Error("Wrong config data");
445
+ newConfig = updatedConfig;
442
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 || "-" });
443
450
  }
444
- });
451
+ };
452
+ await config.onConfigGeneration(event);
445
453
  if (!shouldUpdate) {
446
454
  continue;
447
455
  }
448
- if (JSON5.stringify(tag.parameterConfig) !== JSON5.stringify(newConfig)) {
456
+ lastUpdatedLine = tag.line;
457
+ if (!isConfigSame(tag.parameterConfig, newConfig)) {
449
458
  replacements.push({ tag, config: newConfig });
450
459
  }
451
460
  }
452
461
  if (replacements.length) {
453
462
  const newContent = processor.replaceTags(fileContent, replacements);
454
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 });
455
465
  return true;
456
466
  }
457
467
  return false;
458
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
+ }
459
474
  const CONFIG_FILE_NAME = ".lang-tag.config.js";
460
475
  const EXPORTS_FILE_NAME = ".lang-tag.exports.json";
461
476
  const LANG_TAG_DEFAULT_CONFIG = {
@@ -974,7 +989,6 @@ async function $LT_CMD_RegenerateTags() {
974
989
  const path2 = file.substring(charactersToSkip);
975
990
  const localDirty = await checkAndRegenerateFileLangTags(config, logger, file, path2);
976
991
  if (localDirty) {
977
- logger.info('Lang tag configurations written for file "{path}"', { path: path2 });
978
992
  dirty = true;
979
993
  }
980
994
  }
@@ -1375,10 +1389,7 @@ async function handleFile(config, logger, cwdRelativeFilePath, event) {
1375
1389
  }
1376
1390
  const cwd = process.cwd();
1377
1391
  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
- }
1392
+ await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
1382
1393
  const files = await $LT_CollectCandidateFilesWithTags({ filesToScan: [cwdRelativeFilePath], config, logger });
1383
1394
  const namespaces = await $LT_GroupTagsToNamespaces({ logger, files, config });
1384
1395
  const changedNamespaces = await $LT_WriteToNamespaces({ config, namespaces, logger });
@@ -1408,7 +1419,7 @@ async function detectModuleSystem() {
1408
1419
  }
1409
1420
  async function generateDefaultConfig() {
1410
1421
  const moduleSystem = await detectModuleSystem();
1411
- 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');`;
1412
1423
  const exportStatement = moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
1413
1424
  return `${importStatement}
1414
1425
 
@@ -1420,6 +1431,7 @@ const generationAlgorithm = pathBasedConfigGenerator({
1420
1431
  clearOnDefaultNamespace: true,
1421
1432
  ignoreDirectories: ['core', 'utils', 'helpers']
1422
1433
  });
1434
+ const keeper = configKeeper();
1423
1435
 
1424
1436
  /** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
1425
1437
  const config = {
@@ -1432,9 +1444,11 @@ const config = {
1432
1444
  // We do not modify imported configurations
1433
1445
  if (event.isImportedLibrary) return;
1434
1446
 
1435
- if (event.config?.manual) return;
1447
+ if (event.config?.keep === 'both') return;
1436
1448
 
1437
1449
  await generationAlgorithm(event);
1450
+
1451
+ await keeper(event);
1438
1452
  },
1439
1453
  collect: {
1440
1454
  defaultNamespace: 'common',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lang-tag/cli",
3
- "version": "0.11.2",
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, {