@lang-tag/cli 0.16.0 → 0.18.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,77 @@
1
1
  #!/usr/bin/env node
2
2
  import { program } from "commander";
3
- 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";
3
+ import * as process$1 from "node:process";
4
+ import process__default from "node:process";
5
+ import fs, { readFileSync, existsSync, readdirSync, statSync } from "fs";
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 checkbox from "@inquirer/checkbox";
19
+ import confirm from "@inquirer/confirm";
20
+ import input from "@inquirer/input";
21
+ import select from "@inquirer/select";
22
+ import chokidar from "chokidar";
23
+ function $LT_FilterInvalidTags(tags, config, logger) {
24
+ return tags.filter((tag) => {
25
+ if (tag.validity === "invalid-param-1")
26
+ logger.debug(
27
+ 'Skipping tag "{fullMatch}". Invalid JSON: "{invalid}"',
28
+ {
29
+ fullMatch: tag.fullMatch.trim(),
30
+ invalid: tag.parameter1Text
31
+ }
32
+ );
33
+ if (tag.validity === "invalid-param-2")
34
+ logger.debug(
35
+ 'Skipping tag "{fullMatch}". Invalid JSON: "{invalid}"',
36
+ {
37
+ fullMatch: tag.fullMatch.trim(),
38
+ invalid: tag.parameter2Text
39
+ }
40
+ );
41
+ if (tag.validity === "translations-not-found")
42
+ logger.debug(
43
+ 'Skipping tag "{fullMatch}". Translations not found at parameter position: {pos}',
44
+ {
45
+ fullMatch: tag.fullMatch.trim(),
46
+ pos: config.translationArgPosition
47
+ }
48
+ );
49
+ return tag.validity === "ok";
50
+ });
51
+ }
52
+ function $LT_FilterEmptyNamespaceTags(tags, logger) {
53
+ return tags.filter((tag) => {
54
+ if (!tag.parameterConfig) {
55
+ logger.warn(
56
+ 'Skipping tag "{fullMatch}". Tag configuration not defined. (Check lang-tag config at collect.onCollectConfigFix)',
57
+ {
58
+ fullMatch: tag.fullMatch.trim()
59
+ }
60
+ );
61
+ return false;
62
+ }
63
+ if (!tag.parameterConfig.namespace) {
64
+ logger.warn(
65
+ 'Skipping tag "{fullMatch}". Tag configuration namespace not defined. (Check lang-tag config at collect.onCollectConfigFix)',
66
+ {
67
+ fullMatch: tag.fullMatch.trim()
68
+ }
69
+ );
70
+ return false;
71
+ }
72
+ return true;
73
+ });
74
+ }
19
75
  class $LT_TagProcessor {
20
76
  constructor(config) {
21
77
  this.config = config;
@@ -26,7 +82,10 @@ class $LT_TagProcessor {
26
82
  const matches = [];
27
83
  let currentIndex = 0;
28
84
  const skipRanges = this.buildSkipRanges(fileContent);
29
- const startPattern = new RegExp(`${optionalVariableAssignment}${tagName}\\(\\s*\\{`, "g");
85
+ const startPattern = new RegExp(
86
+ `${optionalVariableAssignment}${tagName}\\(\\s*\\{`,
87
+ "g"
88
+ );
30
89
  while (true) {
31
90
  startPattern.lastIndex = currentIndex;
32
91
  const startMatch = startPattern.exec(fileContent);
@@ -48,7 +107,10 @@ class $LT_TagProcessor {
48
107
  currentIndex = matchStartIndex + 1;
49
108
  continue;
50
109
  }
51
- let parameter1Text = fileContent.substring(matchStartIndex + startMatch[0].length - 1, i);
110
+ let parameter1Text = fileContent.substring(
111
+ matchStartIndex + startMatch[0].length - 1,
112
+ i
113
+ );
52
114
  let parameter2Text;
53
115
  while (i < fileContent.length && (fileContent[i] === " " || fileContent[i] === "\n" || fileContent[i] === " ")) {
54
116
  i++;
@@ -100,7 +162,10 @@ class $LT_TagProcessor {
100
162
  }
101
163
  i++;
102
164
  const fullMatch = fileContent.substring(matchStartIndex, i);
103
- const { line, column } = getLineAndColumn(fileContent, matchStartIndex);
165
+ const { line, column } = getLineAndColumn(
166
+ fileContent,
167
+ matchStartIndex
168
+ );
104
169
  let validity = "ok";
105
170
  let parameter1 = void 0;
106
171
  let parameter2 = void 0;
@@ -115,7 +180,9 @@ class $LT_TagProcessor {
115
180
  parameter2 = JSON5.parse(parameter2Text);
116
181
  } catch (error) {
117
182
  try {
118
- parameter2 = JSON5.parse(this.escapeNewlinesInStrings(parameter2Text));
183
+ parameter2 = JSON5.parse(
184
+ this.escapeNewlinesInStrings(parameter2Text)
185
+ );
119
186
  } catch {
120
187
  validity = "invalid-param-2";
121
188
  }
@@ -123,7 +190,9 @@ class $LT_TagProcessor {
123
190
  }
124
191
  } catch (error) {
125
192
  try {
126
- parameter1 = JSON5.parse(this.escapeNewlinesInStrings(parameter1Text));
193
+ parameter1 = JSON5.parse(
194
+ this.escapeNewlinesInStrings(parameter1Text)
195
+ );
127
196
  } catch {
128
197
  validity = "invalid-param-1";
129
198
  }
@@ -157,22 +226,31 @@ class $LT_TagProcessor {
157
226
  }
158
227
  const tag = R.tag;
159
228
  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!");
229
+ if (!newTranslationsString)
230
+ newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text || "{}";
231
+ else if (typeof newTranslationsString !== "string")
232
+ newTranslationsString = JSON5.stringify(newTranslationsString);
233
+ if (!newTranslationsString)
234
+ throw new Error("Tag must have translations provided!");
163
235
  try {
164
236
  JSON5.parse(newTranslationsString);
165
237
  } catch (error) {
166
- throw new Error(`Tag translations are invalid object! Translations: ${newTranslationsString}`);
238
+ throw new Error(
239
+ `Tag translations are invalid object! Translations: ${newTranslationsString}`
240
+ );
167
241
  }
168
242
  let newConfigString = R.config;
169
- if (!newConfigString && newConfigString !== null) newConfigString = tag.parameterConfig;
243
+ if (!newConfigString && newConfigString !== null)
244
+ newConfigString = tag.parameterConfig;
170
245
  if (newConfigString) {
171
246
  try {
172
- if (typeof newConfigString === "string") JSON5.parse(newConfigString);
247
+ if (typeof newConfigString === "string")
248
+ JSON5.parse(newConfigString);
173
249
  else newConfigString = JSON5.stringify(newConfigString);
174
250
  } catch (error) {
175
- throw new Error(`Tag config is invalid object! Config: ${newConfigString}`);
251
+ throw new Error(
252
+ `Tag config is invalid object! Config: ${newConfigString}`
253
+ );
176
254
  }
177
255
  }
178
256
  if (newConfigString === null && this.config.translationArgPosition === 2) {
@@ -183,7 +261,8 @@ class $LT_TagProcessor {
183
261
  let tagFunction = `${this.config.tagName}(${arg1}`;
184
262
  if (arg2) tagFunction += `, ${arg2}`;
185
263
  tagFunction += ")";
186
- if (tag.variableName) replaceMap.set(tag, ` ${tag.variableName} = ${tagFunction}`);
264
+ if (tag.variableName)
265
+ replaceMap.set(tag, ` ${tag.variableName} = ${tagFunction}`);
187
266
  else replaceMap.set(tag, tagFunction);
188
267
  });
189
268
  let offset = 0;
@@ -376,127 +455,397 @@ function getLineAndColumn(text, matchIndex) {
376
455
  const column = lines[lines.length - 1].length + 1;
377
456
  return { line, column };
378
457
  }
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;
458
+ async function $LT_CollectCandidateFilesWithTags(props) {
459
+ const { config, logger } = props;
460
+ const processor = new $LT_TagProcessor(config);
461
+ const cwd = process__default.cwd();
462
+ let filesToScan = props.filesToScan;
463
+ if (!filesToScan) {
464
+ filesToScan = await globby(config.includes, {
465
+ cwd,
466
+ ignore: config.excludes,
467
+ absolute: true
468
+ });
469
+ }
470
+ const candidates = [];
471
+ for (const filePath of filesToScan) {
472
+ const fileContent = readFileSync(filePath, "utf-8");
473
+ let tags = processor.extractTags(fileContent);
474
+ if (!tags.length) {
475
+ continue;
406
476
  }
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;
477
+ tags = $LT_FilterInvalidTags(tags, config, logger);
478
+ if (!tags.length) {
479
+ continue;
412
480
  }
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);
481
+ for (let tag of tags) {
482
+ tag.parameterConfig = config.collect.onCollectConfigFix({
483
+ config: tag.parameterConfig,
484
+ langTagConfig: config
485
+ });
422
486
  }
487
+ tags = $LT_FilterEmptyNamespaceTags(tags, logger);
488
+ const relativeFilePath = path__default.relative(cwd, filePath);
489
+ candidates.push({ relativeFilePath, tags });
423
490
  }
424
- return Object.freeze(obj);
491
+ return candidates;
425
492
  }
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;
493
+ async function $LT_GroupTagsToCollections({
494
+ logger,
495
+ files,
496
+ config
497
+ }) {
498
+ let totalTags = 0;
499
+ const collections = {};
500
+ function getTranslationsCollection(namespace) {
501
+ const collectionName = config.collect.collector.aggregateCollection(namespace);
502
+ const collection = collections[collectionName] || {};
503
+ if (!(collectionName in collections)) {
504
+ collections[collectionName] = collection;
505
+ }
506
+ return collection;
435
507
  }
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 || "-" });
508
+ const allConflicts = [];
509
+ const existingValuesByNamespace = /* @__PURE__ */ new Map();
510
+ for (const file of files) {
511
+ totalTags += file.tags.length;
512
+ for (const _tag of file.tags) {
513
+ const tag = config.collect.collector.transformTag(_tag);
514
+ const tagConfig = tag.parameterConfig;
515
+ const collection = getTranslationsCollection(tagConfig.namespace);
516
+ let existingValues = existingValuesByNamespace.get(
517
+ tagConfig.namespace
518
+ );
519
+ if (!existingValues) {
520
+ existingValues = /* @__PURE__ */ new Map();
521
+ existingValuesByNamespace.set(
522
+ tagConfig.namespace,
523
+ existingValues
524
+ );
457
525
  }
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 });
526
+ const valueTracker = {
527
+ get: (path2) => existingValues.get(path2),
528
+ trackValue: (path2, value) => {
529
+ existingValues.set(path2, {
530
+ tag,
531
+ relativeFilePath: file.relativeFilePath,
532
+ value
533
+ });
534
+ }
535
+ };
536
+ const addConflict = async (path2, tagA, tagBValue, conflictType) => {
537
+ if (conflictType === "path_overwrite" && config.collect?.ignoreConflictsWithMatchingValues !== false && tagA.value === tagBValue) {
538
+ return;
539
+ }
540
+ const conflict = {
541
+ path: path2,
542
+ tagA,
543
+ tagB: {
544
+ tag,
545
+ relativeFilePath: file.relativeFilePath,
546
+ value: tagBValue
547
+ },
548
+ conflictType
549
+ };
550
+ if (config.collect?.onConflictResolution) {
551
+ let shouldContinue = true;
552
+ await config.collect.onConflictResolution({
553
+ conflict,
554
+ logger,
555
+ exit() {
556
+ shouldContinue = false;
557
+ }
558
+ });
559
+ if (!shouldContinue) {
560
+ throw new Error(
561
+ `LangTagConflictResolution:Processing stopped due to conflict resolution: ${conflict.tagA.tag.parameterConfig.namespace}|${conflict.path}`
562
+ );
563
+ }
564
+ }
565
+ allConflicts.push(conflict);
566
+ };
567
+ const target = await ensureNestedObject(
568
+ tagConfig.path,
569
+ collection,
570
+ valueTracker,
571
+ addConflict
572
+ );
573
+ await mergeWithConflictDetection(
574
+ target,
575
+ tag.parameterTranslations,
576
+ tagConfig.path || "",
577
+ valueTracker,
578
+ addConflict
579
+ );
466
580
  }
467
581
  }
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;
582
+ if (allConflicts.length > 0) {
583
+ logger.warn(`Found ${allConflicts.length} conflicts.`);
474
584
  }
475
- return false;
585
+ if (config.collect?.onCollectFinish) {
586
+ let shouldContinue = true;
587
+ config.collect.onCollectFinish({
588
+ totalTags,
589
+ namespaces: collections,
590
+ conflicts: allConflicts,
591
+ logger,
592
+ exit() {
593
+ shouldContinue = false;
594
+ }
595
+ });
596
+ if (!shouldContinue) {
597
+ throw new Error(
598
+ `LangTagConflictResolution:Processing stopped due to collect finish handler`
599
+ );
600
+ }
601
+ }
602
+ return collections;
476
603
  }
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;
604
+ async function ensureNestedObject(path2, rootCollection, valueTracker, addConflict) {
605
+ if (!path2 || !path2.trim()) return rootCollection;
606
+ let current = rootCollection;
607
+ let currentPath = "";
608
+ for (const key of path2.split(".")) {
609
+ currentPath = currentPath ? `${currentPath}.${key}` : key;
610
+ if (current[key] !== void 0 && typeof current[key] !== "object") {
611
+ const existingInfo = valueTracker.get(currentPath);
612
+ if (existingInfo) {
613
+ await addConflict(
614
+ currentPath,
615
+ existingInfo,
616
+ {},
617
+ "type_mismatch"
618
+ );
619
+ }
620
+ return current;
621
+ }
622
+ current[key] = current[key] || {};
623
+ current = current[key];
624
+ }
625
+ return current;
481
626
  }
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",
627
+ async function mergeWithConflictDetection(target, source, basePath = "", valueTracker, addConflict) {
628
+ if (typeof target !== "object" || typeof source !== "object") {
629
+ return;
630
+ }
631
+ for (const key in source) {
632
+ if (!source.hasOwnProperty(key)) {
633
+ continue;
634
+ }
635
+ const currentPath = basePath ? `${basePath}.${key}` : key;
636
+ let targetValue = target[key];
637
+ const sourceValue = source[key];
638
+ if (Array.isArray(sourceValue)) {
639
+ continue;
640
+ }
641
+ if (targetValue !== void 0) {
642
+ const targetType = typeof targetValue;
643
+ const sourceType = typeof sourceValue;
644
+ let existingInfo = valueTracker.get(currentPath);
645
+ if (!existingInfo && targetType === "object" && targetValue !== null && !Array.isArray(targetValue)) {
646
+ const findNestedInfo = (obj, prefix) => {
647
+ for (const key2 in obj) {
648
+ const path2 = prefix ? `${prefix}.${key2}` : key2;
649
+ const info = valueTracker.get(path2);
650
+ if (info) {
651
+ return info;
652
+ }
653
+ if (typeof obj[key2] === "object" && obj[key2] !== null && !Array.isArray(obj[key2])) {
654
+ const nestedInfo = findNestedInfo(obj[key2], path2);
655
+ if (nestedInfo) {
656
+ return nestedInfo;
657
+ }
658
+ }
659
+ }
660
+ return void 0;
661
+ };
662
+ existingInfo = findNestedInfo(targetValue, currentPath);
663
+ }
664
+ if (targetType !== sourceType) {
665
+ if (existingInfo) {
666
+ await addConflict(
667
+ currentPath,
668
+ existingInfo,
669
+ sourceValue,
670
+ "type_mismatch"
671
+ );
672
+ }
673
+ continue;
674
+ }
675
+ if (targetType !== "object") {
676
+ if (existingInfo) {
677
+ await addConflict(
678
+ currentPath,
679
+ existingInfo,
680
+ sourceValue,
681
+ "path_overwrite"
682
+ );
683
+ }
684
+ if (targetValue !== sourceValue) {
685
+ continue;
686
+ }
687
+ }
688
+ }
689
+ if (typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue)) {
690
+ if (!targetValue) {
691
+ targetValue = {};
692
+ target[key] = targetValue;
693
+ }
694
+ await mergeWithConflictDetection(
695
+ targetValue,
696
+ sourceValue,
697
+ currentPath,
698
+ valueTracker,
699
+ addConflict
700
+ );
701
+ } else {
702
+ target[key] = sourceValue;
703
+ valueTracker.trackValue(currentPath, sourceValue);
704
+ }
705
+ }
706
+ }
707
+ const CONFIG_FILE_NAME = "lang-tag.config.js";
708
+ const EXPORTS_FILE_NAME = "lang-tags.json";
709
+ async function $LT_EnsureDirectoryExists(filePath) {
710
+ await mkdir(filePath, { recursive: true });
711
+ }
712
+ async function $LT_WriteJSON(filePath, data) {
713
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
714
+ }
715
+ async function $LT_ReadJSON(filePath) {
716
+ const content = await readFile(filePath, "utf-8");
717
+ return JSON.parse(content);
718
+ }
719
+ async function $LT_WriteFileWithDirs(filePath, content) {
720
+ const dir = dirname(filePath);
721
+ try {
722
+ await mkdir(dir, { recursive: true });
723
+ } catch (error) {
724
+ }
725
+ await writeFile(filePath, content, "utf-8");
726
+ }
727
+ async function $LT_ReadFileContent(relativeFilePath) {
728
+ const cwd = process.cwd();
729
+ const absolutePath = resolve(cwd, relativeFilePath);
730
+ return await readFile(absolutePath, "utf-8");
731
+ }
732
+ async function $LT_WriteAsExportFile({
733
+ config,
734
+ logger,
735
+ files
736
+ }) {
737
+ const packageJson = await $LT_ReadJSON(
738
+ path__default.resolve(process.cwd(), "package.json")
739
+ );
740
+ if (!packageJson) {
741
+ throw new Error("package.json not found");
742
+ }
743
+ const exportData = {
744
+ baseLanguageCode: config.baseLanguageCode,
745
+ files: files.map(({ relativeFilePath, tags }) => ({
746
+ relativeFilePath,
747
+ tags: tags.map((tag) => ({
748
+ variableName: tag.variableName,
749
+ config: tag.parameterConfig,
750
+ translations: tag.parameterTranslations
751
+ }))
752
+ }))
753
+ };
754
+ await writeFile(EXPORTS_FILE_NAME, JSON.stringify(exportData), "utf-8");
755
+ logger.success(`Written {file}`, { file: EXPORTS_FILE_NAME });
756
+ }
757
+ function deepMergeTranslations(target, source) {
758
+ if (typeof target !== "object") {
759
+ throw new Error("Target must be an object");
760
+ }
761
+ if (typeof source !== "object") {
762
+ throw new Error("Source must be an object");
763
+ }
764
+ let changed = false;
765
+ for (const key in source) {
766
+ if (!source.hasOwnProperty(key)) {
767
+ continue;
768
+ }
769
+ let targetValue = target[key];
770
+ const sourceValue = source[key];
771
+ if (typeof targetValue === "string" && typeof sourceValue === "object") {
772
+ throw new Error(
773
+ `Trying to write object into target key "${key}" which is translation already`
774
+ );
775
+ }
776
+ if (Array.isArray(sourceValue)) {
777
+ throw new Error(
778
+ `Trying to write array into target key "${key}", we do not allow arrays inside translations`
779
+ );
780
+ }
781
+ if (typeof sourceValue === "object") {
782
+ if (!targetValue) {
783
+ targetValue = {};
784
+ target[key] = targetValue;
785
+ }
786
+ if (deepMergeTranslations(targetValue, sourceValue)) {
787
+ changed = true;
788
+ }
789
+ } else {
790
+ if (target[key] !== sourceValue) {
791
+ changed = true;
792
+ }
793
+ target[key] = sourceValue;
794
+ }
795
+ }
796
+ return changed;
797
+ }
798
+ async function $LT_WriteToCollections({
799
+ config,
800
+ collections,
801
+ logger,
802
+ clean
803
+ }) {
804
+ await config.collect.collector.preWrite(clean);
805
+ const changedCollections = [];
806
+ for (let collectionName of Object.keys(collections)) {
807
+ if (!collectionName) {
808
+ continue;
809
+ }
810
+ const filePath = await config.collect.collector.resolveCollectionFilePath(
811
+ collectionName
812
+ );
813
+ let originalJSON = {};
814
+ try {
815
+ originalJSON = await $LT_ReadJSON(filePath);
816
+ } catch (e) {
817
+ await config.collect.collector.onMissingCollection(
818
+ collectionName
819
+ );
820
+ }
821
+ if (deepMergeTranslations(originalJSON, collections[collectionName])) {
822
+ changedCollections.push(collectionName);
823
+ await $LT_WriteJSON(filePath, originalJSON);
824
+ }
825
+ }
826
+ await config.collect.collector.postWrite(changedCollections);
827
+ }
828
+ const LANG_TAG_DEFAULT_CONFIG = {
829
+ tagName: "lang",
830
+ isLibrary: false,
831
+ includes: ["src/**/*.{js,ts,jsx,tsx}"],
832
+ excludes: ["node_modules", "dist", "build"],
833
+ localesDirectory: "locales",
834
+ baseLanguageCode: "en",
835
+ collect: {
836
+ collector: new NamespaceCollector(),
837
+ defaultNamespace: "common",
494
838
  ignoreConflictsWithMatchingValues: true,
495
839
  onCollectConfigFix: ({ config, langTagConfig }) => {
496
840
  if (langTagConfig.isLibrary) return config;
497
- if (!config) return { path: "", namespace: langTagConfig.collect.defaultNamespace };
841
+ if (!config)
842
+ return {
843
+ path: "",
844
+ namespace: langTagConfig.collect.defaultNamespace
845
+ };
498
846
  if (!config.path) config.path = "";
499
- if (!config.namespace) config.namespace = langTagConfig.collect.defaultNamespace;
847
+ if (!config.namespace)
848
+ config.namespace = langTagConfig.collect.defaultNamespace;
500
849
  return config;
501
850
  },
502
851
  onConflictResolution: async (event) => {
@@ -509,25 +858,44 @@ const LANG_TAG_DEFAULT_CONFIG = {
509
858
  import: {
510
859
  dir: "src/lang-libraries",
511
860
  tagImportPath: 'import { lang } from "@/my-lang-tag-path"',
512
- onImport: flexibleImportAlgorithm({
513
- filePath: {
514
- includePackageInPath: true
861
+ onImport: (event) => {
862
+ for (let e of event.exports) {
863
+ event.logger.info(
864
+ "Detected lang tag exports at package {packageName}",
865
+ { packageName: e.packageJSON.name }
866
+ );
515
867
  }
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
- // }
868
+ event.logger.warn(
869
+ `
870
+ Import Algorithm Not Configured
871
+
872
+ To import external language tags, you need to configure an import algorithm.
873
+
874
+ Setup Instructions:
875
+ 1. Add this import at the top of your configuration file:
876
+ {importStr}
877
+
878
+ 2. Replace import.onImport function with:
879
+ {onImport}
880
+
881
+ This will enable import of language tags from external packages.
882
+ `.trim(),
883
+ {
884
+ importStr: "const { flexibleImportAlgorithm } = require('@lang-tag/cli/algorithms');",
885
+ onImport: "onImport: flexibleImportAlgorithm({ filePath: { includePackageInPath: true } })"
886
+ }
887
+ );
888
+ }
524
889
  },
525
890
  translationArgPosition: 1,
526
891
  onConfigGeneration: async (event) => {
892
+ event.logger.info(
893
+ "Config generation event is not configured. Add onConfigGeneration handler to customize config generation."
894
+ );
527
895
  }
528
896
  };
529
897
  async function $LT_ReadConfig(projectPath) {
530
- const configPath = resolve(projectPath, CONFIG_FILE_NAME);
898
+ const configPath = resolve$1(projectPath, CONFIG_FILE_NAME);
531
899
  if (!existsSync(configPath)) {
532
900
  throw new Error(`No "${CONFIG_FILE_NAME}" detected`);
533
901
  }
@@ -539,7 +907,9 @@ async function $LT_ReadConfig(projectPath) {
539
907
  const userConfig = configModule.default || {};
540
908
  const tn = (userConfig.tagName || "").toLowerCase().replace(/[-_\s]/g, "");
541
909
  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');
910
+ throw new Error(
911
+ 'Custom tagName cannot be "lang-tag" or "langtag"! (It is not recommended for use with libraries)\n'
912
+ );
543
913
  }
544
914
  const config = {
545
915
  ...LANG_TAG_DEFAULT_CONFIG,
@@ -561,6 +931,75 @@ async function $LT_ReadConfig(projectPath) {
561
931
  throw error;
562
932
  }
563
933
  }
934
+ const ANSI$1 = {
935
+ reset: "\x1B[0m",
936
+ white: "\x1B[97m",
937
+ brightCyan: "\x1B[96m",
938
+ green: "\x1B[92m",
939
+ gray: "\x1B[90m",
940
+ redBg: "\x1B[41m\x1B[97m",
941
+ bold: "\x1B[1m"
942
+ };
943
+ function colorizeFromAST(code, nodes) {
944
+ const ranges = [];
945
+ for (const node of nodes) {
946
+ const color = getColorForNodeType(node.type);
947
+ const priority = getPriorityForNodeType(node.type);
948
+ ranges.push({
949
+ start: node.start,
950
+ end: node.end,
951
+ color,
952
+ priority
953
+ });
954
+ }
955
+ ranges.sort((a, b) => {
956
+ if (b.start !== a.start) return b.start - a.start;
957
+ return b.priority - a.priority;
958
+ });
959
+ let colorized = code;
960
+ for (const range of ranges) {
961
+ const before = colorized.substring(0, range.start);
962
+ const text = colorized.substring(range.start, range.end);
963
+ const after = colorized.substring(range.end);
964
+ colorized = before + range.color + text + ANSI$1.reset + after;
965
+ }
966
+ return colorized;
967
+ }
968
+ function getColorForNodeType(type) {
969
+ switch (type) {
970
+ case "key":
971
+ return ANSI$1.brightCyan;
972
+ case "bracket":
973
+ case "colon":
974
+ return ANSI$1.gray;
975
+ case "value":
976
+ return ANSI$1.green;
977
+ case "comment":
978
+ return ANSI$1.gray;
979
+ case "error":
980
+ return ANSI$1.bold + ANSI$1.redBg;
981
+ default:
982
+ return ANSI$1.white;
983
+ }
984
+ }
985
+ function getPriorityForNodeType(type) {
986
+ switch (type) {
987
+ case "error":
988
+ return 3;
989
+ // Highest priority
990
+ case "key":
991
+ return 2;
992
+ case "value":
993
+ return 1;
994
+ case "bracket":
995
+ case "colon":
996
+ case "comment":
997
+ return 0;
998
+ // Lowest priority
999
+ default:
1000
+ return 0;
1001
+ }
1002
+ }
564
1003
  function parseObjectAST(code) {
565
1004
  const nodes = [];
566
1005
  try {
@@ -654,81 +1093,12 @@ function markConflictNodes(nodes, conflictPath) {
654
1093
  return node;
655
1094
  });
656
1095
  }
657
- const ANSI$1 = {
1096
+ const ANSI = {
658
1097
  reset: "\x1B[0m",
659
1098
  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",
1099
+ cyan: "\x1B[96m",
1100
+ gray: "\x1B[37m",
1101
+ darkGray: "\x1B[90m",
732
1102
  bold: "\x1B[1m"
733
1103
  };
734
1104
  function getVisibleLines(totalLines, errorLines, threshold = 10) {
@@ -754,18 +1124,24 @@ function printLines(lines, startLineNumber, errorLines = /* @__PURE__ */ new Set
754
1124
  lines.forEach((line, i) => {
755
1125
  const lineNumber = startLineNumber + i;
756
1126
  const lineNumStr = String(lineNumber).padStart(3, " ");
757
- console.log(`${ANSI.gray}${lineNumStr}${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${line}`);
1127
+ console.log(
1128
+ `${ANSI.gray}${lineNumStr}${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${line}`
1129
+ );
758
1130
  });
759
1131
  } else {
760
1132
  let lastPrinted = -2;
761
1133
  lines.forEach((line, i) => {
762
1134
  if (visibleLines.has(i)) {
763
1135
  if (i > lastPrinted + 1 && lastPrinted >= 0) {
764
- console.log(`${ANSI.gray} -${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${ANSI.gray}...${ANSI.reset}`);
1136
+ console.log(
1137
+ `${ANSI.gray} -${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${ANSI.gray}...${ANSI.reset}`
1138
+ );
765
1139
  }
766
1140
  const lineNumber = startLineNumber + i;
767
1141
  const lineNumStr = String(lineNumber).padStart(3, " ");
768
- console.log(`${ANSI.gray}${lineNumStr}${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${line}`);
1142
+ console.log(
1143
+ `${ANSI.gray}${lineNumStr}${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${line}`
1144
+ );
769
1145
  lastPrinted = i;
770
1146
  }
771
1147
  });
@@ -815,14 +1191,20 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
815
1191
  const wholeTagCode = await getLangTagCodeSection(tagInfo);
816
1192
  const translationTagCode = translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text;
817
1193
  const configTagCode = translationArgPosition === 1 ? tag.parameter2Text : tag.parameter1Text;
818
- const translationErrorPath = stripPrefix(conflictPath, tag.parameterConfig?.path);
1194
+ const translationErrorPath = stripPrefix(
1195
+ conflictPath,
1196
+ tag.parameterConfig?.path
1197
+ );
819
1198
  let colorizedWhole = wholeTagCode;
820
1199
  let errorLines = /* @__PURE__ */ new Set();
821
1200
  if (translationTagCode) {
822
1201
  try {
823
1202
  const translationNodes = parseObjectAST(translationTagCode);
824
1203
  const markedTranslationNodes = translationErrorPath ? markConflictNodes(translationNodes, translationErrorPath) : translationNodes;
825
- const translationErrorLines = getErrorLineNumbers(translationTagCode, markedTranslationNodes);
1204
+ const translationErrorLines = getErrorLineNumbers(
1205
+ translationTagCode,
1206
+ markedTranslationNodes
1207
+ );
826
1208
  const translationStartInWhole = wholeTagCode.indexOf(translationTagCode);
827
1209
  if (translationStartInWhole >= 0) {
828
1210
  const linesBeforeTranslation = wholeTagCode.substring(0, translationStartInWhole).split("\n").length - 1;
@@ -830,12 +1212,20 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
830
1212
  errorLines.add(linesBeforeTranslation + lineNum2);
831
1213
  });
832
1214
  if (translationErrorLines.size > 0) {
833
- const lastTranslationErrorLine = Math.max(...Array.from(translationErrorLines));
1215
+ const lastTranslationErrorLine = Math.max(
1216
+ ...Array.from(translationErrorLines)
1217
+ );
834
1218
  lineNum = startLine + linesBeforeTranslation + lastTranslationErrorLine;
835
1219
  }
836
1220
  }
837
- const colorizedTranslation = colorizeFromAST(translationTagCode, markedTranslationNodes);
838
- colorizedWhole = colorizedWhole.replace(translationTagCode, colorizedTranslation);
1221
+ const colorizedTranslation = colorizeFromAST(
1222
+ translationTagCode,
1223
+ markedTranslationNodes
1224
+ );
1225
+ colorizedWhole = colorizedWhole.replace(
1226
+ translationTagCode,
1227
+ colorizedTranslation
1228
+ );
839
1229
  } catch (error) {
840
1230
  console.error("Failed to colorize translation:", error);
841
1231
  }
@@ -857,7 +1247,10 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
857
1247
  }
858
1248
  return node;
859
1249
  });
860
- const configErrorLines = getErrorLineNumbers(configTagCode, markedConfigNodes);
1250
+ const configErrorLines = getErrorLineNumbers(
1251
+ configTagCode,
1252
+ markedConfigNodes
1253
+ );
861
1254
  const configStartInWhole = wholeTagCode.indexOf(configTagCode);
862
1255
  if (configStartInWhole >= 0) {
863
1256
  const linesBeforeConfig = wholeTagCode.substring(0, configStartInWhole).split("\n").length - 1;
@@ -865,14 +1258,22 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
865
1258
  errorLines.add(linesBeforeConfig + lineNum2);
866
1259
  });
867
1260
  }
868
- const colorizedConfig = colorizeFromAST(configTagCode, markedConfigNodes);
869
- colorizedWhole = colorizedWhole.replace(configTagCode, colorizedConfig);
1261
+ const colorizedConfig = colorizeFromAST(
1262
+ configTagCode,
1263
+ markedConfigNodes
1264
+ );
1265
+ colorizedWhole = colorizedWhole.replace(
1266
+ configTagCode,
1267
+ colorizedConfig
1268
+ );
870
1269
  } catch (error) {
871
1270
  console.error("Failed to colorize config:", error);
872
1271
  }
873
1272
  }
874
1273
  const encodedPath = encodeURI(filePath);
875
- console.log(`${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${encodedPath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`);
1274
+ console.log(
1275
+ `${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${encodedPath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`
1276
+ );
876
1277
  printLines(colorizedWhole.split("\n"), startLine, errorLines, condense);
877
1278
  } catch (error) {
878
1279
  console.error("Error displaying conflict:", error);
@@ -880,8 +1281,20 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
880
1281
  }
881
1282
  async function $LT_LogConflict(conflict, translationArgPosition, condense) {
882
1283
  const { path: conflictPath, tagA, tagB } = conflict;
883
- await logTagConflictInfo(tagA, "between", conflictPath, translationArgPosition, condense);
884
- await logTagConflictInfo(tagB, "and", conflictPath, translationArgPosition, condense);
1284
+ await logTagConflictInfo(
1285
+ tagA,
1286
+ "between",
1287
+ conflictPath,
1288
+ translationArgPosition,
1289
+ condense
1290
+ );
1291
+ await logTagConflictInfo(
1292
+ tagB,
1293
+ "and",
1294
+ conflictPath,
1295
+ translationArgPosition,
1296
+ condense
1297
+ );
885
1298
  }
886
1299
  const ANSI_COLORS = {
887
1300
  reset: "\x1B[0m",
@@ -895,14 +1308,18 @@ const ANSI_COLORS = {
895
1308
  cyan: "\x1B[36m"
896
1309
  };
897
1310
  function validateAndInterpolate(message, params) {
898
- const placeholders = Array.from(message.matchAll(/\{(\w+)\}/g)).map((m) => m[1]);
1311
+ const placeholders = Array.from(message.matchAll(/\{(\w+)\}/g)).map(
1312
+ (m) => m[1]
1313
+ );
899
1314
  const missing = placeholders.filter((p) => !(p in (params || {})));
900
1315
  if (missing.length) {
901
1316
  throw new Error(`Missing variables in message: ${missing.join(", ")}`);
902
1317
  }
903
1318
  const extra = params ? Object.keys(params).filter((k) => !placeholders.includes(k)) : [];
904
1319
  if (extra.length) {
905
- throw new Error(`Extra variables provided not used in message: ${extra.join(", ")}`);
1320
+ throw new Error(
1321
+ `Extra variables provided not used in message: ${extra.join(", ")}`
1322
+ );
906
1323
  }
907
1324
  const parts = [];
908
1325
  let lastIndex = 0;
@@ -948,12 +1365,24 @@ function $LT_CreateDefaultLogger(debugMode, translationArgPosition = 1) {
948
1365
  conflict: async (conflict, condense) => {
949
1366
  const { path: path2, conflictType, tagA } = conflict;
950
1367
  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}`);
1368
+ console.log(
1369
+ `${ANSI_COLORS.bold}${ANSI_COLORS.red}⚠ Translation Conflict Detected${ANSI_COLORS.reset}`
1370
+ );
1371
+ console.log(
1372
+ `${ANSI_COLORS.gray}${"─".repeat(60)}${ANSI_COLORS.reset}`
1373
+ );
1374
+ console.log(
1375
+ ` ${ANSI_COLORS.cyan}Conflict Type:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${conflictType}${ANSI_COLORS.reset}`
1376
+ );
1377
+ console.log(
1378
+ ` ${ANSI_COLORS.cyan}Translation Key:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${path2}${ANSI_COLORS.reset}`
1379
+ );
1380
+ console.log(
1381
+ ` ${ANSI_COLORS.cyan}Namespace:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${tagA.tag.parameterConfig.namespace}${ANSI_COLORS.reset}`
1382
+ );
1383
+ console.log(
1384
+ `${ANSI_COLORS.gray}${"─".repeat(60)}${ANSI_COLORS.reset}`
1385
+ );
957
1386
  await $LT_LogConflict(conflict, translationArgPosition, condense);
958
1387
  console.log();
959
1388
  }
@@ -961,7 +1390,10 @@ function $LT_CreateDefaultLogger(debugMode, translationArgPosition = 1) {
961
1390
  }
962
1391
  async function $LT_GetCommandEssentials() {
963
1392
  const config = await $LT_ReadConfig(process__default.cwd());
964
- const logger = $LT_CreateDefaultLogger(config.debug, config.translationArgPosition);
1393
+ const logger = $LT_CreateDefaultLogger(
1394
+ config.debug,
1395
+ config.translationArgPosition
1396
+ );
965
1397
  config.collect.collector.config = config;
966
1398
  config.collect.collector.logger = logger;
967
1399
  return {
@@ -969,573 +1401,141 @@ async function $LT_GetCommandEssentials() {
969
1401
  logger
970
1402
  };
971
1403
  }
972
- async function $LT_CMD_RegenerateTags() {
1404
+ async function $LT_CMD_Collect(options) {
973
1405
  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;
1406
+ logger.info("Collecting translations from source files...");
1407
+ const files = await $LT_CollectCandidateFilesWithTags({ config, logger });
1408
+ if (config.debug) {
1409
+ for (let file of files) {
1410
+ logger.debug("Found {count} translations tags inside: {file}", {
1411
+ count: file.tags.length,
1412
+ file: file.relativeFilePath
1413
+ });
986
1414
  }
987
1415
  }
988
- if (!dirty) {
989
- logger.info("No changes were made based on the current configuration and files");
1416
+ if (config.isLibrary) {
1417
+ await $LT_WriteAsExportFile({ config, logger, files });
1418
+ return;
990
1419
  }
991
- }
992
- function deepMergeTranslations(target, source) {
993
- if (typeof target !== "object") {
994
- throw new Error("Target must be an object");
1420
+ try {
1421
+ const collections = await $LT_GroupTagsToCollections({
1422
+ logger,
1423
+ files,
1424
+ config
1425
+ });
1426
+ const totalTags = files.reduce(
1427
+ (sum, file) => sum + file.tags.length,
1428
+ 0
1429
+ );
1430
+ logger.debug("Found {totalTags} translation tags", { totalTags });
1431
+ await $LT_WriteToCollections({
1432
+ config,
1433
+ collections,
1434
+ logger,
1435
+ clean: options?.clean
1436
+ });
1437
+ } catch (e) {
1438
+ const prefix = "LangTagConflictResolution:";
1439
+ if (e.message.startsWith(prefix)) {
1440
+ logger.error(e.message.substring(prefix.length));
1441
+ return;
1442
+ }
1443
+ throw e;
995
1444
  }
996
- if (typeof source !== "object") {
997
- throw new Error("Source must be an object");
1445
+ }
1446
+ async function $LT_CollectExportFiles(logger) {
1447
+ const nodeModulesPath = path$1.join(process__default.cwd(), "node_modules");
1448
+ if (!fs.existsSync(nodeModulesPath)) {
1449
+ logger.error('"node_modules" directory not found');
1450
+ return [];
998
1451
  }
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;
1452
+ const results = [];
1453
+ try {
1454
+ const exportFiles = await globby(
1455
+ [`node_modules/**/${EXPORTS_FILE_NAME}`],
1456
+ {
1457
+ cwd: process__default.cwd(),
1458
+ onlyFiles: true,
1459
+ ignore: ["**/node_modules/**/node_modules/**"],
1460
+ deep: 4
1019
1461
  }
1020
- } else {
1021
- if (target[key] !== sourceValue) {
1022
- changed = true;
1462
+ );
1463
+ for (const exportFile of exportFiles) {
1464
+ const fullExportPath = path$1.resolve(exportFile);
1465
+ const packageJsonPath = findPackageJsonForExport(
1466
+ fullExportPath,
1467
+ nodeModulesPath
1468
+ );
1469
+ if (packageJsonPath) {
1470
+ results.push({
1471
+ exportPath: fullExportPath,
1472
+ packageJsonPath
1473
+ });
1474
+ } else {
1475
+ logger.warn(
1476
+ "Found export file but could not match package.json: {exportPath}",
1477
+ {
1478
+ exportPath: fullExportPath
1479
+ }
1480
+ );
1023
1481
  }
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
1482
  }
1483
+ logger.debug(
1484
+ "Found {count} export files with matching package.json in node_modules",
1485
+ {
1486
+ count: results.length
1487
+ }
1488
+ );
1489
+ } catch (error) {
1490
+ logger.error("Error scanning node_modules with globby: {error}", {
1491
+ error: String(error)
1492
+ });
1047
1493
  }
1048
- await config.collect.collector.postWrite(changedCollections);
1494
+ return results;
1049
1495
  }
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 });
1496
+ function findPackageJsonForExport(exportPath, nodeModulesPath) {
1497
+ const relativePath = path$1.relative(nodeModulesPath, exportPath);
1498
+ const pathParts = relativePath.split(path$1.sep);
1499
+ if (pathParts.length < 2) {
1500
+ return null;
1057
1501
  }
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;
1502
+ if (pathParts[0].startsWith("@")) {
1503
+ if (pathParts.length >= 3) {
1504
+ const packageDir = path$1.join(
1505
+ nodeModulesPath,
1506
+ pathParts[0],
1507
+ pathParts[1]
1508
+ );
1509
+ const packageJsonPath = path$1.join(packageDir, "package.json");
1510
+ if (fs.existsSync(packageJsonPath)) {
1511
+ return packageJsonPath;
1512
+ }
1068
1513
  }
1069
- for (let tag of tags) {
1070
- tag.parameterConfig = config.collect.onCollectConfigFix({ config: tag.parameterConfig, langTagConfig: config });
1514
+ } else {
1515
+ const packageDir = path$1.join(nodeModulesPath, pathParts[0]);
1516
+ const packageJsonPath = path$1.join(packageDir, "package.json");
1517
+ if (fs.existsSync(packageJsonPath)) {
1518
+ return packageJsonPath;
1071
1519
  }
1072
- tags = $LT_FilterEmptyNamespaceTags(tags, logger);
1073
- const relativeFilePath = path__default.relative(cwd, filePath);
1074
- candidates.push({ relativeFilePath, tags });
1075
1520
  }
1076
- return candidates;
1521
+ return null;
1077
1522
  }
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 });
1096
- }
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
- }
1153
- }
1154
- allConflicts.push(conflict);
1155
- };
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
- });
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];
1206
- }
1207
- return current;
1208
- }
1209
- async function mergeWithConflictDetection(target, source, basePath = "", valueTracker, addConflict) {
1210
- if (typeof target !== "object" || typeof source !== "object") {
1211
- return;
1212
- }
1213
- for (const key in source) {
1214
- if (!source.hasOwnProperty(key)) {
1215
- continue;
1216
- }
1217
- const currentPath = basePath ? `${basePath}.${key}` : key;
1218
- let targetValue = target[key];
1219
- const sourceValue = source[key];
1220
- if (Array.isArray(sourceValue)) {
1221
- continue;
1222
- }
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
- }
1260
- }
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
1272
- );
1273
- } else {
1274
- target[key] = sourceValue;
1275
- valueTracker.trackValue(currentPath, sourceValue);
1276
- }
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 });
1286
- }
1287
- }
1288
- if (config.isLibrary) {
1289
- await $LT_WriteAsExportFile({ config, logger, files });
1290
- return;
1291
- }
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;
1304
- }
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;
1316
- }
1317
- if (endIndex === -1) {
1318
- const lastSlashIndex = pattern.lastIndexOf("/");
1319
- return lastSlashIndex !== -1 ? pattern.substring(0, lastSlashIndex) : ".";
1320
- }
1321
- const lastSeparatorIndex = pattern.lastIndexOf("/", endIndex);
1322
- return lastSeparatorIndex === -1 ? "." : pattern.substring(0, lastSeparatorIndex);
1323
- }
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 });
1352
- });
1353
- }
1354
- async function handleFile(config, logger, cwdRelativeFilePath, event) {
1355
- if (!micromatch.isMatch(cwdRelativeFilePath, config.includes)) {
1356
- return;
1357
- }
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 });
1364
- }
1365
- async function detectModuleSystem() {
1366
- const packageJsonPath = join(process.cwd(), "package.json");
1367
- if (!existsSync(packageJsonPath)) {
1368
- return "cjs";
1369
- }
1370
- try {
1371
- const content = await readFile(packageJsonPath, "utf-8");
1372
- const packageJson = JSON.parse(content);
1373
- if (packageJson.type === "module") {
1374
- return "esm";
1375
- }
1376
- return "cjs";
1377
- } catch (error) {
1378
- return "cjs";
1379
- }
1380
- }
1381
- async function generateDefaultConfig() {
1382
- const moduleSystem = await detectModuleSystem();
1383
- const importStatement = moduleSystem === "esm" ? `import { pathBasedConfigGenerator, configKeeper } from '@lang-tag/cli/algorithms';` : `const { pathBasedConfigGenerator, configKeeper } = require('@lang-tag/cli/algorithms');`;
1384
- const exportStatement = moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
1385
- return `${importStatement}
1386
-
1387
- const generationAlgorithm = pathBasedConfigGenerator({
1388
- ignoreIncludesRootDirectories: true,
1389
- removeBracketedDirectories: true,
1390
- namespaceCase: 'kebab',
1391
- pathCase: 'camel',
1392
- clearOnDefaultNamespace: true,
1393
- ignoreDirectories: ['core', 'utils', 'helpers'],
1394
- // Advanced: Use pathRules for hierarchical transformations with ignore and rename
1395
- // pathRules: {
1396
- // app: {
1397
- // dashboard: {
1398
- // _: false, // ignore "dashboard" but continue with nested rules
1399
- // modules: false // also ignore "modules"
1400
- // },
1401
- // admin: {
1402
- // '>': 'management', // rename "admin" to "management"
1403
- // users: false // ignore "users",
1404
- // ui: {
1405
- // '>>': { // 'redirect' - ignore everything, jump to 'ui' namespace and prefix all paths with 'admin'
1406
- // namespace: 'ui',
1407
- // pathPrefix: 'admin'
1408
- // }
1409
- // }
1410
- // }
1411
- // }
1412
- // }
1413
- });
1414
- const keeper = configKeeper({ propertyName: 'keep' });
1415
-
1416
- /** @type {import('@lang-tag/cli/config').LangTagCLIConfig} */
1417
- const config = {
1418
- tagName: 'lang',
1419
- isLibrary: false,
1420
- includes: ['src/**/*.{js,ts,jsx,tsx}'],
1421
- excludes: ['node_modules', 'dist', 'build', '**/*.test.ts'],
1422
- localesDirectory: 'public/locales',
1423
- baseLanguageCode: 'en',
1424
- onConfigGeneration: async event => {
1425
- // We do not modify imported configurations
1426
- if (event.isImportedLibrary) return;
1427
-
1428
- if (event.config?.keep === 'both') return;
1429
-
1430
- await generationAlgorithm(event);
1431
-
1432
- await keeper(event);
1433
- },
1434
- collect: {
1435
- defaultNamespace: 'common',
1436
- onConflictResolution: async event => {
1437
- 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 });
1523
+ const __filename = fileURLToPath(import.meta.url);
1524
+ const __dirname = dirname(__filename);
1525
+ const templatePath = join(
1526
+ __dirname,
1527
+ "templates",
1528
+ "import",
1529
+ "imported-tag.mustache"
1530
+ );
1531
+ const template = readFileSync(templatePath, "utf-8");
1532
+ function renderTemplate$2(data) {
1533
+ return mustache.render(template, data, {}, { escape: (text) => text });
1534
1534
  }
1535
1535
  async function generateImportFiles(config, logger, importManager) {
1536
1536
  const importedFiles = importManager.getImportedFiles();
1537
1537
  for (const importedFile of importedFiles) {
1538
- const filePath = resolve(
1538
+ const filePath = resolve$1(
1539
1539
  process$1.cwd(),
1540
1540
  config.import.dir,
1541
1541
  importedFile.pathRelativeToImportDir
@@ -1558,10 +1558,12 @@ async function generateImportFiles(config, logger, importManager) {
1558
1558
  tagImportPath: config.import.tagImportPath,
1559
1559
  exports: processedExports
1560
1560
  };
1561
- const content = renderTemplate$1(templateData);
1561
+ const content = renderTemplate$2(templateData);
1562
1562
  await $LT_EnsureDirectoryExists(dirname(filePath));
1563
1563
  await writeFile(filePath, content, "utf-8");
1564
- logger.success('Created tag file: "{file}"', { file: importedFile.pathRelativeToImportDir });
1564
+ logger.success('Created tag file: "{file}"', {
1565
+ file: importedFile.pathRelativeToImportDir
1566
+ });
1565
1567
  }
1566
1568
  }
1567
1569
  class ImportManager {
@@ -1571,13 +1573,19 @@ class ImportManager {
1571
1573
  }
1572
1574
  importTag(pathRelativeToImportDir, tag) {
1573
1575
  if (!pathRelativeToImportDir) {
1574
- throw new Error(`pathRelativeToImportDir required, got: ${pathRelativeToImportDir}`);
1576
+ throw new Error(
1577
+ `pathRelativeToImportDir required, got: ${pathRelativeToImportDir}`
1578
+ );
1575
1579
  }
1576
1580
  if (!tag?.variableName) {
1577
- throw new Error(`tag.variableName required, got: ${tag?.variableName}`);
1581
+ throw new Error(
1582
+ `tag.variableName required, got: ${tag?.variableName}`
1583
+ );
1578
1584
  }
1579
1585
  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.`);
1586
+ throw new Error(
1587
+ `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.`
1588
+ );
1581
1589
  }
1582
1590
  if (tag.translations == null) {
1583
1591
  throw new Error(`tag.translations required`);
@@ -1590,7 +1598,9 @@ class ImportManager {
1590
1598
  (existingTag) => existingTag.variableName === tag.variableName
1591
1599
  );
1592
1600
  if (duplicateTag) {
1593
- throw new Error(`Duplicate variable name "${tag.variableName}" in file "${pathRelativeToImportDir}". Variable names must be unique within the same file.`);
1601
+ throw new Error(
1602
+ `Duplicate variable name "${tag.variableName}" in file "${pathRelativeToImportDir}". Variable names must be unique within the same file.`
1603
+ );
1594
1604
  }
1595
1605
  }
1596
1606
  if (!importedFile) {
@@ -1621,7 +1631,12 @@ async function $LT_ImportLibraries(config, logger) {
1621
1631
  const packageJSON = await $LT_ReadJSON(packageJsonPath);
1622
1632
  exports.push({ packageJSON, exportData });
1623
1633
  }
1624
- config.import.onImport({ exports, importManager, logger, langTagConfig: config });
1634
+ config.import.onImport({
1635
+ exports,
1636
+ importManager,
1637
+ logger,
1638
+ langTagConfig: config
1639
+ });
1625
1640
  if (!importManager.hasImportedFiles()) {
1626
1641
  logger.warn("No tags were imported from any library files");
1627
1642
  return;
@@ -1636,37 +1651,362 @@ async function $LT_ImportTranslations() {
1636
1651
  await $LT_ImportLibraries(config, logger);
1637
1652
  logger.success("Successfully imported translations from libraries.");
1638
1653
  }
1639
- function renderTemplate(template2, data) {
1640
- return mustache.render(template2, data, {}, { escape: (text) => text });
1654
+ function parseGitignore(cwd) {
1655
+ const gitignorePath = join(cwd, ".gitignore");
1656
+ try {
1657
+ const content = readFileSync(gitignorePath, "utf-8");
1658
+ return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).filter((line) => !line.startsWith("!")).map((line) => {
1659
+ if (line.endsWith("/")) line = line.slice(0, -1);
1660
+ if (line.startsWith("/")) line = line.slice(1);
1661
+ return line;
1662
+ }).filter((line) => {
1663
+ if (line.startsWith("*.")) return false;
1664
+ if (line.includes(".") && line.split(".")[1].length <= 4)
1665
+ return false;
1666
+ return true;
1667
+ });
1668
+ } catch {
1669
+ return [];
1670
+ }
1671
+ }
1672
+ function isIgnored(entry, ignorePatterns) {
1673
+ if (entry.startsWith(".")) {
1674
+ return true;
1675
+ }
1676
+ if (ignorePatterns.length === 0) {
1677
+ return false;
1678
+ }
1679
+ return micromatch.isMatch(entry, ignorePatterns);
1680
+ }
1681
+ function detectProjectDirectories() {
1682
+ const cwd = process.cwd();
1683
+ const ignorePatterns = parseGitignore(cwd);
1684
+ const detectedFolders = [];
1685
+ try {
1686
+ const entries = readdirSync(cwd);
1687
+ for (const entry of entries) {
1688
+ if (isIgnored(entry, ignorePatterns)) {
1689
+ continue;
1690
+ }
1691
+ try {
1692
+ const fullPath = join(cwd, entry);
1693
+ const stat = statSync(fullPath);
1694
+ if (stat.isDirectory()) {
1695
+ detectedFolders.push(entry);
1696
+ }
1697
+ } catch {
1698
+ }
1699
+ }
1700
+ } catch {
1701
+ return ["src", "app"];
1702
+ }
1703
+ return detectedFolders.length > 0 ? detectedFolders.sort() : ["src", "app"];
1704
+ }
1705
+ async function askProjectSetupQuestions() {
1706
+ const projectType = await select({
1707
+ message: "Is this a project or a library?",
1708
+ choices: [
1709
+ {
1710
+ name: "Project (application that consumes translations)",
1711
+ value: "project",
1712
+ description: "For applications that use translations"
1713
+ },
1714
+ {
1715
+ name: "Library (exports translations for other projects)",
1716
+ value: "library",
1717
+ description: "For packages that provide translations"
1718
+ }
1719
+ ]
1720
+ });
1721
+ const tagName = await input({
1722
+ message: "What name would you like for your translation tag function?",
1723
+ default: "lang"
1724
+ });
1725
+ let collectorType = "namespace";
1726
+ let namespaceOptions;
1727
+ let localesDirectory = "locales";
1728
+ const modifyNamespaceOptions = false;
1729
+ if (projectType === "project") {
1730
+ collectorType = await select({
1731
+ message: "How would you like to collect translations?",
1732
+ choices: [
1733
+ {
1734
+ name: "Namespace (organized by modules/features)",
1735
+ value: "namespace",
1736
+ description: "Organized structure with namespaces"
1737
+ },
1738
+ {
1739
+ name: "Dictionary (flat structure, all translations in one file)",
1740
+ value: "dictionary",
1741
+ description: "Simple flat dictionary structure"
1742
+ }
1743
+ ]
1744
+ });
1745
+ localesDirectory = await input({
1746
+ message: "Where should the translation files be stored?",
1747
+ default: "public/locales"
1748
+ });
1749
+ }
1750
+ const defaultNamespace = await input({
1751
+ message: "Default namespace for tags without explicit namespace:",
1752
+ default: "common"
1753
+ });
1754
+ namespaceOptions = {
1755
+ modifyNamespaceOptions,
1756
+ defaultNamespace
1757
+ };
1758
+ const enableConfigGeneration = await confirm({
1759
+ message: "Do you want to script config generation for tags?",
1760
+ default: projectType === "project"
1761
+ });
1762
+ let configGeneration = {
1763
+ enabled: enableConfigGeneration
1764
+ };
1765
+ if (enableConfigGeneration) {
1766
+ const algorithmChoice = await select({
1767
+ message: "Which config generation approach would you like?",
1768
+ choices: [
1769
+ {
1770
+ name: "Path-based (automatic based on file structure)",
1771
+ value: "path-based",
1772
+ description: "Generates namespace and path from file location"
1773
+ },
1774
+ {
1775
+ name: "Custom (write your own algorithm)",
1776
+ value: "custom",
1777
+ description: "Implement custom config generation logic"
1778
+ }
1779
+ ]
1780
+ });
1781
+ const keepVariables = await confirm({
1782
+ message: "Add a keeper mechanism that locks parts of the configuration from being overwritten?",
1783
+ default: true
1784
+ });
1785
+ configGeneration = {
1786
+ enabled: true,
1787
+ useAlgorithm: algorithmChoice,
1788
+ keepVariables
1789
+ };
1790
+ }
1791
+ const importLibraries = await confirm({
1792
+ message: "Do you plan to import translation tags from external libraries?",
1793
+ default: projectType === "project"
1794
+ });
1795
+ const interfereWithCollection = await confirm({
1796
+ message: "Do you want to interfere with collection mechanisms (conflict resolution, collection finish)?",
1797
+ default: false
1798
+ });
1799
+ const detectedDirectories = detectProjectDirectories();
1800
+ const includeDirectories = await checkbox({
1801
+ message: "Select directories where lang tags will be used (you can add more later):",
1802
+ choices: detectedDirectories.map((directory) => ({
1803
+ name: directory,
1804
+ value: directory,
1805
+ checked: directory === "src" || detectedDirectories.length === 1
1806
+ })),
1807
+ required: true
1808
+ });
1809
+ const baseLanguageCode = await input({
1810
+ message: "Base language code:",
1811
+ default: "en",
1812
+ validate: (value) => {
1813
+ if (!value || value.length < 2) {
1814
+ return "Please enter a valid language code (e.g., en, pl, fr, de, es)";
1815
+ }
1816
+ return true;
1817
+ }
1818
+ });
1819
+ const addCommentGuides = await confirm({
1820
+ message: "Would you like guides in comments?",
1821
+ default: true
1822
+ });
1823
+ return {
1824
+ projectType,
1825
+ tagName,
1826
+ collectorType,
1827
+ namespaceOptions,
1828
+ localesDirectory,
1829
+ configGeneration,
1830
+ importLibraries,
1831
+ interfereWithCollection,
1832
+ includeDirectories,
1833
+ baseLanguageCode,
1834
+ addCommentGuides
1835
+ };
1836
+ }
1837
+ function renderTemplate$1(template2, data, partials) {
1838
+ return mustache.render(template2, data, partials, {
1839
+ escape: (text) => text
1840
+ });
1641
1841
  }
1642
- function loadTemplate(templateName) {
1842
+ function loadTemplateFile(filename, required = true) {
1643
1843
  const __filename2 = fileURLToPath(import.meta.url);
1644
1844
  const __dirname2 = dirname(__filename2);
1645
- const templatePath2 = join(__dirname2, "templates", "tag", `${templateName}.mustache`);
1845
+ let templatePath2 = join(__dirname2, "templates", "config", filename);
1646
1846
  try {
1647
1847
  return readFileSync(templatePath2, "utf-8");
1648
- } catch (error) {
1649
- throw new Error(`Failed to load template ${templateName}: ${error}`);
1848
+ } catch {
1849
+ templatePath2 = join(
1850
+ __dirname2,
1851
+ "..",
1852
+ "..",
1853
+ "templates",
1854
+ "config",
1855
+ filename
1856
+ );
1857
+ try {
1858
+ return readFileSync(templatePath2, "utf-8");
1859
+ } catch (error) {
1860
+ if (required) {
1861
+ throw new Error(
1862
+ `Failed to load template "${filename}": ${error}`
1863
+ );
1864
+ }
1865
+ return null;
1866
+ }
1650
1867
  }
1651
1868
  }
1652
- function prepareTemplateData(options) {
1869
+ function loadTemplate$1() {
1870
+ return loadTemplateFile("config.mustache", true);
1871
+ }
1872
+ function loadPartials() {
1873
+ const partials = {};
1874
+ const generationAlgorithm = loadTemplateFile(
1875
+ "generation-algorithm.mustache",
1876
+ false
1877
+ );
1878
+ if (generationAlgorithm) {
1879
+ partials["generation-algorithm"] = generationAlgorithm;
1880
+ }
1881
+ return partials;
1882
+ }
1883
+ function buildIncludesPattern(directories) {
1884
+ return directories.map((directory) => `'${directory}/**/*.{js,ts,jsx,tsx}'`).join(", ");
1885
+ }
1886
+ function buildExcludesPattern() {
1887
+ const excludes = [
1888
+ "node_modules",
1889
+ "dist",
1890
+ "build",
1891
+ "**/*.test.ts",
1892
+ "**/*.spec.ts"
1893
+ ];
1894
+ return excludes.map((e) => `'${e}'`).join(", ");
1895
+ }
1896
+ function prepareTemplateData$1(options) {
1897
+ const { answers, moduleSystem } = options;
1898
+ const needsPathBasedImport = answers.configGeneration.enabled && answers.configGeneration.useAlgorithm === "path-based";
1899
+ const hasConfigGeneration = answers.configGeneration.enabled;
1900
+ const usePathBased = hasConfigGeneration && answers.configGeneration.useAlgorithm === "path-based";
1901
+ const useCustom = hasConfigGeneration && answers.configGeneration.useAlgorithm === "custom";
1902
+ const needsTagName = answers.tagName !== "lang";
1903
+ const useKeeper = answers.configGeneration.keepVariables || false;
1904
+ const isDictionaryCollector = answers.collectorType === "dictionary";
1905
+ const isModifiedNamespaceCollector = answers.collectorType === "namespace" && !!answers.namespaceOptions?.modifyNamespaceOptions;
1906
+ const importLibraries = answers.importLibraries;
1907
+ const defaultNamespace = answers.namespaceOptions?.defaultNamespace || "common";
1908
+ const isDefaultNamespace = defaultNamespace === "common";
1909
+ const hasCollectContent = isDictionaryCollector || isModifiedNamespaceCollector || answers.interfereWithCollection || !isDefaultNamespace;
1910
+ const needsAlgorithms = needsPathBasedImport || useKeeper || isDictionaryCollector || isModifiedNamespaceCollector || importLibraries;
1653
1911
  return {
1654
- ...options,
1655
- tmpVariables: {
1656
- key: "{{key}}",
1657
- username: "{{username}}",
1658
- processRegex: "{{(.*?)}}"
1659
- }
1912
+ isCJS: moduleSystem === "cjs",
1913
+ addComments: answers.addCommentGuides,
1914
+ needsAlgorithms,
1915
+ needsPathBasedImport,
1916
+ useKeeper,
1917
+ isDictionaryCollector,
1918
+ isModifiedNamespaceCollector,
1919
+ importLibraries,
1920
+ needsTagName,
1921
+ tagName: answers.tagName,
1922
+ isLibrary: answers.projectType === "library",
1923
+ includes: buildIncludesPattern(answers.includeDirectories),
1924
+ excludes: buildExcludesPattern(),
1925
+ localesDirectory: answers.localesDirectory,
1926
+ baseLanguageCode: answers.baseLanguageCode,
1927
+ hasConfigGeneration,
1928
+ usePathBased,
1929
+ useCustom,
1930
+ defaultNamespace,
1931
+ isDefaultNamespace,
1932
+ interfereWithCollection: answers.interfereWithCollection,
1933
+ hasCollectContent
1660
1934
  };
1661
1935
  }
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;
1936
+ function renderConfigTemplate(options) {
1937
+ const template2 = loadTemplate$1();
1938
+ const templateData = prepareTemplateData$1(options);
1939
+ const partials = loadPartials();
1940
+ return renderTemplate$1(template2, templateData, partials);
1941
+ }
1942
+ async function detectModuleSystem() {
1943
+ const packageJsonPath = join(process.cwd(), "package.json");
1944
+ if (!existsSync(packageJsonPath)) {
1945
+ return "cjs";
1946
+ }
1947
+ try {
1948
+ const content = await readFile(packageJsonPath, "utf-8");
1949
+ const packageJson = JSON.parse(content);
1950
+ if (packageJson.type === "module") {
1951
+ return "esm";
1952
+ }
1953
+ return "cjs";
1954
+ } catch (error) {
1955
+ return "cjs";
1956
+ }
1957
+ }
1958
+ async function $LT_CMD_InitConfig() {
1959
+ const logger = $LT_CreateDefaultLogger();
1960
+ if (existsSync(CONFIG_FILE_NAME)) {
1961
+ logger.error(
1962
+ "Configuration file already exists. Please remove the existing configuration file before creating a new one"
1963
+ );
1964
+ return;
1965
+ }
1966
+ console.log("");
1967
+ logger.info("Welcome to Lang Tag CLI Setup!");
1968
+ console.log("");
1969
+ try {
1970
+ const answers = await askProjectSetupQuestions();
1971
+ const moduleSystem = await detectModuleSystem();
1972
+ const configContent = renderConfigTemplate({
1973
+ answers,
1974
+ moduleSystem
1975
+ });
1976
+ await writeFile(CONFIG_FILE_NAME, configContent, "utf-8");
1977
+ logger.success("Configuration file created successfully!");
1978
+ logger.success("Created {configFile}", {
1979
+ configFile: CONFIG_FILE_NAME
1980
+ });
1981
+ logger.info("Next steps:");
1982
+ logger.info(" 1. Review and customize {configFile}", {
1983
+ configFile: CONFIG_FILE_NAME
1984
+ });
1985
+ logger.info(
1986
+ " 2. Since you have installed all basic libraries (React, TypeScript, etc.)"
1987
+ );
1988
+ logger.info(
1989
+ " and the initialized basic tag is based on what you use in your project,"
1990
+ );
1991
+ logger.info(
1992
+ ' we recommend using "npx lang-tag init-tag" to generate an initial version of the tag'
1993
+ );
1994
+ logger.info(
1995
+ " 3. Use your tag in the project under the include directories"
1996
+ );
1997
+ logger.info(' 4. Run "npx lang-tag collect" to collect translations');
1998
+ if (answers.projectType === "project") {
1999
+ logger.info(" 5. Your translations will be in {dir}", {
2000
+ dir: answers.localesDirectory
2001
+ });
2002
+ }
2003
+ } catch (error) {
2004
+ if (error.name === "ExitPromptError") {
2005
+ logger.warn("Setup cancelled");
2006
+ return;
2007
+ }
2008
+ logger.error(error?.message || "An error occurred during setup");
2009
+ }
1670
2010
  }
1671
2011
  async function readPackageJson() {
1672
2012
  const packageJsonPath = join(process.cwd(), "package.json");
@@ -1709,51 +2049,312 @@ async function detectInitTagOptions(options, config) {
1709
2049
  packageVersion: packageJson?.version || "1.0.0"
1710
2050
  };
1711
2051
  }
2052
+ function renderTemplate(template2, data) {
2053
+ return mustache.render(template2, data, {}, { escape: (text) => text });
2054
+ }
2055
+ function loadTemplate(templateName) {
2056
+ const __filename2 = fileURLToPath(import.meta.url);
2057
+ const __dirname2 = dirname(__filename2);
2058
+ const templatePath2 = join(
2059
+ __dirname2,
2060
+ "templates",
2061
+ "tag",
2062
+ `${templateName}.mustache`
2063
+ );
2064
+ try {
2065
+ return readFileSync(templatePath2, "utf-8");
2066
+ } catch (error) {
2067
+ throw new Error(`Failed to load template ${templateName}: ${error}`);
2068
+ }
2069
+ }
2070
+ function prepareTemplateData(options) {
2071
+ return {
2072
+ ...options,
2073
+ tmpVariables: {
2074
+ key: "{{key}}",
2075
+ username: "{{username}}",
2076
+ processRegex: "{{(.*?)}}"
2077
+ }
2078
+ };
2079
+ }
2080
+ function renderInitTagTemplates(options) {
2081
+ const baseTemplateName = options.isLibrary ? "base-library" : "base-app";
2082
+ const baseTemplate = loadTemplate(baseTemplateName);
2083
+ const placeholderTemplate = loadTemplate("placeholder");
2084
+ const templateData = prepareTemplateData(options);
2085
+ const renderedBase = renderTemplate(baseTemplate, templateData);
2086
+ const renderedPlaceholders = renderTemplate(
2087
+ placeholderTemplate,
2088
+ templateData
2089
+ );
2090
+ return renderedBase + "\n\n" + renderedPlaceholders;
2091
+ }
1712
2092
  async function $LT_CMD_InitTagFile(options = {}) {
1713
2093
  const { config, logger } = await $LT_GetCommandEssentials();
1714
2094
  const renderOptions = await detectInitTagOptions(options, config);
1715
2095
  const outputPath = options.output || `${renderOptions.tagName}.${renderOptions.fileExtension}`;
1716
2096
  logger.info("Initializing lang-tag with the following options:");
1717
2097
  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" });
2098
+ logger.info(" Library mode: {isLibrary}", {
2099
+ isLibrary: renderOptions.isLibrary ? "Yes" : "No"
2100
+ });
2101
+ logger.info(" React: {isReact}", {
2102
+ isReact: renderOptions.isReact ? "Yes" : "No"
2103
+ });
2104
+ logger.info(" TypeScript: {isTypeScript}", {
2105
+ isTypeScript: renderOptions.isTypeScript ? "Yes" : "No"
2106
+ });
1721
2107
  logger.info(" Output path: {outputPath}", { outputPath });
1722
2108
  let renderedContent;
1723
2109
  try {
1724
2110
  renderedContent = renderInitTagTemplates(renderOptions);
1725
2111
  } catch (error) {
1726
- logger.error("Failed to render templates: {error}", { error: error?.message });
2112
+ logger.error("Failed to render templates: {error}", {
2113
+ error: error?.message
2114
+ });
1727
2115
  return;
1728
2116
  }
1729
2117
  if (existsSync(outputPath)) {
1730
2118
  logger.warn("File already exists: {outputPath}", { outputPath });
1731
- logger.info("Use --output to specify a different path or remove the existing file");
2119
+ logger.info(
2120
+ "Use --output to specify a different path or remove the existing file"
2121
+ );
1732
2122
  return;
1733
2123
  }
1734
2124
  try {
1735
2125
  await $LT_WriteFileWithDirs(outputPath, renderedContent);
1736
- logger.success("Lang-tag file created successfully: {outputPath}", { outputPath });
2126
+ logger.success("Lang-tag file created successfully: {outputPath}", {
2127
+ outputPath
2128
+ });
1737
2129
  logger.info("Next steps:");
1738
- logger.info("1. Import the {tagName} function in your files:", { tagName: renderOptions.tagName });
2130
+ logger.info("1. Import the {tagName} function in your files:", {
2131
+ tagName: renderOptions.tagName
2132
+ });
1739
2133
  logger.info(" import { {tagName} } from './{importPath}';", {
1740
2134
  tagName: renderOptions.tagName,
1741
2135
  importPath: outputPath.replace(/^src\//, "")
1742
2136
  });
1743
- logger.info("2. Create your translation objects and use the tag function");
2137
+ logger.info(
2138
+ "2. Create your translation objects and use the tag function"
2139
+ );
1744
2140
  logger.info('3. Run "lang-tag collect" to extract translations');
1745
2141
  } catch (error) {
1746
- logger.error("Failed to write file: {error}", { error: error?.message });
2142
+ logger.error("Failed to write file: {error}", {
2143
+ error: error?.message
2144
+ });
2145
+ }
2146
+ }
2147
+ function deepFreezeObject(obj) {
2148
+ const propNames = Object.getOwnPropertyNames(obj);
2149
+ for (const name of propNames) {
2150
+ const value = obj[name];
2151
+ if (value && typeof value === "object") {
2152
+ deepFreezeObject(value);
2153
+ }
2154
+ }
2155
+ return Object.freeze(obj);
2156
+ }
2157
+ async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
2158
+ let libraryImportsDir = config.import.dir;
2159
+ if (!libraryImportsDir.endsWith(sep)) libraryImportsDir += sep;
2160
+ const fileContent = readFileSync(file, "utf-8");
2161
+ const processor = new $LT_TagProcessor(config);
2162
+ let tags = processor.extractTags(fileContent);
2163
+ tags = $LT_FilterInvalidTags(tags, config, logger);
2164
+ if (!tags.length) {
2165
+ return false;
2166
+ }
2167
+ const replacements = [];
2168
+ let lastUpdatedLine = 0;
2169
+ for (let tag of tags) {
2170
+ let newConfig = void 0;
2171
+ let shouldUpdate = false;
2172
+ const frozenConfig = tag.parameterConfig ? deepFreezeObject(tag.parameterConfig) : tag.parameterConfig;
2173
+ const event = {
2174
+ langTagConfig: config,
2175
+ logger,
2176
+ config: frozenConfig,
2177
+ absolutePath: file,
2178
+ relativePath: path2,
2179
+ isImportedLibrary: path2.startsWith(libraryImportsDir),
2180
+ isSaved: false,
2181
+ savedConfig: void 0,
2182
+ save: (updatedConfig, triggerName) => {
2183
+ if (!updatedConfig && updatedConfig !== null)
2184
+ throw new Error("Wrong config data");
2185
+ newConfig = updatedConfig;
2186
+ shouldUpdate = true;
2187
+ event.isSaved = true;
2188
+ event.savedConfig = updatedConfig;
2189
+ logger.debug(
2190
+ 'Called save for "{path}" with config "{config}" triggered by: ("{trigger}")',
2191
+ {
2192
+ path: path2,
2193
+ config: JSON.stringify(updatedConfig),
2194
+ trigger: triggerName || "-"
2195
+ }
2196
+ );
2197
+ }
2198
+ };
2199
+ await config.onConfigGeneration(event);
2200
+ if (!shouldUpdate) {
2201
+ continue;
2202
+ }
2203
+ lastUpdatedLine = tag.line;
2204
+ if (!isConfigSame(tag.parameterConfig, newConfig)) {
2205
+ replacements.push({ tag, config: newConfig });
2206
+ }
2207
+ }
2208
+ if (replacements.length) {
2209
+ const newContent = processor.replaceTags(fileContent, replacements);
2210
+ await writeFile(file, newContent, "utf-8");
2211
+ const encodedFile = encodeURI(file);
2212
+ logger.info(
2213
+ 'Lang tag configurations written for file "{path}" (file://{file}:{line})',
2214
+ { path: path2, file: encodedFile, line: lastUpdatedLine }
2215
+ );
2216
+ return true;
2217
+ }
2218
+ return false;
2219
+ }
2220
+ function isConfigSame(c1, c2) {
2221
+ if (!c1 && !c2) return true;
2222
+ if (c1 && typeof c1 === "object" && c2 && typeof c2 === "object" && JSON5.stringify(c1) === JSON5.stringify(c2))
2223
+ return true;
2224
+ return false;
2225
+ }
2226
+ async function $LT_CMD_RegenerateTags() {
2227
+ const { config, logger } = await $LT_GetCommandEssentials();
2228
+ const files = await globby(config.includes, {
2229
+ cwd: process.cwd(),
2230
+ ignore: config.excludes,
2231
+ absolute: true
2232
+ });
2233
+ const charactersToSkip = process.cwd().length + 1;
2234
+ let dirty = false;
2235
+ for (const file of files) {
2236
+ const path2 = file.substring(charactersToSkip);
2237
+ const localDirty = await checkAndRegenerateFileLangTags(
2238
+ config,
2239
+ logger,
2240
+ file,
2241
+ path2
2242
+ );
2243
+ if (localDirty) {
2244
+ dirty = true;
2245
+ }
1747
2246
  }
2247
+ if (!dirty) {
2248
+ logger.info(
2249
+ "No changes were made based on the current configuration and files"
2250
+ );
2251
+ }
2252
+ }
2253
+ function getBasePath(pattern) {
2254
+ const globStartIndex = pattern.indexOf("*");
2255
+ const braceStartIndex = pattern.indexOf("{");
2256
+ let endIndex = -1;
2257
+ if (globStartIndex !== -1 && braceStartIndex !== -1) {
2258
+ endIndex = Math.min(globStartIndex, braceStartIndex);
2259
+ } else if (globStartIndex !== -1) {
2260
+ endIndex = globStartIndex;
2261
+ } else if (braceStartIndex !== -1) {
2262
+ endIndex = braceStartIndex;
2263
+ }
2264
+ if (endIndex === -1) {
2265
+ const lastSlashIndex = pattern.lastIndexOf("/");
2266
+ return lastSlashIndex !== -1 ? pattern.substring(0, lastSlashIndex) : ".";
2267
+ }
2268
+ const lastSeparatorIndex = pattern.lastIndexOf("/", endIndex);
2269
+ return lastSeparatorIndex === -1 ? "." : pattern.substring(0, lastSeparatorIndex);
2270
+ }
2271
+ function $LT_CreateChokidarWatcher(config) {
2272
+ const cwd = process.cwd();
2273
+ const baseDirsToWatch = [
2274
+ ...new Set(config.includes.map((pattern) => getBasePath(pattern)))
2275
+ ];
2276
+ const finalDirsToWatch = baseDirsToWatch.map(
2277
+ (dir) => dir === "." ? cwd : dir
2278
+ );
2279
+ const ignored = [...config.excludes, "**/.git/**"];
2280
+ return chokidar.watch(finalDirsToWatch, {
2281
+ // Watch base directories
2282
+ cwd,
2283
+ ignored,
2284
+ persistent: true,
2285
+ ignoreInitial: true,
2286
+ awaitWriteFinish: {
2287
+ stabilityThreshold: 300,
2288
+ pollInterval: 100
2289
+ }
2290
+ });
2291
+ }
2292
+ async function $LT_WatchTranslations() {
2293
+ const { config, logger } = await $LT_GetCommandEssentials();
2294
+ await $LT_CMD_Collect();
2295
+ const watcher = $LT_CreateChokidarWatcher(config);
2296
+ logger.info("Starting watch mode for translations...");
2297
+ logger.info("Watching for changes...");
2298
+ logger.info("Press Ctrl+C to stop watching");
2299
+ watcher.on(
2300
+ "change",
2301
+ async (filePath) => await handleFile(config, logger, filePath)
2302
+ ).on(
2303
+ "add",
2304
+ async (filePath) => await handleFile(config, logger, filePath)
2305
+ ).on("error", (error) => {
2306
+ logger.error("Error in file watcher: {error}", { error });
2307
+ });
2308
+ }
2309
+ async function handleFile(config, logger, cwdRelativeFilePath, event) {
2310
+ if (!micromatch.isMatch(cwdRelativeFilePath, config.includes)) {
2311
+ return;
2312
+ }
2313
+ const cwd = process.cwd();
2314
+ const absoluteFilePath = path__default.join(cwd, cwdRelativeFilePath);
2315
+ await checkAndRegenerateFileLangTags(
2316
+ config,
2317
+ logger,
2318
+ absoluteFilePath,
2319
+ cwdRelativeFilePath
2320
+ );
2321
+ const files = await $LT_CollectCandidateFilesWithTags({
2322
+ filesToScan: [cwdRelativeFilePath],
2323
+ config,
2324
+ logger
2325
+ });
2326
+ const namespaces = await $LT_GroupTagsToCollections({
2327
+ logger,
2328
+ files,
2329
+ config
2330
+ });
2331
+ await $LT_WriteToCollections({ config, collections: namespaces, logger });
1748
2332
  }
1749
2333
  function createCli() {
1750
2334
  program.name("lang-tag").description("CLI to manage language translations").version("0.1.0");
1751
2335
  program.command("collect").alias("c").description("Collect translations from source files").option("-c, --clean", "Remove output directory before collecting").action($LT_CMD_Collect);
1752
2336
  program.command("import").alias("i").description("Import translations from libraries in node_modules").action($LT_ImportTranslations);
1753
2337
  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);
2338
+ program.command("watch").alias("w").description(
2339
+ "Watch for changes in source files and automatically collect translations"
2340
+ ).action($LT_WatchTranslations);
1755
2341
  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) => {
2342
+ program.command("init-tag").description("Initialize a new lang-tag function file").option(
2343
+ "-n, --name <name>",
2344
+ "Name of the tag function (default: from config)"
2345
+ ).option(
2346
+ "-l, --library",
2347
+ "Generate library-style tag (default: from config)"
2348
+ ).option(
2349
+ "-r, --react",
2350
+ "Include React-specific optimizations (default: auto-detect)"
2351
+ ).option(
2352
+ "-t, --typescript",
2353
+ "Force TypeScript output (default: auto-detect)"
2354
+ ).option(
2355
+ "-o, --output <path>",
2356
+ "Output file path (default: auto-generated)"
2357
+ ).action(async (options) => {
1757
2358
  await $LT_CMD_InitTagFile(options);
1758
2359
  });
1759
2360
  return program;