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