@lang-tag/cli 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs CHANGED
@@ -2,20 +2,20 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  const commander = require("commander");
5
+ const process$1 = require("node:process");
5
6
  const fs = require("fs");
6
- const promises = require("fs/promises");
7
- const JSON5 = require("json5");
8
- const path = require("path");
9
7
  const globby = require("globby");
8
+ const path = require("path");
9
+ const JSON5 = require("json5");
10
+ const promises = require("fs/promises");
10
11
  const path$1 = require("pathe");
11
12
  const url = require("url");
12
13
  require("case");
13
- const process$1 = require("node:process");
14
- const flexibleImportAlgorithm = require("./flexible-import-algorithm-Fa-l4jWj.cjs");
15
- const acorn = require("acorn");
14
+ const namespaceCollector = require("./chunks/namespace-collector.cjs");
16
15
  const micromatch = require("micromatch");
17
- const chokidar = require("chokidar");
16
+ const acorn = require("acorn");
18
17
  const mustache = require("mustache");
18
+ const chokidar = require("chokidar");
19
19
  var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
20
20
  function _interopNamespaceDefault(e) {
21
21
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -33,9 +33,61 @@ function _interopNamespaceDefault(e) {
33
33
  n.default = e;
34
34
  return Object.freeze(n);
35
35
  }
36
- const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path);
37
36
  const process__namespace = /* @__PURE__ */ _interopNamespaceDefault(process$1);
37
+ const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path);
38
38
  const acorn__namespace = /* @__PURE__ */ _interopNamespaceDefault(acorn);
39
+ function $LT_FilterInvalidTags(tags, config, logger) {
40
+ return tags.filter((tag) => {
41
+ if (tag.validity === "invalid-param-1")
42
+ logger.debug(
43
+ 'Skipping tag "{fullMatch}". Invalid JSON: "{invalid}"',
44
+ {
45
+ fullMatch: tag.fullMatch.trim(),
46
+ invalid: tag.parameter1Text
47
+ }
48
+ );
49
+ if (tag.validity === "invalid-param-2")
50
+ logger.debug(
51
+ 'Skipping tag "{fullMatch}". Invalid JSON: "{invalid}"',
52
+ {
53
+ fullMatch: tag.fullMatch.trim(),
54
+ invalid: tag.parameter2Text
55
+ }
56
+ );
57
+ if (tag.validity === "translations-not-found")
58
+ logger.debug(
59
+ 'Skipping tag "{fullMatch}". Translations not found at parameter position: {pos}',
60
+ {
61
+ fullMatch: tag.fullMatch.trim(),
62
+ pos: config.translationArgPosition
63
+ }
64
+ );
65
+ return tag.validity === "ok";
66
+ });
67
+ }
68
+ function $LT_FilterEmptyNamespaceTags(tags, logger) {
69
+ return tags.filter((tag) => {
70
+ if (!tag.parameterConfig) {
71
+ logger.warn(
72
+ 'Skipping tag "{fullMatch}". Tag configuration not defined. (Check lang-tag config at collect.onCollectConfigFix)',
73
+ {
74
+ fullMatch: tag.fullMatch.trim()
75
+ }
76
+ );
77
+ return false;
78
+ }
79
+ if (!tag.parameterConfig.namespace) {
80
+ logger.warn(
81
+ 'Skipping tag "{fullMatch}". Tag configuration namespace not defined. (Check lang-tag config at collect.onCollectConfigFix)',
82
+ {
83
+ fullMatch: tag.fullMatch.trim()
84
+ }
85
+ );
86
+ return false;
87
+ }
88
+ return true;
89
+ });
90
+ }
39
91
  class $LT_TagProcessor {
40
92
  constructor(config) {
41
93
  this.config = config;
@@ -46,7 +98,10 @@ class $LT_TagProcessor {
46
98
  const matches = [];
47
99
  let currentIndex = 0;
48
100
  const skipRanges = this.buildSkipRanges(fileContent);
49
- const startPattern = new RegExp(`${optionalVariableAssignment}${tagName}\\(\\s*\\{`, "g");
101
+ const startPattern = new RegExp(
102
+ `${optionalVariableAssignment}${tagName}\\(\\s*\\{`,
103
+ "g"
104
+ );
50
105
  while (true) {
51
106
  startPattern.lastIndex = currentIndex;
52
107
  const startMatch = startPattern.exec(fileContent);
@@ -68,7 +123,10 @@ class $LT_TagProcessor {
68
123
  currentIndex = matchStartIndex + 1;
69
124
  continue;
70
125
  }
71
- let parameter1Text = fileContent.substring(matchStartIndex + startMatch[0].length - 1, i);
126
+ let parameter1Text = fileContent.substring(
127
+ matchStartIndex + startMatch[0].length - 1,
128
+ i
129
+ );
72
130
  let parameter2Text;
73
131
  while (i < fileContent.length && (fileContent[i] === " " || fileContent[i] === "\n" || fileContent[i] === " ")) {
74
132
  i++;
@@ -120,7 +178,10 @@ class $LT_TagProcessor {
120
178
  }
121
179
  i++;
122
180
  const fullMatch = fileContent.substring(matchStartIndex, i);
123
- const { line, column } = getLineAndColumn(fileContent, matchStartIndex);
181
+ const { line, column } = getLineAndColumn(
182
+ fileContent,
183
+ matchStartIndex
184
+ );
124
185
  let validity = "ok";
125
186
  let parameter1 = void 0;
126
187
  let parameter2 = void 0;
@@ -135,7 +196,9 @@ class $LT_TagProcessor {
135
196
  parameter2 = JSON5.parse(parameter2Text);
136
197
  } catch (error) {
137
198
  try {
138
- parameter2 = JSON5.parse(this.escapeNewlinesInStrings(parameter2Text));
199
+ parameter2 = JSON5.parse(
200
+ this.escapeNewlinesInStrings(parameter2Text)
201
+ );
139
202
  } catch {
140
203
  validity = "invalid-param-2";
141
204
  }
@@ -143,7 +206,9 @@ class $LT_TagProcessor {
143
206
  }
144
207
  } catch (error) {
145
208
  try {
146
- parameter1 = JSON5.parse(this.escapeNewlinesInStrings(parameter1Text));
209
+ parameter1 = JSON5.parse(
210
+ this.escapeNewlinesInStrings(parameter1Text)
211
+ );
147
212
  } catch {
148
213
  validity = "invalid-param-1";
149
214
  }
@@ -177,22 +242,31 @@ class $LT_TagProcessor {
177
242
  }
178
243
  const tag = R.tag;
179
244
  let newTranslationsString = R.translations;
180
- if (!newTranslationsString) newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text || "{}";
181
- else if (typeof newTranslationsString !== "string") newTranslationsString = JSON5.stringify(newTranslationsString);
182
- if (!newTranslationsString) throw new Error("Tag must have translations provided!");
245
+ if (!newTranslationsString)
246
+ newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text || "{}";
247
+ else if (typeof newTranslationsString !== "string")
248
+ newTranslationsString = JSON5.stringify(newTranslationsString);
249
+ if (!newTranslationsString)
250
+ throw new Error("Tag must have translations provided!");
183
251
  try {
184
252
  JSON5.parse(newTranslationsString);
185
253
  } catch (error) {
186
- throw new Error(`Tag translations are invalid object! Translations: ${newTranslationsString}`);
254
+ throw new Error(
255
+ `Tag translations are invalid object! Translations: ${newTranslationsString}`
256
+ );
187
257
  }
188
258
  let newConfigString = R.config;
189
- if (!newConfigString && newConfigString !== null) newConfigString = tag.parameterConfig;
259
+ if (!newConfigString && newConfigString !== null)
260
+ newConfigString = tag.parameterConfig;
190
261
  if (newConfigString) {
191
262
  try {
192
- if (typeof newConfigString === "string") JSON5.parse(newConfigString);
263
+ if (typeof newConfigString === "string")
264
+ JSON5.parse(newConfigString);
193
265
  else newConfigString = JSON5.stringify(newConfigString);
194
266
  } catch (error) {
195
- throw new Error(`Tag config is invalid object! Config: ${newConfigString}`);
267
+ throw new Error(
268
+ `Tag config is invalid object! Config: ${newConfigString}`
269
+ );
196
270
  }
197
271
  }
198
272
  if (newConfigString === null && this.config.translationArgPosition === 2) {
@@ -203,7 +277,8 @@ class $LT_TagProcessor {
203
277
  let tagFunction = `${this.config.tagName}(${arg1}`;
204
278
  if (arg2) tagFunction += `, ${arg2}`;
205
279
  tagFunction += ")";
206
- if (tag.variableName) replaceMap.set(tag, ` ${tag.variableName} = ${tagFunction}`);
280
+ if (tag.variableName)
281
+ replaceMap.set(tag, ` ${tag.variableName} = ${tagFunction}`);
207
282
  else replaceMap.set(tag, tagFunction);
208
283
  });
209
284
  let offset = 0;
@@ -396,127 +471,397 @@ function getLineAndColumn(text, matchIndex) {
396
471
  const column = lines[lines.length - 1].length + 1;
397
472
  return { line, column };
398
473
  }
399
- function $LT_FilterInvalidTags(tags, config, logger) {
400
- return tags.filter((tag) => {
401
- if (tag.validity === "invalid-param-1")
402
- logger.debug('Skipping tag "{fullMatch}". Invalid JSON: "{invalid}"', {
403
- fullMatch: tag.fullMatch.trim(),
404
- invalid: tag.parameter1Text
405
- });
406
- if (tag.validity === "invalid-param-2")
407
- logger.debug('Skipping tag "{fullMatch}". Invalid JSON: "{invalid}"', {
408
- fullMatch: tag.fullMatch.trim(),
409
- invalid: tag.parameter2Text
410
- });
411
- if (tag.validity === "translations-not-found")
412
- logger.debug('Skipping tag "{fullMatch}". Translations not found at parameter position: {pos}', {
413
- fullMatch: tag.fullMatch.trim(),
414
- pos: config.translationArgPosition
415
- });
416
- return tag.validity === "ok";
417
- });
418
- }
419
- function $LT_FilterEmptyNamespaceTags(tags, logger) {
420
- return tags.filter((tag) => {
421
- if (!tag.parameterConfig) {
422
- logger.warn('Skipping tag "{fullMatch}". Tag configuration not defined. (Check lang-tag config at collect.onCollectConfigFix)', {
423
- fullMatch: tag.fullMatch.trim()
424
- });
425
- return false;
474
+ async function $LT_CollectCandidateFilesWithTags(props) {
475
+ const { config, logger } = props;
476
+ const processor = new $LT_TagProcessor(config);
477
+ const cwd = process$1.cwd();
478
+ let filesToScan = props.filesToScan;
479
+ if (!filesToScan) {
480
+ filesToScan = await globby.globby(config.includes, {
481
+ cwd,
482
+ ignore: config.excludes,
483
+ absolute: true
484
+ });
485
+ }
486
+ const candidates = [];
487
+ for (const filePath of filesToScan) {
488
+ const fileContent = fs.readFileSync(filePath, "utf-8");
489
+ let tags = processor.extractTags(fileContent);
490
+ if (!tags.length) {
491
+ continue;
426
492
  }
427
- if (!tag.parameterConfig.namespace) {
428
- logger.warn('Skipping tag "{fullMatch}". Tag configuration namespace not defined. (Check lang-tag config at collect.onCollectConfigFix)', {
429
- fullMatch: tag.fullMatch.trim()
430
- });
431
- return false;
493
+ tags = $LT_FilterInvalidTags(tags, config, logger);
494
+ if (!tags.length) {
495
+ continue;
432
496
  }
433
- return true;
434
- });
435
- }
436
- function deepFreezeObject(obj) {
437
- const propNames = Object.getOwnPropertyNames(obj);
438
- for (const name of propNames) {
439
- const value = obj[name];
440
- if (value && typeof value === "object") {
441
- deepFreezeObject(value);
497
+ for (let tag of tags) {
498
+ tag.parameterConfig = config.collect.onCollectConfigFix({
499
+ config: tag.parameterConfig,
500
+ langTagConfig: config
501
+ });
442
502
  }
503
+ tags = $LT_FilterEmptyNamespaceTags(tags, logger);
504
+ const relativeFilePath = path.relative(cwd, filePath);
505
+ candidates.push({ relativeFilePath, tags });
443
506
  }
444
- return Object.freeze(obj);
507
+ return candidates;
445
508
  }
446
- async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
447
- let libraryImportsDir = config.import.dir;
448
- if (!libraryImportsDir.endsWith(path.sep)) libraryImportsDir += path.sep;
449
- const fileContent = fs.readFileSync(file, "utf-8");
450
- const processor = new $LT_TagProcessor(config);
451
- let tags = processor.extractTags(fileContent);
452
- tags = $LT_FilterInvalidTags(tags, config, logger);
453
- if (!tags.length) {
454
- return false;
509
+ async function $LT_GroupTagsToCollections({
510
+ logger,
511
+ files,
512
+ config
513
+ }) {
514
+ let totalTags = 0;
515
+ const collections = {};
516
+ function getTranslationsCollection(namespace) {
517
+ const collectionName = config.collect.collector.aggregateCollection(namespace);
518
+ const collection = collections[collectionName] || {};
519
+ if (!(collectionName in collections)) {
520
+ collections[collectionName] = collection;
521
+ }
522
+ return collection;
455
523
  }
456
- const replacements = [];
457
- let lastUpdatedLine = 0;
458
- for (let tag of tags) {
459
- let newConfig = void 0;
460
- let shouldUpdate = false;
461
- const frozenConfig = tag.parameterConfig ? deepFreezeObject(tag.parameterConfig) : tag.parameterConfig;
462
- const event = {
463
- langTagConfig: config,
464
- config: frozenConfig,
465
- absolutePath: file,
466
- relativePath: path$12,
467
- isImportedLibrary: path$12.startsWith(libraryImportsDir),
468
- isSaved: false,
469
- savedConfig: void 0,
470
- save: (updatedConfig, triggerName) => {
471
- if (!updatedConfig && updatedConfig !== null) throw new Error("Wrong config data");
472
- newConfig = updatedConfig;
473
- shouldUpdate = true;
474
- event.isSaved = true;
475
- event.savedConfig = updatedConfig;
476
- logger.debug('Called save for "{path}" with config "{config}" triggered by: ("{trigger}")', { path: path$12, config: JSON.stringify(updatedConfig), trigger: triggerName || "-" });
524
+ const allConflicts = [];
525
+ const existingValuesByNamespace = /* @__PURE__ */ new Map();
526
+ for (const file of files) {
527
+ totalTags += file.tags.length;
528
+ for (const _tag of file.tags) {
529
+ const tag = config.collect.collector.transformTag(_tag);
530
+ const tagConfig = tag.parameterConfig;
531
+ const collection = getTranslationsCollection(tagConfig.namespace);
532
+ let existingValues = existingValuesByNamespace.get(
533
+ tagConfig.namespace
534
+ );
535
+ if (!existingValues) {
536
+ existingValues = /* @__PURE__ */ new Map();
537
+ existingValuesByNamespace.set(
538
+ tagConfig.namespace,
539
+ existingValues
540
+ );
477
541
  }
478
- };
479
- await config.onConfigGeneration(event);
480
- if (!shouldUpdate) {
481
- continue;
482
- }
483
- lastUpdatedLine = tag.line;
484
- if (!isConfigSame(tag.parameterConfig, newConfig)) {
485
- replacements.push({ tag, config: newConfig });
542
+ const valueTracker = {
543
+ get: (path2) => existingValues.get(path2),
544
+ trackValue: (path2, value) => {
545
+ existingValues.set(path2, {
546
+ tag,
547
+ relativeFilePath: file.relativeFilePath,
548
+ value
549
+ });
550
+ }
551
+ };
552
+ const addConflict = async (path2, tagA, tagBValue, conflictType) => {
553
+ if (conflictType === "path_overwrite" && config.collect?.ignoreConflictsWithMatchingValues !== false && tagA.value === tagBValue) {
554
+ return;
555
+ }
556
+ const conflict = {
557
+ path: path2,
558
+ tagA,
559
+ tagB: {
560
+ tag,
561
+ relativeFilePath: file.relativeFilePath,
562
+ value: tagBValue
563
+ },
564
+ conflictType
565
+ };
566
+ if (config.collect?.onConflictResolution) {
567
+ let shouldContinue = true;
568
+ await config.collect.onConflictResolution({
569
+ conflict,
570
+ logger,
571
+ exit() {
572
+ shouldContinue = false;
573
+ }
574
+ });
575
+ if (!shouldContinue) {
576
+ throw new Error(
577
+ `LangTagConflictResolution:Processing stopped due to conflict resolution: ${conflict.tagA.tag.parameterConfig.namespace}|${conflict.path}`
578
+ );
579
+ }
580
+ }
581
+ allConflicts.push(conflict);
582
+ };
583
+ const target = await ensureNestedObject(
584
+ tagConfig.path,
585
+ collection,
586
+ valueTracker,
587
+ addConflict
588
+ );
589
+ await mergeWithConflictDetection(
590
+ target,
591
+ tag.parameterTranslations,
592
+ tagConfig.path || "",
593
+ valueTracker,
594
+ addConflict
595
+ );
486
596
  }
487
597
  }
488
- if (replacements.length) {
489
- const newContent = processor.replaceTags(fileContent, replacements);
490
- await promises.writeFile(file, newContent, "utf-8");
491
- const encodedFile = encodeURI(file);
492
- logger.info('Lang tag configurations written for file "{path}" (file://{file}:{line})', { path: path$12, file: encodedFile, line: lastUpdatedLine });
493
- return true;
598
+ if (allConflicts.length > 0) {
599
+ logger.warn(`Found ${allConflicts.length} conflicts.`);
494
600
  }
495
- return false;
601
+ if (config.collect?.onCollectFinish) {
602
+ let shouldContinue = true;
603
+ config.collect.onCollectFinish({
604
+ totalTags,
605
+ namespaces: collections,
606
+ conflicts: allConflicts,
607
+ logger,
608
+ exit() {
609
+ shouldContinue = false;
610
+ }
611
+ });
612
+ if (!shouldContinue) {
613
+ throw new Error(
614
+ `LangTagConflictResolution:Processing stopped due to collect finish handler`
615
+ );
616
+ }
617
+ }
618
+ return collections;
496
619
  }
497
- function isConfigSame(c1, c2) {
498
- if (!c1 && !c2) return true;
499
- if (c1 && typeof c1 === "object" && c2 && typeof c2 === "object" && JSON5.stringify(c1) === JSON5.stringify(c2)) return true;
500
- return false;
620
+ async function ensureNestedObject(path2, rootCollection, valueTracker, addConflict) {
621
+ if (!path2 || !path2.trim()) return rootCollection;
622
+ let current = rootCollection;
623
+ let currentPath = "";
624
+ for (const key of path2.split(".")) {
625
+ currentPath = currentPath ? `${currentPath}.${key}` : key;
626
+ if (current[key] !== void 0 && typeof current[key] !== "object") {
627
+ const existingInfo = valueTracker.get(currentPath);
628
+ if (existingInfo) {
629
+ await addConflict(
630
+ currentPath,
631
+ existingInfo,
632
+ {},
633
+ "type_mismatch"
634
+ );
635
+ }
636
+ return current;
637
+ }
638
+ current[key] = current[key] || {};
639
+ current = current[key];
640
+ }
641
+ return current;
501
642
  }
502
- const CONFIG_FILE_NAME = ".lang-tag.config.js";
503
- const EXPORTS_FILE_NAME = "lang-tags.json";
504
- const LANG_TAG_DEFAULT_CONFIG = {
505
- tagName: "lang",
506
- isLibrary: false,
507
- includes: ["src/**/*.{js,ts,jsx,tsx}"],
508
- excludes: ["node_modules", "dist", "build"],
509
- localesDirectory: "locales",
510
- baseLanguageCode: "en",
511
- collect: {
512
- collector: new flexibleImportAlgorithm.NamespaceCollector(),
643
+ async function mergeWithConflictDetection(target, source, basePath = "", valueTracker, addConflict) {
644
+ if (typeof target !== "object" || typeof source !== "object") {
645
+ return;
646
+ }
647
+ for (const key in source) {
648
+ if (!source.hasOwnProperty(key)) {
649
+ continue;
650
+ }
651
+ const currentPath = basePath ? `${basePath}.${key}` : key;
652
+ let targetValue = target[key];
653
+ const sourceValue = source[key];
654
+ if (Array.isArray(sourceValue)) {
655
+ continue;
656
+ }
657
+ if (targetValue !== void 0) {
658
+ const targetType = typeof targetValue;
659
+ const sourceType = typeof sourceValue;
660
+ let existingInfo = valueTracker.get(currentPath);
661
+ if (!existingInfo && targetType === "object" && targetValue !== null && !Array.isArray(targetValue)) {
662
+ const findNestedInfo = (obj, prefix) => {
663
+ for (const key2 in obj) {
664
+ const path2 = prefix ? `${prefix}.${key2}` : key2;
665
+ const info = valueTracker.get(path2);
666
+ if (info) {
667
+ return info;
668
+ }
669
+ if (typeof obj[key2] === "object" && obj[key2] !== null && !Array.isArray(obj[key2])) {
670
+ const nestedInfo = findNestedInfo(obj[key2], path2);
671
+ if (nestedInfo) {
672
+ return nestedInfo;
673
+ }
674
+ }
675
+ }
676
+ return void 0;
677
+ };
678
+ existingInfo = findNestedInfo(targetValue, currentPath);
679
+ }
680
+ if (targetType !== sourceType) {
681
+ if (existingInfo) {
682
+ await addConflict(
683
+ currentPath,
684
+ existingInfo,
685
+ sourceValue,
686
+ "type_mismatch"
687
+ );
688
+ }
689
+ continue;
690
+ }
691
+ if (targetType !== "object") {
692
+ if (existingInfo) {
693
+ await addConflict(
694
+ currentPath,
695
+ existingInfo,
696
+ sourceValue,
697
+ "path_overwrite"
698
+ );
699
+ }
700
+ if (targetValue !== sourceValue) {
701
+ continue;
702
+ }
703
+ }
704
+ }
705
+ if (typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue)) {
706
+ if (!targetValue) {
707
+ targetValue = {};
708
+ target[key] = targetValue;
709
+ }
710
+ await mergeWithConflictDetection(
711
+ targetValue,
712
+ sourceValue,
713
+ currentPath,
714
+ valueTracker,
715
+ addConflict
716
+ );
717
+ } else {
718
+ target[key] = sourceValue;
719
+ valueTracker.trackValue(currentPath, sourceValue);
720
+ }
721
+ }
722
+ }
723
+ const CONFIG_FILE_NAME = "lang-tag.config.js";
724
+ const EXPORTS_FILE_NAME = "lang-tags.json";
725
+ async function $LT_EnsureDirectoryExists(filePath) {
726
+ await promises.mkdir(filePath, { recursive: true });
727
+ }
728
+ async function $LT_WriteJSON(filePath, data) {
729
+ await promises.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
730
+ }
731
+ async function $LT_ReadJSON(filePath) {
732
+ const content = await promises.readFile(filePath, "utf-8");
733
+ return JSON.parse(content);
734
+ }
735
+ async function $LT_WriteFileWithDirs(filePath, content) {
736
+ const dir = path.dirname(filePath);
737
+ try {
738
+ await promises.mkdir(dir, { recursive: true });
739
+ } catch (error) {
740
+ }
741
+ await promises.writeFile(filePath, content, "utf-8");
742
+ }
743
+ async function $LT_ReadFileContent(relativeFilePath) {
744
+ const cwd = process.cwd();
745
+ const absolutePath = path.resolve(cwd, relativeFilePath);
746
+ return await promises.readFile(absolutePath, "utf-8");
747
+ }
748
+ async function $LT_WriteAsExportFile({
749
+ config,
750
+ logger,
751
+ files
752
+ }) {
753
+ const packageJson = await $LT_ReadJSON(
754
+ path.resolve(process.cwd(), "package.json")
755
+ );
756
+ if (!packageJson) {
757
+ throw new Error("package.json not found");
758
+ }
759
+ const exportData = {
760
+ baseLanguageCode: config.baseLanguageCode,
761
+ files: files.map(({ relativeFilePath, tags }) => ({
762
+ relativeFilePath,
763
+ tags: tags.map((tag) => ({
764
+ variableName: tag.variableName,
765
+ config: tag.parameterConfig,
766
+ translations: tag.parameterTranslations
767
+ }))
768
+ }))
769
+ };
770
+ await promises.writeFile(EXPORTS_FILE_NAME, JSON.stringify(exportData), "utf-8");
771
+ logger.success(`Written {file}`, { file: EXPORTS_FILE_NAME });
772
+ }
773
+ function deepMergeTranslations(target, source) {
774
+ if (typeof target !== "object") {
775
+ throw new Error("Target must be an object");
776
+ }
777
+ if (typeof source !== "object") {
778
+ throw new Error("Source must be an object");
779
+ }
780
+ let changed = false;
781
+ for (const key in source) {
782
+ if (!source.hasOwnProperty(key)) {
783
+ continue;
784
+ }
785
+ let targetValue = target[key];
786
+ const sourceValue = source[key];
787
+ if (typeof targetValue === "string" && typeof sourceValue === "object") {
788
+ throw new Error(
789
+ `Trying to write object into target key "${key}" which is translation already`
790
+ );
791
+ }
792
+ if (Array.isArray(sourceValue)) {
793
+ throw new Error(
794
+ `Trying to write array into target key "${key}", we do not allow arrays inside translations`
795
+ );
796
+ }
797
+ if (typeof sourceValue === "object") {
798
+ if (!targetValue) {
799
+ targetValue = {};
800
+ target[key] = targetValue;
801
+ }
802
+ if (deepMergeTranslations(targetValue, sourceValue)) {
803
+ changed = true;
804
+ }
805
+ } else {
806
+ if (target[key] !== sourceValue) {
807
+ changed = true;
808
+ }
809
+ target[key] = sourceValue;
810
+ }
811
+ }
812
+ return changed;
813
+ }
814
+ async function $LT_WriteToCollections({
815
+ config,
816
+ collections,
817
+ logger,
818
+ clean
819
+ }) {
820
+ await config.collect.collector.preWrite(clean);
821
+ const changedCollections = [];
822
+ for (let collectionName of Object.keys(collections)) {
823
+ if (!collectionName) {
824
+ continue;
825
+ }
826
+ const filePath = await config.collect.collector.resolveCollectionFilePath(
827
+ collectionName
828
+ );
829
+ let originalJSON = {};
830
+ try {
831
+ originalJSON = await $LT_ReadJSON(filePath);
832
+ } catch (e) {
833
+ await config.collect.collector.onMissingCollection(
834
+ collectionName
835
+ );
836
+ }
837
+ if (deepMergeTranslations(originalJSON, collections[collectionName])) {
838
+ changedCollections.push(collectionName);
839
+ await $LT_WriteJSON(filePath, originalJSON);
840
+ }
841
+ }
842
+ await config.collect.collector.postWrite(changedCollections);
843
+ }
844
+ const LANG_TAG_DEFAULT_CONFIG = {
845
+ tagName: "lang",
846
+ isLibrary: false,
847
+ includes: ["src/**/*.{js,ts,jsx,tsx}"],
848
+ excludes: ["node_modules", "dist", "build"],
849
+ localesDirectory: "locales",
850
+ baseLanguageCode: "en",
851
+ collect: {
852
+ collector: new namespaceCollector.NamespaceCollector(),
513
853
  defaultNamespace: "common",
514
854
  ignoreConflictsWithMatchingValues: true,
515
855
  onCollectConfigFix: ({ config, langTagConfig }) => {
516
856
  if (langTagConfig.isLibrary) return config;
517
- if (!config) return { path: "", namespace: langTagConfig.collect.defaultNamespace };
857
+ if (!config)
858
+ return {
859
+ path: "",
860
+ namespace: langTagConfig.collect.defaultNamespace
861
+ };
518
862
  if (!config.path) config.path = "";
519
- if (!config.namespace) config.namespace = langTagConfig.collect.defaultNamespace;
863
+ if (!config.namespace)
864
+ config.namespace = langTagConfig.collect.defaultNamespace;
520
865
  return config;
521
866
  },
522
867
  onConflictResolution: async (event) => {
@@ -529,21 +874,40 @@ const LANG_TAG_DEFAULT_CONFIG = {
529
874
  import: {
530
875
  dir: "src/lang-libraries",
531
876
  tagImportPath: 'import { lang } from "@/my-lang-tag-path"',
532
- onImport: flexibleImportAlgorithm.flexibleImportAlgorithm({
533
- filePath: {
534
- includePackageInPath: true
877
+ onImport: (event) => {
878
+ for (let e of event.exports) {
879
+ event.logger.info(
880
+ "Detected lang tag exports at package {packageName}",
881
+ { packageName: e.packageJSON.name }
882
+ );
535
883
  }
536
- })
537
- // onImport: ({importedRelativePath, fileGenerationData}: LangTagCLIOnImportParams, actions)=> {
538
- // const exportIndex = (fileGenerationData.index || 0) + 1;
539
- // fileGenerationData.index = exportIndex;
540
- //
541
- // actions.setFile(path.basename(importedRelativePath));
542
- // actions.setExportName(`translations${exportIndex}`);
543
- // }
884
+ event.logger.warn(
885
+ `
886
+ Import Algorithm Not Configured
887
+
888
+ To import external language tags, you need to configure an import algorithm.
889
+
890
+ Setup Instructions:
891
+ 1. Add this import at the top of your configuration file:
892
+ {importStr}
893
+
894
+ 2. Replace import.onImport function with:
895
+ {onImport}
896
+
897
+ This will enable import of language tags from external packages.
898
+ `.trim(),
899
+ {
900
+ importStr: "const { flexibleImportAlgorithm } = require('@lang-tag/cli/algorithms');",
901
+ onImport: "onImport: flexibleImportAlgorithm({ filePath: { includePackageInPath: true } })"
902
+ }
903
+ );
904
+ }
544
905
  },
545
906
  translationArgPosition: 1,
546
907
  onConfigGeneration: async (event) => {
908
+ event.logger.info(
909
+ "Config generation event is not configured. Add onConfigGeneration handler to customize config generation."
910
+ );
547
911
  }
548
912
  };
549
913
  async function $LT_ReadConfig(projectPath) {
@@ -559,7 +923,9 @@ async function $LT_ReadConfig(projectPath) {
559
923
  const userConfig = configModule.default || {};
560
924
  const tn = (userConfig.tagName || "").toLowerCase().replace(/[-_\s]/g, "");
561
925
  if (tn === "langtag" || tn === "lang-tag") {
562
- throw new Error('Custom tagName cannot be "lang-tag" or "langtag"! (It is not recommended for use with libraries)\n');
926
+ throw new Error(
927
+ 'Custom tagName cannot be "lang-tag" or "langtag"! (It is not recommended for use with libraries)\n'
928
+ );
563
929
  }
564
930
  const config = {
565
931
  ...LANG_TAG_DEFAULT_CONFIG,
@@ -581,6 +947,75 @@ async function $LT_ReadConfig(projectPath) {
581
947
  throw error;
582
948
  }
583
949
  }
950
+ const ANSI$1 = {
951
+ reset: "\x1B[0m",
952
+ white: "\x1B[97m",
953
+ brightCyan: "\x1B[96m",
954
+ green: "\x1B[92m",
955
+ gray: "\x1B[90m",
956
+ redBg: "\x1B[41m\x1B[97m",
957
+ bold: "\x1B[1m"
958
+ };
959
+ function colorizeFromAST(code, nodes) {
960
+ const ranges = [];
961
+ for (const node of nodes) {
962
+ const color = getColorForNodeType(node.type);
963
+ const priority = getPriorityForNodeType(node.type);
964
+ ranges.push({
965
+ start: node.start,
966
+ end: node.end,
967
+ color,
968
+ priority
969
+ });
970
+ }
971
+ ranges.sort((a, b) => {
972
+ if (b.start !== a.start) return b.start - a.start;
973
+ return b.priority - a.priority;
974
+ });
975
+ let colorized = code;
976
+ for (const range of ranges) {
977
+ const before = colorized.substring(0, range.start);
978
+ const text = colorized.substring(range.start, range.end);
979
+ const after = colorized.substring(range.end);
980
+ colorized = before + range.color + text + ANSI$1.reset + after;
981
+ }
982
+ return colorized;
983
+ }
984
+ function getColorForNodeType(type) {
985
+ switch (type) {
986
+ case "key":
987
+ return ANSI$1.brightCyan;
988
+ case "bracket":
989
+ case "colon":
990
+ return ANSI$1.gray;
991
+ case "value":
992
+ return ANSI$1.green;
993
+ case "comment":
994
+ return ANSI$1.gray;
995
+ case "error":
996
+ return ANSI$1.bold + ANSI$1.redBg;
997
+ default:
998
+ return ANSI$1.white;
999
+ }
1000
+ }
1001
+ function getPriorityForNodeType(type) {
1002
+ switch (type) {
1003
+ case "error":
1004
+ return 3;
1005
+ // Highest priority
1006
+ case "key":
1007
+ return 2;
1008
+ case "value":
1009
+ return 1;
1010
+ case "bracket":
1011
+ case "colon":
1012
+ case "comment":
1013
+ return 0;
1014
+ // Lowest priority
1015
+ default:
1016
+ return 0;
1017
+ }
1018
+ }
584
1019
  function parseObjectAST(code) {
585
1020
  const nodes = [];
586
1021
  try {
@@ -674,81 +1109,12 @@ function markConflictNodes(nodes, conflictPath) {
674
1109
  return node;
675
1110
  });
676
1111
  }
677
- const ANSI$1 = {
1112
+ const ANSI = {
678
1113
  reset: "\x1B[0m",
679
1114
  white: "\x1B[97m",
680
- brightCyan: "\x1B[96m",
681
- green: "\x1B[92m",
682
- gray: "\x1B[90m",
683
- redBg: "\x1B[41m\x1B[97m",
684
- bold: "\x1B[1m"
685
- };
686
- function colorizeFromAST(code, nodes) {
687
- const ranges = [];
688
- for (const node of nodes) {
689
- const color = getColorForNodeType(node.type);
690
- const priority = getPriorityForNodeType(node.type);
691
- ranges.push({
692
- start: node.start,
693
- end: node.end,
694
- color,
695
- priority
696
- });
697
- }
698
- ranges.sort((a, b) => {
699
- if (b.start !== a.start) return b.start - a.start;
700
- return b.priority - a.priority;
701
- });
702
- let colorized = code;
703
- for (const range of ranges) {
704
- const before = colorized.substring(0, range.start);
705
- const text = colorized.substring(range.start, range.end);
706
- const after = colorized.substring(range.end);
707
- colorized = before + range.color + text + ANSI$1.reset + after;
708
- }
709
- return colorized;
710
- }
711
- function getColorForNodeType(type) {
712
- switch (type) {
713
- case "key":
714
- return ANSI$1.brightCyan;
715
- case "bracket":
716
- case "colon":
717
- return ANSI$1.gray;
718
- case "value":
719
- return ANSI$1.green;
720
- case "comment":
721
- return ANSI$1.gray;
722
- case "error":
723
- return ANSI$1.bold + ANSI$1.redBg;
724
- default:
725
- return ANSI$1.white;
726
- }
727
- }
728
- function getPriorityForNodeType(type) {
729
- switch (type) {
730
- case "error":
731
- return 3;
732
- // Highest priority
733
- case "key":
734
- return 2;
735
- case "value":
736
- return 1;
737
- case "bracket":
738
- case "colon":
739
- case "comment":
740
- return 0;
741
- // Lowest priority
742
- default:
743
- return 0;
744
- }
745
- }
746
- const ANSI = {
747
- reset: "\x1B[0m",
748
- white: "\x1B[97m",
749
- cyan: "\x1B[96m",
750
- gray: "\x1B[37m",
751
- darkGray: "\x1B[90m",
1115
+ cyan: "\x1B[96m",
1116
+ gray: "\x1B[37m",
1117
+ darkGray: "\x1B[90m",
752
1118
  bold: "\x1B[1m"
753
1119
  };
754
1120
  function getVisibleLines(totalLines, errorLines, threshold = 10) {
@@ -774,18 +1140,24 @@ function printLines(lines, startLineNumber, errorLines = /* @__PURE__ */ new Set
774
1140
  lines.forEach((line, i) => {
775
1141
  const lineNumber = startLineNumber + i;
776
1142
  const lineNumStr = String(lineNumber).padStart(3, " ");
777
- console.log(`${ANSI.gray}${lineNumStr}${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${line}`);
1143
+ console.log(
1144
+ `${ANSI.gray}${lineNumStr}${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${line}`
1145
+ );
778
1146
  });
779
1147
  } else {
780
1148
  let lastPrinted = -2;
781
1149
  lines.forEach((line, i) => {
782
1150
  if (visibleLines.has(i)) {
783
1151
  if (i > lastPrinted + 1 && lastPrinted >= 0) {
784
- console.log(`${ANSI.gray} -${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${ANSI.gray}...${ANSI.reset}`);
1152
+ console.log(
1153
+ `${ANSI.gray} -${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${ANSI.gray}...${ANSI.reset}`
1154
+ );
785
1155
  }
786
1156
  const lineNumber = startLineNumber + i;
787
1157
  const lineNumStr = String(lineNumber).padStart(3, " ");
788
- console.log(`${ANSI.gray}${lineNumStr}${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${line}`);
1158
+ console.log(
1159
+ `${ANSI.gray}${lineNumStr}${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${line}`
1160
+ );
789
1161
  lastPrinted = i;
790
1162
  }
791
1163
  });
@@ -793,7 +1165,7 @@ function printLines(lines, startLineNumber, errorLines = /* @__PURE__ */ new Set
793
1165
  }
794
1166
  async function getLangTagCodeSection(tagInfo) {
795
1167
  const { tag, relativeFilePath } = tagInfo;
796
- const fileContent = await flexibleImportAlgorithm.$LT_ReadFileContent(relativeFilePath);
1168
+ const fileContent = await $LT_ReadFileContent(relativeFilePath);
797
1169
  const fileLines = fileContent.split("\n");
798
1170
  const startLine = tag.line;
799
1171
  const endLine = tag.line + tag.fullMatch.split("\n").length - 1;
@@ -835,14 +1207,20 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
835
1207
  const wholeTagCode = await getLangTagCodeSection(tagInfo);
836
1208
  const translationTagCode = translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text;
837
1209
  const configTagCode = translationArgPosition === 1 ? tag.parameter2Text : tag.parameter1Text;
838
- const translationErrorPath = stripPrefix(conflictPath, tag.parameterConfig?.path);
1210
+ const translationErrorPath = stripPrefix(
1211
+ conflictPath,
1212
+ tag.parameterConfig?.path
1213
+ );
839
1214
  let colorizedWhole = wholeTagCode;
840
1215
  let errorLines = /* @__PURE__ */ new Set();
841
1216
  if (translationTagCode) {
842
1217
  try {
843
1218
  const translationNodes = parseObjectAST(translationTagCode);
844
1219
  const markedTranslationNodes = translationErrorPath ? markConflictNodes(translationNodes, translationErrorPath) : translationNodes;
845
- const translationErrorLines = getErrorLineNumbers(translationTagCode, markedTranslationNodes);
1220
+ const translationErrorLines = getErrorLineNumbers(
1221
+ translationTagCode,
1222
+ markedTranslationNodes
1223
+ );
846
1224
  const translationStartInWhole = wholeTagCode.indexOf(translationTagCode);
847
1225
  if (translationStartInWhole >= 0) {
848
1226
  const linesBeforeTranslation = wholeTagCode.substring(0, translationStartInWhole).split("\n").length - 1;
@@ -850,12 +1228,20 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
850
1228
  errorLines.add(linesBeforeTranslation + lineNum2);
851
1229
  });
852
1230
  if (translationErrorLines.size > 0) {
853
- const lastTranslationErrorLine = Math.max(...Array.from(translationErrorLines));
1231
+ const lastTranslationErrorLine = Math.max(
1232
+ ...Array.from(translationErrorLines)
1233
+ );
854
1234
  lineNum = startLine + linesBeforeTranslation + lastTranslationErrorLine;
855
1235
  }
856
1236
  }
857
- const colorizedTranslation = colorizeFromAST(translationTagCode, markedTranslationNodes);
858
- colorizedWhole = colorizedWhole.replace(translationTagCode, colorizedTranslation);
1237
+ const colorizedTranslation = colorizeFromAST(
1238
+ translationTagCode,
1239
+ markedTranslationNodes
1240
+ );
1241
+ colorizedWhole = colorizedWhole.replace(
1242
+ translationTagCode,
1243
+ colorizedTranslation
1244
+ );
859
1245
  } catch (error) {
860
1246
  console.error("Failed to colorize translation:", error);
861
1247
  }
@@ -877,7 +1263,10 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
877
1263
  }
878
1264
  return node;
879
1265
  });
880
- const configErrorLines = getErrorLineNumbers(configTagCode, markedConfigNodes);
1266
+ const configErrorLines = getErrorLineNumbers(
1267
+ configTagCode,
1268
+ markedConfigNodes
1269
+ );
881
1270
  const configStartInWhole = wholeTagCode.indexOf(configTagCode);
882
1271
  if (configStartInWhole >= 0) {
883
1272
  const linesBeforeConfig = wholeTagCode.substring(0, configStartInWhole).split("\n").length - 1;
@@ -885,14 +1274,22 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
885
1274
  errorLines.add(linesBeforeConfig + lineNum2);
886
1275
  });
887
1276
  }
888
- const colorizedConfig = colorizeFromAST(configTagCode, markedConfigNodes);
889
- colorizedWhole = colorizedWhole.replace(configTagCode, colorizedConfig);
1277
+ const colorizedConfig = colorizeFromAST(
1278
+ configTagCode,
1279
+ markedConfigNodes
1280
+ );
1281
+ colorizedWhole = colorizedWhole.replace(
1282
+ configTagCode,
1283
+ colorizedConfig
1284
+ );
890
1285
  } catch (error) {
891
1286
  console.error("Failed to colorize config:", error);
892
1287
  }
893
1288
  }
894
1289
  const encodedPath = encodeURI(filePath);
895
- console.log(`${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${encodedPath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`);
1290
+ console.log(
1291
+ `${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${encodedPath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`
1292
+ );
896
1293
  printLines(colorizedWhole.split("\n"), startLine, errorLines, condense);
897
1294
  } catch (error) {
898
1295
  console.error("Error displaying conflict:", error);
@@ -900,8 +1297,20 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
900
1297
  }
901
1298
  async function $LT_LogConflict(conflict, translationArgPosition, condense) {
902
1299
  const { path: conflictPath, tagA, tagB } = conflict;
903
- await logTagConflictInfo(tagA, "between", conflictPath, translationArgPosition, condense);
904
- await logTagConflictInfo(tagB, "and", conflictPath, translationArgPosition, condense);
1300
+ await logTagConflictInfo(
1301
+ tagA,
1302
+ "between",
1303
+ conflictPath,
1304
+ translationArgPosition,
1305
+ condense
1306
+ );
1307
+ await logTagConflictInfo(
1308
+ tagB,
1309
+ "and",
1310
+ conflictPath,
1311
+ translationArgPosition,
1312
+ condense
1313
+ );
905
1314
  }
906
1315
  const ANSI_COLORS = {
907
1316
  reset: "\x1B[0m",
@@ -915,14 +1324,18 @@ const ANSI_COLORS = {
915
1324
  cyan: "\x1B[36m"
916
1325
  };
917
1326
  function validateAndInterpolate(message, params) {
918
- const placeholders = Array.from(message.matchAll(/\{(\w+)\}/g)).map((m) => m[1]);
1327
+ const placeholders = Array.from(message.matchAll(/\{(\w+)\}/g)).map(
1328
+ (m) => m[1]
1329
+ );
919
1330
  const missing = placeholders.filter((p) => !(p in (params || {})));
920
1331
  if (missing.length) {
921
1332
  throw new Error(`Missing variables in message: ${missing.join(", ")}`);
922
1333
  }
923
1334
  const extra = params ? Object.keys(params).filter((k) => !placeholders.includes(k)) : [];
924
1335
  if (extra.length) {
925
- throw new Error(`Extra variables provided not used in message: ${extra.join(", ")}`);
1336
+ throw new Error(
1337
+ `Extra variables provided not used in message: ${extra.join(", ")}`
1338
+ );
926
1339
  }
927
1340
  const parts = [];
928
1341
  let lastIndex = 0;
@@ -968,12 +1381,24 @@ function $LT_CreateDefaultLogger(debugMode, translationArgPosition = 1) {
968
1381
  conflict: async (conflict, condense) => {
969
1382
  const { path: path2, conflictType, tagA } = conflict;
970
1383
  console.log();
971
- console.log(`${ANSI_COLORS.bold}${ANSI_COLORS.red}⚠ Translation Conflict Detected${ANSI_COLORS.reset}`);
972
- console.log(`${ANSI_COLORS.gray}${"─".repeat(60)}${ANSI_COLORS.reset}`);
973
- console.log(` ${ANSI_COLORS.cyan}Conflict Type:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${conflictType}${ANSI_COLORS.reset}`);
974
- console.log(` ${ANSI_COLORS.cyan}Translation Key:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${path2}${ANSI_COLORS.reset}`);
975
- console.log(` ${ANSI_COLORS.cyan}Namespace:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${tagA.tag.parameterConfig.namespace}${ANSI_COLORS.reset}`);
976
- console.log(`${ANSI_COLORS.gray}${"─".repeat(60)}${ANSI_COLORS.reset}`);
1384
+ console.log(
1385
+ `${ANSI_COLORS.bold}${ANSI_COLORS.red}⚠ Translation Conflict Detected${ANSI_COLORS.reset}`
1386
+ );
1387
+ console.log(
1388
+ `${ANSI_COLORS.gray}${"─".repeat(60)}${ANSI_COLORS.reset}`
1389
+ );
1390
+ console.log(
1391
+ ` ${ANSI_COLORS.cyan}Conflict Type:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${conflictType}${ANSI_COLORS.reset}`
1392
+ );
1393
+ console.log(
1394
+ ` ${ANSI_COLORS.cyan}Translation Key:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${path2}${ANSI_COLORS.reset}`
1395
+ );
1396
+ console.log(
1397
+ ` ${ANSI_COLORS.cyan}Namespace:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${tagA.tag.parameterConfig.namespace}${ANSI_COLORS.reset}`
1398
+ );
1399
+ console.log(
1400
+ `${ANSI_COLORS.gray}${"─".repeat(60)}${ANSI_COLORS.reset}`
1401
+ );
977
1402
  await $LT_LogConflict(conflict, translationArgPosition, condense);
978
1403
  console.log();
979
1404
  }
@@ -981,7 +1406,10 @@ function $LT_CreateDefaultLogger(debugMode, translationArgPosition = 1) {
981
1406
  }
982
1407
  async function $LT_GetCommandEssentials() {
983
1408
  const config = await $LT_ReadConfig(process$1.cwd());
984
- const logger = $LT_CreateDefaultLogger(config.debug, config.translationArgPosition);
1409
+ const logger = $LT_CreateDefaultLogger(
1410
+ config.debug,
1411
+ config.translationArgPosition
1412
+ );
985
1413
  config.collect.collector.config = config;
986
1414
  config.collect.collector.logger = logger;
987
1415
  return {
@@ -989,398 +1417,255 @@ async function $LT_GetCommandEssentials() {
989
1417
  logger
990
1418
  };
991
1419
  }
992
- async function $LT_CMD_RegenerateTags() {
1420
+ async function $LT_CMD_Collect(options) {
993
1421
  const { config, logger } = await $LT_GetCommandEssentials();
994
- const files = await globby.globby(config.includes, {
995
- cwd: process.cwd(),
996
- ignore: config.excludes,
997
- absolute: true
998
- });
999
- const charactersToSkip = process.cwd().length + 1;
1000
- let dirty = false;
1001
- for (const file of files) {
1002
- const path2 = file.substring(charactersToSkip);
1003
- const localDirty = await checkAndRegenerateFileLangTags(config, logger, file, path2);
1004
- if (localDirty) {
1005
- dirty = true;
1422
+ logger.info("Collecting translations from source files...");
1423
+ const files = await $LT_CollectCandidateFilesWithTags({ config, logger });
1424
+ if (config.debug) {
1425
+ for (let file of files) {
1426
+ logger.debug("Found {count} translations tags inside: {file}", {
1427
+ count: file.tags.length,
1428
+ file: file.relativeFilePath
1429
+ });
1006
1430
  }
1007
1431
  }
1008
- if (!dirty) {
1009
- logger.info("No changes were made based on the current configuration and files");
1432
+ if (config.isLibrary) {
1433
+ await $LT_WriteAsExportFile({ config, logger, files });
1434
+ return;
1010
1435
  }
1011
- }
1012
- function deepMergeTranslations(target, source) {
1013
- if (typeof target !== "object") {
1014
- throw new Error("Target must be an object");
1436
+ try {
1437
+ const collections = await $LT_GroupTagsToCollections({
1438
+ logger,
1439
+ files,
1440
+ config
1441
+ });
1442
+ const totalTags = files.reduce(
1443
+ (sum, file) => sum + file.tags.length,
1444
+ 0
1445
+ );
1446
+ logger.debug("Found {totalTags} translation tags", { totalTags });
1447
+ await $LT_WriteToCollections({
1448
+ config,
1449
+ collections,
1450
+ logger,
1451
+ clean: options?.clean
1452
+ });
1453
+ } catch (e) {
1454
+ const prefix = "LangTagConflictResolution:";
1455
+ if (e.message.startsWith(prefix)) {
1456
+ logger.error(e.message.substring(prefix.length));
1457
+ return;
1458
+ }
1459
+ throw e;
1015
1460
  }
1016
- if (typeof source !== "object") {
1017
- throw new Error("Source must be an object");
1461
+ }
1462
+ async function $LT_CollectExportFiles(logger) {
1463
+ const nodeModulesPath = path$1.join(process$1.cwd(), "node_modules");
1464
+ if (!fs.existsSync(nodeModulesPath)) {
1465
+ logger.error('"node_modules" directory not found');
1466
+ return [];
1018
1467
  }
1019
- let changed = false;
1020
- for (const key in source) {
1021
- if (!source.hasOwnProperty(key)) {
1022
- continue;
1023
- }
1024
- let targetValue = target[key];
1025
- const sourceValue = source[key];
1026
- if (typeof targetValue === "string" && typeof sourceValue === "object") {
1027
- throw new Error(`Trying to write object into target key "${key}" which is translation already`);
1028
- }
1029
- if (Array.isArray(sourceValue)) {
1030
- throw new Error(`Trying to write array into target key "${key}", we do not allow arrays inside translations`);
1031
- }
1032
- if (typeof sourceValue === "object") {
1033
- if (!targetValue) {
1034
- targetValue = {};
1035
- target[key] = targetValue;
1036
- }
1037
- if (deepMergeTranslations(targetValue, sourceValue)) {
1038
- changed = true;
1468
+ const results = [];
1469
+ try {
1470
+ const exportFiles = await globby.globby(
1471
+ [`node_modules/**/${EXPORTS_FILE_NAME}`],
1472
+ {
1473
+ cwd: process$1.cwd(),
1474
+ onlyFiles: true,
1475
+ ignore: ["**/node_modules/**/node_modules/**"],
1476
+ deep: 4
1039
1477
  }
1040
- } else {
1041
- if (target[key] !== sourceValue) {
1042
- changed = true;
1478
+ );
1479
+ for (const exportFile of exportFiles) {
1480
+ const fullExportPath = path$1.resolve(exportFile);
1481
+ const packageJsonPath = findPackageJsonForExport(
1482
+ fullExportPath,
1483
+ nodeModulesPath
1484
+ );
1485
+ if (packageJsonPath) {
1486
+ results.push({
1487
+ exportPath: fullExportPath,
1488
+ packageJsonPath
1489
+ });
1490
+ } else {
1491
+ logger.warn(
1492
+ "Found export file but could not match package.json: {exportPath}",
1493
+ {
1494
+ exportPath: fullExportPath
1495
+ }
1496
+ );
1043
1497
  }
1044
- target[key] = sourceValue;
1045
- }
1046
- }
1047
- return changed;
1048
- }
1049
- async function $LT_WriteToCollections({ config, collections, logger, clean }) {
1050
- await config.collect.collector.preWrite(clean);
1051
- const changedCollections = [];
1052
- for (let collectionName of Object.keys(collections)) {
1053
- if (!collectionName) {
1054
- continue;
1055
- }
1056
- const filePath = await config.collect.collector.resolveCollectionFilePath(collectionName);
1057
- let originalJSON = {};
1058
- try {
1059
- originalJSON = await flexibleImportAlgorithm.$LT_ReadJSON(filePath);
1060
- } catch (e) {
1061
- await config.collect.collector.onMissingCollection(collectionName);
1062
- }
1063
- if (deepMergeTranslations(originalJSON, collections[collectionName])) {
1064
- changedCollections.push(collectionName);
1065
- await flexibleImportAlgorithm.$LT_WriteJSON(filePath, originalJSON);
1066
1498
  }
1499
+ logger.debug(
1500
+ "Found {count} export files with matching package.json in node_modules",
1501
+ {
1502
+ count: results.length
1503
+ }
1504
+ );
1505
+ } catch (error) {
1506
+ logger.error("Error scanning node_modules with globby: {error}", {
1507
+ error: String(error)
1508
+ });
1067
1509
  }
1068
- await config.collect.collector.postWrite(changedCollections);
1510
+ return results;
1069
1511
  }
1070
- async function $LT_CollectCandidateFilesWithTags(props) {
1071
- const { config, logger } = props;
1072
- const processor = new $LT_TagProcessor(config);
1073
- const cwd = process$1.cwd();
1074
- let filesToScan = props.filesToScan;
1075
- if (!filesToScan) {
1076
- filesToScan = await globby.globby(config.includes, { cwd, ignore: config.excludes, absolute: true });
1512
+ function findPackageJsonForExport(exportPath, nodeModulesPath) {
1513
+ const relativePath = path$1.relative(nodeModulesPath, exportPath);
1514
+ const pathParts = relativePath.split(path$1.sep);
1515
+ if (pathParts.length < 2) {
1516
+ return null;
1077
1517
  }
1078
- const candidates = [];
1079
- for (const filePath of filesToScan) {
1080
- const fileContent = fs.readFileSync(filePath, "utf-8");
1081
- let tags = processor.extractTags(fileContent);
1082
- if (!tags.length) {
1083
- continue;
1084
- }
1085
- tags = $LT_FilterInvalidTags(tags, config, logger);
1086
- if (!tags.length) {
1087
- continue;
1518
+ if (pathParts[0].startsWith("@")) {
1519
+ if (pathParts.length >= 3) {
1520
+ const packageDir = path$1.join(
1521
+ nodeModulesPath,
1522
+ pathParts[0],
1523
+ pathParts[1]
1524
+ );
1525
+ const packageJsonPath = path$1.join(packageDir, "package.json");
1526
+ if (fs.existsSync(packageJsonPath)) {
1527
+ return packageJsonPath;
1528
+ }
1088
1529
  }
1089
- for (let tag of tags) {
1090
- tag.parameterConfig = config.collect.onCollectConfigFix({ config: tag.parameterConfig, langTagConfig: config });
1530
+ } else {
1531
+ const packageDir = path$1.join(nodeModulesPath, pathParts[0]);
1532
+ const packageJsonPath = path$1.join(packageDir, "package.json");
1533
+ if (fs.existsSync(packageJsonPath)) {
1534
+ return packageJsonPath;
1091
1535
  }
1092
- tags = $LT_FilterEmptyNamespaceTags(tags, logger);
1093
- const relativeFilePath = path.relative(cwd, filePath);
1094
- candidates.push({ relativeFilePath, tags });
1095
1536
  }
1096
- return candidates;
1537
+ return null;
1097
1538
  }
1098
- async function $LT_WriteAsExportFile({ config, logger, files }) {
1099
- const packageJson = await flexibleImportAlgorithm.$LT_ReadJSON(path.resolve(process.cwd(), "package.json"));
1100
- if (!packageJson) {
1101
- throw new Error("package.json not found");
1102
- }
1103
- const exportData = {
1104
- baseLanguageCode: config.baseLanguageCode,
1105
- files: files.map(({ relativeFilePath, tags }) => ({
1106
- relativeFilePath,
1107
- tags: tags.map((tag) => ({
1108
- variableName: tag.variableName,
1109
- config: tag.parameterConfig,
1110
- translations: tag.parameterTranslations
1111
- }))
1112
- }))
1113
- };
1114
- await promises.writeFile(EXPORTS_FILE_NAME, JSON.stringify(exportData), "utf-8");
1115
- logger.success(`Written {file}`, { file: EXPORTS_FILE_NAME });
1539
+ const __filename$1 = url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href);
1540
+ const __dirname$1 = path.dirname(__filename$1);
1541
+ const templatePath = path.join(
1542
+ __dirname$1,
1543
+ "templates",
1544
+ "import",
1545
+ "imported-tag.mustache"
1546
+ );
1547
+ const template = fs.readFileSync(templatePath, "utf-8");
1548
+ function renderTemplate$1(data) {
1549
+ return mustache.render(template, data, {}, { escape: (text) => text });
1116
1550
  }
1117
- async function $LT_GroupTagsToCollections({ logger, files, config }) {
1118
- let totalTags = 0;
1119
- const collections = {};
1120
- function getTranslationsCollection(namespace) {
1121
- const collectionName = config.collect.collector.aggregateCollection(namespace);
1122
- const collection = collections[collectionName] || {};
1123
- if (!(collectionName in collections)) {
1124
- collections[collectionName] = collection;
1125
- }
1126
- return collection;
1127
- }
1128
- const allConflicts = [];
1129
- const existingValuesByNamespace = /* @__PURE__ */ new Map();
1130
- for (const file of files) {
1131
- totalTags += file.tags.length;
1132
- for (const _tag of file.tags) {
1133
- const tag = config.collect.collector.transformTag(_tag);
1134
- const tagConfig = tag.parameterConfig;
1135
- const collection = getTranslationsCollection(tagConfig.namespace);
1136
- let existingValues = existingValuesByNamespace.get(tagConfig.namespace);
1137
- if (!existingValues) {
1138
- existingValues = /* @__PURE__ */ new Map();
1139
- existingValuesByNamespace.set(tagConfig.namespace, existingValues);
1140
- }
1141
- const valueTracker = {
1142
- get: (path2) => existingValues.get(path2),
1143
- trackValue: (path2, value) => {
1144
- existingValues.set(path2, { tag, relativeFilePath: file.relativeFilePath, value });
1145
- }
1146
- };
1147
- const addConflict = async (path2, tagA, tagBValue, conflictType) => {
1148
- if (conflictType === "path_overwrite" && config.collect?.ignoreConflictsWithMatchingValues !== false && tagA.value === tagBValue) {
1149
- return;
1150
- }
1151
- const conflict = {
1152
- path: path2,
1153
- tagA,
1154
- tagB: {
1155
- tag,
1156
- relativeFilePath: file.relativeFilePath,
1157
- value: tagBValue
1158
- },
1159
- conflictType
1160
- };
1161
- if (config.collect?.onConflictResolution) {
1162
- let shouldContinue = true;
1163
- await config.collect.onConflictResolution({
1164
- conflict,
1165
- logger,
1166
- exit() {
1167
- shouldContinue = false;
1168
- }
1169
- });
1170
- if (!shouldContinue) {
1171
- throw new Error(`LangTagConflictResolution:Processing stopped due to conflict resolution: ${conflict.tagA.tag.parameterConfig.namespace}|${conflict.path}`);
1172
- }
1551
+ async function generateImportFiles(config, logger, importManager) {
1552
+ const importedFiles = importManager.getImportedFiles();
1553
+ for (const importedFile of importedFiles) {
1554
+ const filePath = path$1.resolve(
1555
+ process__namespace.cwd(),
1556
+ config.import.dir,
1557
+ importedFile.pathRelativeToImportDir
1558
+ );
1559
+ const processedExports = importedFile.tags.map((tag) => {
1560
+ const parameter1 = config.translationArgPosition === 1 ? tag.translations : tag.config;
1561
+ const parameter2 = config.translationArgPosition === 1 ? tag.config : tag.translations;
1562
+ const hasParameter2 = parameter2 !== null && parameter2 !== void 0 && (typeof parameter2 !== "object" || Object.keys(parameter2).length > 0);
1563
+ return {
1564
+ name: tag.variableName,
1565
+ parameter1: JSON5.stringify(parameter1, void 0, 4),
1566
+ parameter2: hasParameter2 ? JSON5.stringify(parameter2, void 0, 4) : null,
1567
+ hasParameter2,
1568
+ config: {
1569
+ tagName: config.tagName
1173
1570
  }
1174
- allConflicts.push(conflict);
1175
1571
  };
1176
- const target = await ensureNestedObject(
1177
- tagConfig.path,
1178
- collection,
1179
- valueTracker,
1180
- addConflict
1181
- );
1182
- await mergeWithConflictDetection(
1183
- target,
1184
- tag.parameterTranslations,
1185
- tagConfig.path || "",
1186
- valueTracker,
1187
- addConflict
1188
- );
1189
- }
1190
- }
1191
- if (allConflicts.length > 0) {
1192
- logger.warn(`Found ${allConflicts.length} conflicts.`);
1193
- }
1194
- if (config.collect?.onCollectFinish) {
1195
- let shouldContinue = true;
1196
- config.collect.onCollectFinish({
1197
- totalTags,
1198
- namespaces: collections,
1199
- conflicts: allConflicts,
1200
- logger,
1201
- exit() {
1202
- shouldContinue = false;
1203
- }
1204
1572
  });
1205
- if (!shouldContinue) {
1206
- throw new Error(`LangTagConflictResolution:Processing stopped due to collect finish handler`);
1207
- }
1208
- }
1209
- return collections;
1210
- }
1211
- async function ensureNestedObject(path2, rootCollection, valueTracker, addConflict) {
1212
- if (!path2 || !path2.trim()) return rootCollection;
1213
- let current = rootCollection;
1214
- let currentPath = "";
1215
- for (const key of path2.split(".")) {
1216
- currentPath = currentPath ? `${currentPath}.${key}` : key;
1217
- if (current[key] !== void 0 && typeof current[key] !== "object") {
1218
- const existingInfo = valueTracker.get(currentPath);
1219
- if (existingInfo) {
1220
- await addConflict(currentPath, existingInfo, {}, "type_mismatch");
1221
- }
1222
- return current;
1223
- }
1224
- current[key] = current[key] || {};
1225
- current = current[key];
1573
+ const templateData = {
1574
+ tagImportPath: config.import.tagImportPath,
1575
+ exports: processedExports
1576
+ };
1577
+ const content = renderTemplate$1(templateData);
1578
+ await $LT_EnsureDirectoryExists(path.dirname(filePath));
1579
+ await promises.writeFile(filePath, content, "utf-8");
1580
+ logger.success('Created tag file: "{file}"', {
1581
+ file: importedFile.pathRelativeToImportDir
1582
+ });
1226
1583
  }
1227
- return current;
1228
1584
  }
1229
- async function mergeWithConflictDetection(target, source, basePath = "", valueTracker, addConflict) {
1230
- if (typeof target !== "object" || typeof source !== "object") {
1231
- return;
1585
+ class ImportManager {
1586
+ importedFiles = [];
1587
+ constructor() {
1588
+ this.importedFiles = [];
1232
1589
  }
1233
- for (const key in source) {
1234
- if (!source.hasOwnProperty(key)) {
1235
- continue;
1590
+ importTag(pathRelativeToImportDir, tag) {
1591
+ if (!pathRelativeToImportDir) {
1592
+ throw new Error(
1593
+ `pathRelativeToImportDir required, got: ${pathRelativeToImportDir}`
1594
+ );
1236
1595
  }
1237
- const currentPath = basePath ? `${basePath}.${key}` : key;
1238
- let targetValue = target[key];
1239
- const sourceValue = source[key];
1240
- if (Array.isArray(sourceValue)) {
1241
- continue;
1596
+ if (!tag?.variableName) {
1597
+ throw new Error(
1598
+ `tag.variableName required, got: ${tag?.variableName}`
1599
+ );
1242
1600
  }
1243
- if (targetValue !== void 0) {
1244
- const targetType = typeof targetValue;
1245
- const sourceType = typeof sourceValue;
1246
- let existingInfo = valueTracker.get(currentPath);
1247
- if (!existingInfo && targetType === "object" && targetValue !== null && !Array.isArray(targetValue)) {
1248
- const findNestedInfo = (obj, prefix) => {
1249
- for (const key2 in obj) {
1250
- const path2 = prefix ? `${prefix}.${key2}` : key2;
1251
- const info = valueTracker.get(path2);
1252
- if (info) {
1253
- return info;
1254
- }
1255
- if (typeof obj[key2] === "object" && obj[key2] !== null && !Array.isArray(obj[key2])) {
1256
- const nestedInfo = findNestedInfo(obj[key2], path2);
1257
- if (nestedInfo) {
1258
- return nestedInfo;
1259
- }
1260
- }
1261
- }
1262
- return void 0;
1263
- };
1264
- existingInfo = findNestedInfo(targetValue, currentPath);
1265
- }
1266
- if (targetType !== sourceType) {
1267
- if (existingInfo) {
1268
- await addConflict(currentPath, existingInfo, sourceValue, "type_mismatch");
1269
- }
1270
- continue;
1271
- }
1272
- if (targetType !== "object") {
1273
- if (existingInfo) {
1274
- await addConflict(currentPath, existingInfo, sourceValue, "path_overwrite");
1275
- }
1276
- if (targetValue !== sourceValue) {
1277
- continue;
1278
- }
1279
- }
1601
+ if (!this.isValidJavaScriptIdentifier(tag.variableName)) {
1602
+ throw new Error(
1603
+ `Invalid JavaScript identifier: "${tag.variableName}". Variable names must start with a letter, underscore, or dollar sign, and contain only letters, digits, underscores, and dollar signs.`
1604
+ );
1280
1605
  }
1281
- if (typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue)) {
1282
- if (!targetValue) {
1283
- targetValue = {};
1284
- target[key] = targetValue;
1285
- }
1286
- await mergeWithConflictDetection(
1287
- targetValue,
1288
- sourceValue,
1289
- currentPath,
1290
- valueTracker,
1291
- addConflict
1606
+ if (tag.translations == null) {
1607
+ throw new Error(`tag.translations required`);
1608
+ }
1609
+ let importedFile = this.importedFiles.find(
1610
+ (file) => file.pathRelativeToImportDir === pathRelativeToImportDir
1611
+ );
1612
+ if (importedFile) {
1613
+ const duplicateTag = importedFile.tags.find(
1614
+ (existingTag) => existingTag.variableName === tag.variableName
1292
1615
  );
1293
- } else {
1294
- target[key] = sourceValue;
1295
- valueTracker.trackValue(currentPath, sourceValue);
1616
+ if (duplicateTag) {
1617
+ throw new Error(
1618
+ `Duplicate variable name "${tag.variableName}" in file "${pathRelativeToImportDir}". Variable names must be unique within the same file.`
1619
+ );
1620
+ }
1296
1621
  }
1297
- }
1298
- }
1299
- async function $LT_CMD_Collect(options) {
1300
- const { config, logger } = await $LT_GetCommandEssentials();
1301
- logger.info("Collecting translations from source files...");
1302
- const files = await $LT_CollectCandidateFilesWithTags({ config, logger });
1303
- if (config.debug) {
1304
- for (let file of files) {
1305
- logger.debug("Found {count} translations tags inside: {file}", { count: file.tags.length, file: file.relativeFilePath });
1622
+ if (!importedFile) {
1623
+ importedFile = { pathRelativeToImportDir, tags: [] };
1624
+ this.importedFiles.push(importedFile);
1306
1625
  }
1626
+ importedFile.tags.push(tag);
1307
1627
  }
1308
- if (config.isLibrary) {
1309
- await $LT_WriteAsExportFile({ config, logger, files });
1310
- return;
1628
+ getImportedFiles() {
1629
+ return [...this.importedFiles];
1311
1630
  }
1312
- try {
1313
- const collections = await $LT_GroupTagsToCollections({ logger, files, config });
1314
- const totalTags = files.reduce((sum, file) => sum + file.tags.length, 0);
1315
- logger.debug("Found {totalTags} translation tags", { totalTags });
1316
- await $LT_WriteToCollections({ config, collections, logger, clean: options?.clean });
1317
- } catch (e) {
1318
- const prefix = "LangTagConflictResolution:";
1319
- if (e.message.startsWith(prefix)) {
1320
- logger.error(e.message.substring(prefix.length));
1321
- return;
1322
- }
1323
- throw e;
1631
+ getImportedFilesCount() {
1632
+ return this.importedFiles.length;
1324
1633
  }
1325
- }
1326
- function getBasePath(pattern) {
1327
- const globStartIndex = pattern.indexOf("*");
1328
- const braceStartIndex = pattern.indexOf("{");
1329
- let endIndex = -1;
1330
- if (globStartIndex !== -1 && braceStartIndex !== -1) {
1331
- endIndex = Math.min(globStartIndex, braceStartIndex);
1332
- } else if (globStartIndex !== -1) {
1333
- endIndex = globStartIndex;
1334
- } else if (braceStartIndex !== -1) {
1335
- endIndex = braceStartIndex;
1634
+ hasImportedFiles() {
1635
+ return this.importedFiles.length > 0;
1336
1636
  }
1337
- if (endIndex === -1) {
1338
- const lastSlashIndex = pattern.lastIndexOf("/");
1339
- return lastSlashIndex !== -1 ? pattern.substring(0, lastSlashIndex) : ".";
1637
+ isValidJavaScriptIdentifier(name) {
1638
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
1340
1639
  }
1341
- const lastSeparatorIndex = pattern.lastIndexOf("/", endIndex);
1342
- return lastSeparatorIndex === -1 ? "." : pattern.substring(0, lastSeparatorIndex);
1343
- }
1344
- function $LT_CreateChokidarWatcher(config) {
1345
- const cwd = process.cwd();
1346
- const baseDirsToWatch = [
1347
- ...new Set(config.includes.map((pattern) => getBasePath(pattern)))
1348
- ];
1349
- const finalDirsToWatch = baseDirsToWatch.map((dir) => dir === "." ? cwd : dir);
1350
- const ignored = [...config.excludes, "**/.git/**"];
1351
- return chokidar.watch(finalDirsToWatch, {
1352
- // Watch base directories
1353
- cwd,
1354
- ignored,
1355
- persistent: true,
1356
- ignoreInitial: true,
1357
- awaitWriteFinish: {
1358
- stabilityThreshold: 300,
1359
- pollInterval: 100
1360
- }
1361
- });
1362
1640
  }
1363
- async function $LT_WatchTranslations() {
1364
- const { config, logger } = await $LT_GetCommandEssentials();
1365
- await $LT_CMD_Collect();
1366
- const watcher = $LT_CreateChokidarWatcher(config);
1367
- logger.info("Starting watch mode for translations...");
1368
- logger.info("Watching for changes...");
1369
- logger.info("Press Ctrl+C to stop watching");
1370
- watcher.on("change", async (filePath) => await handleFile(config, logger, filePath)).on("add", async (filePath) => await handleFile(config, logger, filePath)).on("error", (error) => {
1371
- logger.error("Error in file watcher: {error}", { error });
1641
+ async function $LT_ImportLibraries(config, logger) {
1642
+ const exportFiles = await $LT_CollectExportFiles(logger);
1643
+ const importManager = new ImportManager();
1644
+ let exports2 = [];
1645
+ for (const { exportPath, packageJsonPath } of exportFiles) {
1646
+ const exportData = await $LT_ReadJSON(exportPath);
1647
+ const packageJSON = await $LT_ReadJSON(packageJsonPath);
1648
+ exports2.push({ packageJSON, exportData });
1649
+ }
1650
+ config.import.onImport({
1651
+ exports: exports2,
1652
+ importManager,
1653
+ logger,
1654
+ langTagConfig: config
1372
1655
  });
1373
- }
1374
- async function handleFile(config, logger, cwdRelativeFilePath, event) {
1375
- if (!micromatch.isMatch(cwdRelativeFilePath, config.includes)) {
1656
+ if (!importManager.hasImportedFiles()) {
1657
+ logger.warn("No tags were imported from any library files");
1376
1658
  return;
1377
1659
  }
1378
- const cwd = process.cwd();
1379
- const absoluteFilePath = path.join(cwd, cwdRelativeFilePath);
1380
- await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
1381
- const files = await $LT_CollectCandidateFilesWithTags({ filesToScan: [cwdRelativeFilePath], config, logger });
1382
- const namespaces = await $LT_GroupTagsToCollections({ logger, files, config });
1383
- await $LT_WriteToCollections({ config, collections: namespaces, logger });
1660
+ await generateImportFiles(config, logger, importManager);
1661
+ if (config.import.onImportFinish) config.import.onImportFinish();
1662
+ }
1663
+ async function $LT_ImportTranslations() {
1664
+ const { config, logger } = await $LT_GetCommandEssentials();
1665
+ await $LT_EnsureDirectoryExists(config.import.dir);
1666
+ logger.info("Importing translations from libraries...");
1667
+ await $LT_ImportLibraries(config, logger);
1668
+ logger.success("Successfully imported translations from libraries.");
1384
1669
  }
1385
1670
  async function detectModuleSystem() {
1386
1671
  const packageJsonPath = path.join(process.cwd(), "package.json");
@@ -1433,7 +1718,7 @@ const generationAlgorithm = pathBasedConfigGenerator({
1433
1718
  });
1434
1719
  const keeper = configKeeper({ propertyName: 'keep' });
1435
1720
 
1436
- /** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
1721
+ /** @type {import('@lang-tag/cli/type').LangTagCLIConfig} */
1437
1722
  const config = {
1438
1723
  tagName: 'lang',
1439
1724
  isLibrary: false,
@@ -1452,242 +1737,38 @@ const config = {
1452
1737
  await keeper(event);
1453
1738
  },
1454
1739
  collect: {
1455
- defaultNamespace: 'common',
1456
- onConflictResolution: async event => {
1457
- await event.logger.conflict(event.conflict, true);
1458
- // By default, continue processing even if conflicts occur
1459
- // Call event.exit(); to terminate the process upon the first conflict
1460
- },
1461
- onCollectFinish: event => {
1462
- if (event.conflicts.length) event.exit(); // Stop the process to avoid merging on conflict
1463
- }
1464
- },
1465
- translationArgPosition: 1,
1466
- debug: false,
1467
- };
1468
-
1469
- ${exportStatement}`;
1470
- }
1471
- async function $LT_CMD_InitConfig() {
1472
- const logger = $LT_CreateDefaultLogger();
1473
- if (fs.existsSync(CONFIG_FILE_NAME)) {
1474
- logger.success("Configuration file already exists. Please remove the existing configuration file before creating a new default one");
1475
- return;
1476
- }
1477
- try {
1478
- const configContent = await generateDefaultConfig();
1479
- await promises.writeFile(CONFIG_FILE_NAME, configContent, "utf-8");
1480
- logger.success("Configuration file created successfully");
1481
- } catch (error) {
1482
- logger.error(error?.message);
1483
- }
1484
- }
1485
- async function $LT_CollectExportFiles(logger) {
1486
- const nodeModulesPath = path$1.join(process$1.cwd(), "node_modules");
1487
- if (!fs.existsSync(nodeModulesPath)) {
1488
- logger.error('"node_modules" directory not found');
1489
- return [];
1490
- }
1491
- const results = [];
1492
- try {
1493
- const exportFiles = await globby.globby([
1494
- `node_modules/**/${EXPORTS_FILE_NAME}`
1495
- ], {
1496
- cwd: process$1.cwd(),
1497
- onlyFiles: true,
1498
- ignore: ["**/node_modules/**/node_modules/**"],
1499
- deep: 4
1500
- });
1501
- for (const exportFile of exportFiles) {
1502
- const fullExportPath = path$1.resolve(exportFile);
1503
- const packageJsonPath = findPackageJsonForExport(fullExportPath, nodeModulesPath);
1504
- if (packageJsonPath) {
1505
- results.push({
1506
- exportPath: fullExportPath,
1507
- packageJsonPath
1508
- });
1509
- } else {
1510
- logger.warn("Found export file but could not match package.json: {exportPath}", {
1511
- exportPath: fullExportPath
1512
- });
1513
- }
1514
- }
1515
- logger.debug("Found {count} export files with matching package.json in node_modules", {
1516
- count: results.length
1517
- });
1518
- } catch (error) {
1519
- logger.error("Error scanning node_modules with globby: {error}", {
1520
- error: String(error)
1521
- });
1522
- }
1523
- return results;
1524
- }
1525
- function findPackageJsonForExport(exportPath, nodeModulesPath) {
1526
- const relativePath = path$1.relative(nodeModulesPath, exportPath);
1527
- const pathParts = relativePath.split(path$1.sep);
1528
- if (pathParts.length < 2) {
1529
- return null;
1530
- }
1531
- if (pathParts[0].startsWith("@")) {
1532
- if (pathParts.length >= 3) {
1533
- const packageDir = path$1.join(nodeModulesPath, pathParts[0], pathParts[1]);
1534
- const packageJsonPath = path$1.join(packageDir, "package.json");
1535
- if (fs.existsSync(packageJsonPath)) {
1536
- return packageJsonPath;
1537
- }
1538
- }
1539
- } else {
1540
- const packageDir = path$1.join(nodeModulesPath, pathParts[0]);
1541
- const packageJsonPath = path$1.join(packageDir, "package.json");
1542
- if (fs.existsSync(packageJsonPath)) {
1543
- return packageJsonPath;
1544
- }
1545
- }
1546
- return null;
1547
- }
1548
- const __filename$1 = url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href);
1549
- const __dirname$1 = path.dirname(__filename$1);
1550
- const templatePath = path.join(__dirname$1, "templates", "import", "imported-tag.mustache");
1551
- const template = fs.readFileSync(templatePath, "utf-8");
1552
- function renderTemplate$1(data) {
1553
- return mustache.render(template, data, {}, { escape: (text) => text });
1554
- }
1555
- async function generateImportFiles(config, logger, importManager) {
1556
- const importedFiles = importManager.getImportedFiles();
1557
- for (const importedFile of importedFiles) {
1558
- const filePath = path$1.resolve(
1559
- process__namespace.cwd(),
1560
- config.import.dir,
1561
- importedFile.pathRelativeToImportDir
1562
- );
1563
- const processedExports = importedFile.tags.map((tag) => {
1564
- const parameter1 = config.translationArgPosition === 1 ? tag.translations : tag.config;
1565
- const parameter2 = config.translationArgPosition === 1 ? tag.config : tag.translations;
1566
- const hasParameter2 = parameter2 !== null && parameter2 !== void 0 && (typeof parameter2 !== "object" || Object.keys(parameter2).length > 0);
1567
- return {
1568
- name: tag.variableName,
1569
- parameter1: JSON5.stringify(parameter1, void 0, 4),
1570
- parameter2: hasParameter2 ? JSON5.stringify(parameter2, void 0, 4) : null,
1571
- hasParameter2,
1572
- config: {
1573
- tagName: config.tagName
1574
- }
1575
- };
1576
- });
1577
- const templateData = {
1578
- tagImportPath: config.import.tagImportPath,
1579
- exports: processedExports
1580
- };
1581
- const content = renderTemplate$1(templateData);
1582
- await flexibleImportAlgorithm.$LT_EnsureDirectoryExists(path.dirname(filePath));
1583
- await promises.writeFile(filePath, content, "utf-8");
1584
- logger.success('Created tag file: "{file}"', { file: importedFile.pathRelativeToImportDir });
1585
- }
1586
- }
1587
- class ImportManager {
1588
- importedFiles = [];
1589
- constructor() {
1590
- this.importedFiles = [];
1591
- }
1592
- importTag(pathRelativeToImportDir, tag) {
1593
- if (!pathRelativeToImportDir) {
1594
- throw new Error(`pathRelativeToImportDir required, got: ${pathRelativeToImportDir}`);
1595
- }
1596
- if (!tag?.variableName) {
1597
- throw new Error(`tag.variableName required, got: ${tag?.variableName}`);
1598
- }
1599
- if (!this.isValidJavaScriptIdentifier(tag.variableName)) {
1600
- throw new Error(`Invalid JavaScript identifier: "${tag.variableName}". Variable names must start with a letter, underscore, or dollar sign, and contain only letters, digits, underscores, and dollar signs.`);
1601
- }
1602
- if (tag.translations == null) {
1603
- throw new Error(`tag.translations required`);
1604
- }
1605
- let importedFile = this.importedFiles.find(
1606
- (file) => file.pathRelativeToImportDir === pathRelativeToImportDir
1607
- );
1608
- if (importedFile) {
1609
- const duplicateTag = importedFile.tags.find(
1610
- (existingTag) => existingTag.variableName === tag.variableName
1611
- );
1612
- if (duplicateTag) {
1613
- throw new Error(`Duplicate variable name "${tag.variableName}" in file "${pathRelativeToImportDir}". Variable names must be unique within the same file.`);
1614
- }
1615
- }
1616
- if (!importedFile) {
1617
- importedFile = { pathRelativeToImportDir, tags: [] };
1618
- this.importedFiles.push(importedFile);
1619
- }
1620
- importedFile.tags.push(tag);
1621
- }
1622
- getImportedFiles() {
1623
- return [...this.importedFiles];
1624
- }
1625
- getImportedFilesCount() {
1626
- return this.importedFiles.length;
1627
- }
1628
- hasImportedFiles() {
1629
- return this.importedFiles.length > 0;
1630
- }
1631
- isValidJavaScriptIdentifier(name) {
1632
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
1633
- }
1740
+ defaultNamespace: 'common',
1741
+ onConflictResolution: async event => {
1742
+ await event.logger.conflict(event.conflict, true);
1743
+ // By default, continue processing even if conflicts occur
1744
+ // Call event.exit(); to terminate the process upon the first conflict
1745
+ },
1746
+ onCollectFinish: event => {
1747
+ if (event.conflicts.length) event.exit(); // Stop the process to avoid merging on conflict
1748
+ }
1749
+ },
1750
+ translationArgPosition: 1,
1751
+ debug: false,
1752
+ };
1753
+
1754
+ ${exportStatement}`;
1634
1755
  }
1635
- async function $LT_ImportLibraries(config, logger) {
1636
- const exportFiles = await $LT_CollectExportFiles(logger);
1637
- const importManager = new ImportManager();
1638
- let exports2 = [];
1639
- for (const { exportPath, packageJsonPath } of exportFiles) {
1640
- const exportData = await flexibleImportAlgorithm.$LT_ReadJSON(exportPath);
1641
- const packageJSON = await flexibleImportAlgorithm.$LT_ReadJSON(packageJsonPath);
1642
- exports2.push({ packageJSON, exportData });
1643
- }
1644
- config.import.onImport({ exports: exports2, importManager, logger, langTagConfig: config });
1645
- if (!importManager.hasImportedFiles()) {
1646
- logger.warn("No tags were imported from any library files");
1756
+ async function $LT_CMD_InitConfig() {
1757
+ const logger = $LT_CreateDefaultLogger();
1758
+ if (fs.existsSync(CONFIG_FILE_NAME)) {
1759
+ logger.success(
1760
+ "Configuration file already exists. Please remove the existing configuration file before creating a new default one"
1761
+ );
1647
1762
  return;
1648
1763
  }
1649
- await generateImportFiles(config, logger, importManager);
1650
- if (config.import.onImportFinish) config.import.onImportFinish();
1651
- }
1652
- async function $LT_ImportTranslations() {
1653
- const { config, logger } = await $LT_GetCommandEssentials();
1654
- await flexibleImportAlgorithm.$LT_EnsureDirectoryExists(config.import.dir);
1655
- logger.info("Importing translations from libraries...");
1656
- await $LT_ImportLibraries(config, logger);
1657
- logger.success("Successfully imported translations from libraries.");
1658
- }
1659
- function renderTemplate(template2, data) {
1660
- return mustache.render(template2, data, {}, { escape: (text) => text });
1661
- }
1662
- function loadTemplate(templateName) {
1663
- const __filename2 = url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename2).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href);
1664
- const __dirname = path.dirname(__filename2);
1665
- const templatePath2 = path.join(__dirname, "templates", "tag", `${templateName}.mustache`);
1666
1764
  try {
1667
- return fs.readFileSync(templatePath2, "utf-8");
1765
+ const configContent = await generateDefaultConfig();
1766
+ await promises.writeFile(CONFIG_FILE_NAME, configContent, "utf-8");
1767
+ logger.success("Configuration file created successfully");
1668
1768
  } catch (error) {
1669
- throw new Error(`Failed to load template ${templateName}: ${error}`);
1769
+ logger.error(error?.message);
1670
1770
  }
1671
1771
  }
1672
- function prepareTemplateData(options) {
1673
- return {
1674
- ...options,
1675
- tmpVariables: {
1676
- key: "{{key}}",
1677
- username: "{{username}}",
1678
- processRegex: "{{(.*?)}}"
1679
- }
1680
- };
1681
- }
1682
- function renderInitTagTemplates(options) {
1683
- const baseTemplateName = options.isLibrary ? "base-library" : "base-app";
1684
- const baseTemplate = loadTemplate(baseTemplateName);
1685
- const placeholderTemplate = loadTemplate("placeholder");
1686
- const templateData = prepareTemplateData(options);
1687
- const renderedBase = renderTemplate(baseTemplate, templateData);
1688
- const renderedPlaceholders = renderTemplate(placeholderTemplate, templateData);
1689
- return renderedBase + "\n\n" + renderedPlaceholders;
1690
- }
1691
1772
  async function readPackageJson() {
1692
1773
  const packageJsonPath = path.join(process.cwd(), "package.json");
1693
1774
  if (!fs.existsSync(packageJsonPath)) {
@@ -1729,51 +1810,312 @@ async function detectInitTagOptions(options, config) {
1729
1810
  packageVersion: packageJson?.version || "1.0.0"
1730
1811
  };
1731
1812
  }
1813
+ function renderTemplate(template2, data) {
1814
+ return mustache.render(template2, data, {}, { escape: (text) => text });
1815
+ }
1816
+ function loadTemplate(templateName) {
1817
+ const __filename2 = url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename2).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href);
1818
+ const __dirname = path.dirname(__filename2);
1819
+ const templatePath2 = path.join(
1820
+ __dirname,
1821
+ "templates",
1822
+ "tag",
1823
+ `${templateName}.mustache`
1824
+ );
1825
+ try {
1826
+ return fs.readFileSync(templatePath2, "utf-8");
1827
+ } catch (error) {
1828
+ throw new Error(`Failed to load template ${templateName}: ${error}`);
1829
+ }
1830
+ }
1831
+ function prepareTemplateData(options) {
1832
+ return {
1833
+ ...options,
1834
+ tmpVariables: {
1835
+ key: "{{key}}",
1836
+ username: "{{username}}",
1837
+ processRegex: "{{(.*?)}}"
1838
+ }
1839
+ };
1840
+ }
1841
+ function renderInitTagTemplates(options) {
1842
+ const baseTemplateName = options.isLibrary ? "base-library" : "base-app";
1843
+ const baseTemplate = loadTemplate(baseTemplateName);
1844
+ const placeholderTemplate = loadTemplate("placeholder");
1845
+ const templateData = prepareTemplateData(options);
1846
+ const renderedBase = renderTemplate(baseTemplate, templateData);
1847
+ const renderedPlaceholders = renderTemplate(
1848
+ placeholderTemplate,
1849
+ templateData
1850
+ );
1851
+ return renderedBase + "\n\n" + renderedPlaceholders;
1852
+ }
1732
1853
  async function $LT_CMD_InitTagFile(options = {}) {
1733
1854
  const { config, logger } = await $LT_GetCommandEssentials();
1734
1855
  const renderOptions = await detectInitTagOptions(options, config);
1735
1856
  const outputPath = options.output || `${renderOptions.tagName}.${renderOptions.fileExtension}`;
1736
1857
  logger.info("Initializing lang-tag with the following options:");
1737
1858
  logger.info(" Tag name: {tagName}", { tagName: renderOptions.tagName });
1738
- logger.info(" Library mode: {isLibrary}", { isLibrary: renderOptions.isLibrary ? "Yes" : "No" });
1739
- logger.info(" React: {isReact}", { isReact: renderOptions.isReact ? "Yes" : "No" });
1740
- logger.info(" TypeScript: {isTypeScript}", { isTypeScript: renderOptions.isTypeScript ? "Yes" : "No" });
1859
+ logger.info(" Library mode: {isLibrary}", {
1860
+ isLibrary: renderOptions.isLibrary ? "Yes" : "No"
1861
+ });
1862
+ logger.info(" React: {isReact}", {
1863
+ isReact: renderOptions.isReact ? "Yes" : "No"
1864
+ });
1865
+ logger.info(" TypeScript: {isTypeScript}", {
1866
+ isTypeScript: renderOptions.isTypeScript ? "Yes" : "No"
1867
+ });
1741
1868
  logger.info(" Output path: {outputPath}", { outputPath });
1742
1869
  let renderedContent;
1743
1870
  try {
1744
1871
  renderedContent = renderInitTagTemplates(renderOptions);
1745
1872
  } catch (error) {
1746
- logger.error("Failed to render templates: {error}", { error: error?.message });
1873
+ logger.error("Failed to render templates: {error}", {
1874
+ error: error?.message
1875
+ });
1747
1876
  return;
1748
1877
  }
1749
1878
  if (fs.existsSync(outputPath)) {
1750
1879
  logger.warn("File already exists: {outputPath}", { outputPath });
1751
- logger.info("Use --output to specify a different path or remove the existing file");
1880
+ logger.info(
1881
+ "Use --output to specify a different path or remove the existing file"
1882
+ );
1752
1883
  return;
1753
1884
  }
1754
1885
  try {
1755
- await flexibleImportAlgorithm.$LT_WriteFileWithDirs(outputPath, renderedContent);
1756
- logger.success("Lang-tag file created successfully: {outputPath}", { outputPath });
1886
+ await $LT_WriteFileWithDirs(outputPath, renderedContent);
1887
+ logger.success("Lang-tag file created successfully: {outputPath}", {
1888
+ outputPath
1889
+ });
1757
1890
  logger.info("Next steps:");
1758
- logger.info("1. Import the {tagName} function in your files:", { tagName: renderOptions.tagName });
1891
+ logger.info("1. Import the {tagName} function in your files:", {
1892
+ tagName: renderOptions.tagName
1893
+ });
1759
1894
  logger.info(" import { {tagName} } from './{importPath}';", {
1760
1895
  tagName: renderOptions.tagName,
1761
1896
  importPath: outputPath.replace(/^src\//, "")
1762
1897
  });
1763
- logger.info("2. Create your translation objects and use the tag function");
1898
+ logger.info(
1899
+ "2. Create your translation objects and use the tag function"
1900
+ );
1764
1901
  logger.info('3. Run "lang-tag collect" to extract translations');
1765
1902
  } catch (error) {
1766
- logger.error("Failed to write file: {error}", { error: error?.message });
1903
+ logger.error("Failed to write file: {error}", {
1904
+ error: error?.message
1905
+ });
1906
+ }
1907
+ }
1908
+ function deepFreezeObject(obj) {
1909
+ const propNames = Object.getOwnPropertyNames(obj);
1910
+ for (const name of propNames) {
1911
+ const value = obj[name];
1912
+ if (value && typeof value === "object") {
1913
+ deepFreezeObject(value);
1914
+ }
1915
+ }
1916
+ return Object.freeze(obj);
1917
+ }
1918
+ async function checkAndRegenerateFileLangTags(config, logger, file, path$12) {
1919
+ let libraryImportsDir = config.import.dir;
1920
+ if (!libraryImportsDir.endsWith(path.sep)) libraryImportsDir += path.sep;
1921
+ const fileContent = fs.readFileSync(file, "utf-8");
1922
+ const processor = new $LT_TagProcessor(config);
1923
+ let tags = processor.extractTags(fileContent);
1924
+ tags = $LT_FilterInvalidTags(tags, config, logger);
1925
+ if (!tags.length) {
1926
+ return false;
1927
+ }
1928
+ const replacements = [];
1929
+ let lastUpdatedLine = 0;
1930
+ for (let tag of tags) {
1931
+ let newConfig = void 0;
1932
+ let shouldUpdate = false;
1933
+ const frozenConfig = tag.parameterConfig ? deepFreezeObject(tag.parameterConfig) : tag.parameterConfig;
1934
+ const event = {
1935
+ langTagConfig: config,
1936
+ logger,
1937
+ config: frozenConfig,
1938
+ absolutePath: file,
1939
+ relativePath: path$12,
1940
+ isImportedLibrary: path$12.startsWith(libraryImportsDir),
1941
+ isSaved: false,
1942
+ savedConfig: void 0,
1943
+ save: (updatedConfig, triggerName) => {
1944
+ if (!updatedConfig && updatedConfig !== null)
1945
+ throw new Error("Wrong config data");
1946
+ newConfig = updatedConfig;
1947
+ shouldUpdate = true;
1948
+ event.isSaved = true;
1949
+ event.savedConfig = updatedConfig;
1950
+ logger.debug(
1951
+ 'Called save for "{path}" with config "{config}" triggered by: ("{trigger}")',
1952
+ {
1953
+ path: path$12,
1954
+ config: JSON.stringify(updatedConfig),
1955
+ trigger: triggerName || "-"
1956
+ }
1957
+ );
1958
+ }
1959
+ };
1960
+ await config.onConfigGeneration(event);
1961
+ if (!shouldUpdate) {
1962
+ continue;
1963
+ }
1964
+ lastUpdatedLine = tag.line;
1965
+ if (!isConfigSame(tag.parameterConfig, newConfig)) {
1966
+ replacements.push({ tag, config: newConfig });
1967
+ }
1968
+ }
1969
+ if (replacements.length) {
1970
+ const newContent = processor.replaceTags(fileContent, replacements);
1971
+ await promises.writeFile(file, newContent, "utf-8");
1972
+ const encodedFile = encodeURI(file);
1973
+ logger.info(
1974
+ 'Lang tag configurations written for file "{path}" (file://{file}:{line})',
1975
+ { path: path$12, file: encodedFile, line: lastUpdatedLine }
1976
+ );
1977
+ return true;
1978
+ }
1979
+ return false;
1980
+ }
1981
+ function isConfigSame(c1, c2) {
1982
+ if (!c1 && !c2) return true;
1983
+ if (c1 && typeof c1 === "object" && c2 && typeof c2 === "object" && JSON5.stringify(c1) === JSON5.stringify(c2))
1984
+ return true;
1985
+ return false;
1986
+ }
1987
+ async function $LT_CMD_RegenerateTags() {
1988
+ const { config, logger } = await $LT_GetCommandEssentials();
1989
+ const files = await globby.globby(config.includes, {
1990
+ cwd: process.cwd(),
1991
+ ignore: config.excludes,
1992
+ absolute: true
1993
+ });
1994
+ const charactersToSkip = process.cwd().length + 1;
1995
+ let dirty = false;
1996
+ for (const file of files) {
1997
+ const path2 = file.substring(charactersToSkip);
1998
+ const localDirty = await checkAndRegenerateFileLangTags(
1999
+ config,
2000
+ logger,
2001
+ file,
2002
+ path2
2003
+ );
2004
+ if (localDirty) {
2005
+ dirty = true;
2006
+ }
1767
2007
  }
2008
+ if (!dirty) {
2009
+ logger.info(
2010
+ "No changes were made based on the current configuration and files"
2011
+ );
2012
+ }
2013
+ }
2014
+ function getBasePath(pattern) {
2015
+ const globStartIndex = pattern.indexOf("*");
2016
+ const braceStartIndex = pattern.indexOf("{");
2017
+ let endIndex = -1;
2018
+ if (globStartIndex !== -1 && braceStartIndex !== -1) {
2019
+ endIndex = Math.min(globStartIndex, braceStartIndex);
2020
+ } else if (globStartIndex !== -1) {
2021
+ endIndex = globStartIndex;
2022
+ } else if (braceStartIndex !== -1) {
2023
+ endIndex = braceStartIndex;
2024
+ }
2025
+ if (endIndex === -1) {
2026
+ const lastSlashIndex = pattern.lastIndexOf("/");
2027
+ return lastSlashIndex !== -1 ? pattern.substring(0, lastSlashIndex) : ".";
2028
+ }
2029
+ const lastSeparatorIndex = pattern.lastIndexOf("/", endIndex);
2030
+ return lastSeparatorIndex === -1 ? "." : pattern.substring(0, lastSeparatorIndex);
2031
+ }
2032
+ function $LT_CreateChokidarWatcher(config) {
2033
+ const cwd = process.cwd();
2034
+ const baseDirsToWatch = [
2035
+ ...new Set(config.includes.map((pattern) => getBasePath(pattern)))
2036
+ ];
2037
+ const finalDirsToWatch = baseDirsToWatch.map(
2038
+ (dir) => dir === "." ? cwd : dir
2039
+ );
2040
+ const ignored = [...config.excludes, "**/.git/**"];
2041
+ return chokidar.watch(finalDirsToWatch, {
2042
+ // Watch base directories
2043
+ cwd,
2044
+ ignored,
2045
+ persistent: true,
2046
+ ignoreInitial: true,
2047
+ awaitWriteFinish: {
2048
+ stabilityThreshold: 300,
2049
+ pollInterval: 100
2050
+ }
2051
+ });
2052
+ }
2053
+ async function $LT_WatchTranslations() {
2054
+ const { config, logger } = await $LT_GetCommandEssentials();
2055
+ await $LT_CMD_Collect();
2056
+ const watcher = $LT_CreateChokidarWatcher(config);
2057
+ logger.info("Starting watch mode for translations...");
2058
+ logger.info("Watching for changes...");
2059
+ logger.info("Press Ctrl+C to stop watching");
2060
+ watcher.on(
2061
+ "change",
2062
+ async (filePath) => await handleFile(config, logger, filePath)
2063
+ ).on(
2064
+ "add",
2065
+ async (filePath) => await handleFile(config, logger, filePath)
2066
+ ).on("error", (error) => {
2067
+ logger.error("Error in file watcher: {error}", { error });
2068
+ });
2069
+ }
2070
+ async function handleFile(config, logger, cwdRelativeFilePath, event) {
2071
+ if (!micromatch.isMatch(cwdRelativeFilePath, config.includes)) {
2072
+ return;
2073
+ }
2074
+ const cwd = process.cwd();
2075
+ const absoluteFilePath = path.join(cwd, cwdRelativeFilePath);
2076
+ await checkAndRegenerateFileLangTags(
2077
+ config,
2078
+ logger,
2079
+ absoluteFilePath,
2080
+ cwdRelativeFilePath
2081
+ );
2082
+ const files = await $LT_CollectCandidateFilesWithTags({
2083
+ filesToScan: [cwdRelativeFilePath],
2084
+ config,
2085
+ logger
2086
+ });
2087
+ const namespaces = await $LT_GroupTagsToCollections({
2088
+ logger,
2089
+ files,
2090
+ config
2091
+ });
2092
+ await $LT_WriteToCollections({ config, collections: namespaces, logger });
1768
2093
  }
1769
2094
  function createCli() {
1770
2095
  commander.program.name("lang-tag").description("CLI to manage language translations").version("0.1.0");
1771
2096
  commander.program.command("collect").alias("c").description("Collect translations from source files").option("-c, --clean", "Remove output directory before collecting").action($LT_CMD_Collect);
1772
2097
  commander.program.command("import").alias("i").description("Import translations from libraries in node_modules").action($LT_ImportTranslations);
1773
2098
  commander.program.command("regenerate-tags").alias("rt").description("Regenerate configuration for language tags").action($LT_CMD_RegenerateTags);
1774
- commander.program.command("watch").alias("w").description("Watch for changes in source files and automatically collect translations").action($LT_WatchTranslations);
2099
+ commander.program.command("watch").alias("w").description(
2100
+ "Watch for changes in source files and automatically collect translations"
2101
+ ).action($LT_WatchTranslations);
1775
2102
  commander.program.command("init").description("Initialize project with default configuration").action($LT_CMD_InitConfig);
1776
- commander.program.command("init-tag").description("Initialize a new lang-tag function file").option("-n, --name <name>", "Name of the tag function (default: from config)").option("-l, --library", "Generate library-style tag (default: from config)").option("-r, --react", "Include React-specific optimizations (default: auto-detect)").option("-t, --typescript", "Force TypeScript output (default: auto-detect)").option("-o, --output <path>", "Output file path (default: auto-generated)").action(async (options) => {
2103
+ commander.program.command("init-tag").description("Initialize a new lang-tag function file").option(
2104
+ "-n, --name <name>",
2105
+ "Name of the tag function (default: from config)"
2106
+ ).option(
2107
+ "-l, --library",
2108
+ "Generate library-style tag (default: from config)"
2109
+ ).option(
2110
+ "-r, --react",
2111
+ "Include React-specific optimizations (default: auto-detect)"
2112
+ ).option(
2113
+ "-t, --typescript",
2114
+ "Force TypeScript output (default: auto-detect)"
2115
+ ).option(
2116
+ "-o, --output <path>",
2117
+ "Output file path (default: auto-generated)"
2118
+ ).action(async (options) => {
1777
2119
  await $LT_CMD_InitTagFile(options);
1778
2120
  });
1779
2121
  return commander.program;