@lang-tag/cli 0.15.0 → 0.17.0

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