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