@lang-tag/cli 0.9.4 → 0.10.1
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 +1 -1
- package/{cli/config.d.ts → config.d.ts} +21 -16
- package/index.cjs +1674 -74
- package/index.js +1656 -74
- package/package.json +6 -3
- package/cli/index.cjs +0 -1492
- package/cli/index.js +0 -1474
- package/index.d.ts +0 -169
- /package/{cli/logger.d.ts → logger.d.ts} +0 -0
- /package/{cli/template → template}/base-app.mustache +0 -0
- /package/{cli/template → template}/base-library.mustache +0 -0
- /package/{cli/template → template}/placeholder.mustache +0 -0
package/cli/index.js
DELETED
|
@@ -1,1474 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { program } from "commander";
|
|
3
|
-
import fs, { readFileSync, existsSync } from "fs";
|
|
4
|
-
import { writeFile, mkdir, readFile, rm } from "fs/promises";
|
|
5
|
-
import JSON5 from "json5";
|
|
6
|
-
import * as path from "path";
|
|
7
|
-
import path__default, { sep, dirname, resolve as resolve$1, join } from "path";
|
|
8
|
-
import { globby } from "globby";
|
|
9
|
-
import path$1, { resolve, dirname as dirname$1 } from "pathe";
|
|
10
|
-
import { pathToFileURL, fileURLToPath } from "url";
|
|
11
|
-
import * as process$1 from "node:process";
|
|
12
|
-
import process__default from "node:process";
|
|
13
|
-
import * as acorn from "acorn";
|
|
14
|
-
import micromatch from "micromatch";
|
|
15
|
-
import chokidar from "chokidar";
|
|
16
|
-
import mustache from "mustache";
|
|
17
|
-
class $LT_TagProcessor {
|
|
18
|
-
constructor(config) {
|
|
19
|
-
this.config = config;
|
|
20
|
-
}
|
|
21
|
-
extractTags(fileContent) {
|
|
22
|
-
const tagName = this.config.tagName;
|
|
23
|
-
const optionalVariableAssignment = `(?:\\s*(\\w+)\\s*=\\s*)?`;
|
|
24
|
-
const matches = [];
|
|
25
|
-
let currentIndex = 0;
|
|
26
|
-
const startPattern = new RegExp(`${optionalVariableAssignment}${tagName}\\(\\s*\\{`, "g");
|
|
27
|
-
while (true) {
|
|
28
|
-
startPattern.lastIndex = currentIndex;
|
|
29
|
-
const startMatch = startPattern.exec(fileContent);
|
|
30
|
-
if (!startMatch) break;
|
|
31
|
-
const matchStartIndex = startMatch.index;
|
|
32
|
-
const variableName = startMatch[1] || void 0;
|
|
33
|
-
let braceCount = 1;
|
|
34
|
-
let i = matchStartIndex + startMatch[0].length;
|
|
35
|
-
while (i < fileContent.length && braceCount > 0) {
|
|
36
|
-
if (fileContent[i] === "{") braceCount++;
|
|
37
|
-
if (fileContent[i] === "}") braceCount--;
|
|
38
|
-
i++;
|
|
39
|
-
}
|
|
40
|
-
if (braceCount !== 0) {
|
|
41
|
-
currentIndex = matchStartIndex + 1;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
let parameter1Text = fileContent.substring(matchStartIndex + startMatch[0].length - 1, i);
|
|
45
|
-
let parameter2Text;
|
|
46
|
-
while (i < fileContent.length && (fileContent[i] === " " || fileContent[i] === "\n" || fileContent[i] === " ")) {
|
|
47
|
-
i++;
|
|
48
|
-
}
|
|
49
|
-
if (i >= fileContent.length) {
|
|
50
|
-
currentIndex = matchStartIndex + 1;
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (fileContent[i] === ",") {
|
|
54
|
-
i++;
|
|
55
|
-
while (i < fileContent.length && (fileContent[i] === " " || fileContent[i] === "\n" || fileContent[i] === " ")) {
|
|
56
|
-
i++;
|
|
57
|
-
}
|
|
58
|
-
if (i >= fileContent.length || fileContent[i] !== "{") {
|
|
59
|
-
currentIndex = matchStartIndex + 1;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
braceCount = 1;
|
|
63
|
-
const secondParamStart = i;
|
|
64
|
-
i++;
|
|
65
|
-
while (i < fileContent.length && braceCount > 0) {
|
|
66
|
-
if (fileContent[i] === "{") braceCount++;
|
|
67
|
-
if (fileContent[i] === "}") braceCount--;
|
|
68
|
-
i++;
|
|
69
|
-
}
|
|
70
|
-
if (braceCount !== 0) {
|
|
71
|
-
currentIndex = matchStartIndex + 1;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
parameter2Text = fileContent.substring(secondParamStart, i);
|
|
75
|
-
while (i < fileContent.length && (fileContent[i] === " " || fileContent[i] === "\n" || fileContent[i] === " ")) {
|
|
76
|
-
i++;
|
|
77
|
-
}
|
|
78
|
-
if (i < fileContent.length && fileContent[i] === ",") {
|
|
79
|
-
i++;
|
|
80
|
-
while (i < fileContent.length && (fileContent[i] === " " || fileContent[i] === "\n" || fileContent[i] === " ")) {
|
|
81
|
-
i++;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
} else if (fileContent[i] !== ")") {
|
|
85
|
-
currentIndex = matchStartIndex + 1;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (i >= fileContent.length || fileContent[i] !== ")") {
|
|
89
|
-
currentIndex = matchStartIndex + 1;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
i++;
|
|
93
|
-
const fullMatch = fileContent.substring(matchStartIndex, i);
|
|
94
|
-
const { line, column } = getLineAndColumn(fileContent, matchStartIndex);
|
|
95
|
-
let validity = "ok";
|
|
96
|
-
let parameter1 = void 0;
|
|
97
|
-
let parameter2 = void 0;
|
|
98
|
-
try {
|
|
99
|
-
parameter1 = JSON5.parse(parameter1Text);
|
|
100
|
-
if (parameter2Text) {
|
|
101
|
-
try {
|
|
102
|
-
parameter2 = JSON5.parse(parameter2Text);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
validity = "invalid-param-2";
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} catch (error) {
|
|
108
|
-
validity = "invalid-param-1";
|
|
109
|
-
}
|
|
110
|
-
let parameterTranslations = this.config.translationArgPosition === 1 ? parameter1 : parameter2;
|
|
111
|
-
let parameterConfig = this.config.translationArgPosition === 1 ? parameter2 : parameter1;
|
|
112
|
-
if (validity === "ok") {
|
|
113
|
-
if (!parameterTranslations) validity = "translations-not-found";
|
|
114
|
-
}
|
|
115
|
-
matches.push({
|
|
116
|
-
fullMatch,
|
|
117
|
-
variableName,
|
|
118
|
-
parameter1Text,
|
|
119
|
-
parameter2Text,
|
|
120
|
-
parameterTranslations,
|
|
121
|
-
parameterConfig,
|
|
122
|
-
index: matchStartIndex,
|
|
123
|
-
line,
|
|
124
|
-
column,
|
|
125
|
-
validity
|
|
126
|
-
});
|
|
127
|
-
currentIndex = i;
|
|
128
|
-
}
|
|
129
|
-
return matches;
|
|
130
|
-
}
|
|
131
|
-
replaceTags(fileContent, replacements) {
|
|
132
|
-
const replaceMap = /* @__PURE__ */ new Map();
|
|
133
|
-
replacements.forEach((R) => {
|
|
134
|
-
if (!R.translations && !R.config) {
|
|
135
|
-
throw new Error("Replacement data is required!");
|
|
136
|
-
}
|
|
137
|
-
const tag = R.tag;
|
|
138
|
-
let newTranslationsString = R.translations;
|
|
139
|
-
if (!newTranslationsString) newTranslationsString = this.config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text;
|
|
140
|
-
else if (typeof newTranslationsString !== "string") newTranslationsString = JSON5.stringify(newTranslationsString);
|
|
141
|
-
if (!newTranslationsString) throw new Error("Tag must have translations provided!");
|
|
142
|
-
try {
|
|
143
|
-
JSON5.parse(newTranslationsString);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
throw new Error(`Tag translations are invalid object! Translations: ${newTranslationsString}`);
|
|
146
|
-
}
|
|
147
|
-
let newConfigString = R.config;
|
|
148
|
-
if (!newConfigString) newConfigString = tag.parameterConfig;
|
|
149
|
-
if (newConfigString) {
|
|
150
|
-
try {
|
|
151
|
-
if (typeof newConfigString === "string") JSON5.parse(newConfigString);
|
|
152
|
-
else newConfigString = JSON5.stringify(newConfigString);
|
|
153
|
-
} catch (error) {
|
|
154
|
-
throw new Error(`Tag config is invalid object! Config: ${newConfigString}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const arg1 = this.config.translationArgPosition === 1 ? newTranslationsString : newConfigString;
|
|
158
|
-
const arg2 = this.config.translationArgPosition === 1 ? newConfigString : newTranslationsString;
|
|
159
|
-
let tagFunction = `${this.config.tagName}(${arg1}`;
|
|
160
|
-
if (arg2) tagFunction += `, ${arg2}`;
|
|
161
|
-
tagFunction += ")";
|
|
162
|
-
if (tag.variableName) replaceMap.set(tag, ` ${tag.variableName} = ${tagFunction}`);
|
|
163
|
-
else replaceMap.set(tag, tagFunction);
|
|
164
|
-
});
|
|
165
|
-
let offset = 0;
|
|
166
|
-
replaceMap.forEach((replacement, match) => {
|
|
167
|
-
const startIdx = match.index + offset;
|
|
168
|
-
const endIdx = startIdx + match.fullMatch.length;
|
|
169
|
-
fileContent = fileContent.slice(0, startIdx) + replacement + fileContent.slice(endIdx);
|
|
170
|
-
offset += replacement.length - match.fullMatch.length;
|
|
171
|
-
});
|
|
172
|
-
return fileContent;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
function getLineAndColumn(text, matchIndex) {
|
|
176
|
-
const lines = text.slice(0, matchIndex).split("\n");
|
|
177
|
-
const line = lines.length;
|
|
178
|
-
const column = lines[lines.length - 1].length + 1;
|
|
179
|
-
return { line, column };
|
|
180
|
-
}
|
|
181
|
-
function $LT_FilterInvalidTags(tags, config, logger) {
|
|
182
|
-
return tags.filter((tag) => {
|
|
183
|
-
if (tag.validity === "invalid-param-1")
|
|
184
|
-
logger.debug('Skipping tag "{fullMatch}". Invalid JSON: "{invalid}"', {
|
|
185
|
-
fullMatch: tag.fullMatch.trim(),
|
|
186
|
-
invalid: tag.parameter1Text
|
|
187
|
-
});
|
|
188
|
-
if (tag.validity === "invalid-param-2")
|
|
189
|
-
logger.debug('Skipping tag "{fullMatch}". Invalid JSON: "{invalid}"', {
|
|
190
|
-
fullMatch: tag.fullMatch.trim(),
|
|
191
|
-
invalid: tag.parameter2Text
|
|
192
|
-
});
|
|
193
|
-
if (tag.validity === "translations-not-found")
|
|
194
|
-
logger.debug('Skipping tag "{fullMatch}". Translations not found at parameter position: {pos}', {
|
|
195
|
-
fullMatch: tag.fullMatch.trim(),
|
|
196
|
-
pos: config.translationArgPosition
|
|
197
|
-
});
|
|
198
|
-
return tag.validity === "ok";
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
function $LT_FilterEmptyNamespaceTags(tags, logger) {
|
|
202
|
-
return tags.filter((tag) => {
|
|
203
|
-
if (!tag.parameterConfig) {
|
|
204
|
-
logger.warn('Skipping tag "{fullMatch}". Tag configuration not defined. (Check lang-tag config at collect.onCollectConfigFix)', {
|
|
205
|
-
fullMatch: tag.fullMatch.trim()
|
|
206
|
-
});
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
if (!tag.parameterConfig.namespace) {
|
|
210
|
-
logger.warn('Skipping tag "{fullMatch}". Tag configuration namespace not defined. (Check lang-tag config at collect.onCollectConfigFix)', {
|
|
211
|
-
fullMatch: tag.fullMatch.trim()
|
|
212
|
-
});
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
return true;
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
|
|
219
|
-
let libraryImportsDir = config.import.dir;
|
|
220
|
-
if (!libraryImportsDir.endsWith(sep)) libraryImportsDir += sep;
|
|
221
|
-
const fileContent = readFileSync(file, "utf-8");
|
|
222
|
-
const processor = new $LT_TagProcessor(config);
|
|
223
|
-
let tags = processor.extractTags(fileContent);
|
|
224
|
-
tags = $LT_FilterInvalidTags(tags, config, logger);
|
|
225
|
-
if (!tags.length) {
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
const replacements = [];
|
|
229
|
-
for (let tag of tags) {
|
|
230
|
-
const newConfig = config.onConfigGeneration({
|
|
231
|
-
config: tag.parameterConfig,
|
|
232
|
-
fullPath: file,
|
|
233
|
-
path: path2,
|
|
234
|
-
isImportedLibrary: path2.startsWith(libraryImportsDir)
|
|
235
|
-
});
|
|
236
|
-
if (newConfig === void 0) {
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (JSON5.stringify(tag.parameterConfig) !== JSON5.stringify(newConfig)) {
|
|
240
|
-
replacements.push({ tag, config: newConfig });
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (replacements.length) {
|
|
244
|
-
const newContent = processor.replaceTags(fileContent, replacements);
|
|
245
|
-
await writeFile(file, newContent, "utf-8");
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
const CONFIG_FILE_NAME = ".lang-tag.config.js";
|
|
251
|
-
const EXPORTS_FILE_NAME = ".lang-tag.exports.json";
|
|
252
|
-
const LANG_TAG_DEFAULT_CONFIG = {
|
|
253
|
-
tagName: "lang",
|
|
254
|
-
includes: ["src/**/*.{js,ts,jsx,tsx}"],
|
|
255
|
-
excludes: ["node_modules", "dist", "build"],
|
|
256
|
-
outputDir: "locales/en",
|
|
257
|
-
collect: {
|
|
258
|
-
defaultNamespace: "common",
|
|
259
|
-
ignoreConflictsWithMatchingValues: true,
|
|
260
|
-
onCollectConfigFix: ({ config, langTagConfig }) => {
|
|
261
|
-
if (langTagConfig.isLibrary) return config;
|
|
262
|
-
if (!config) return { path: "", namespace: langTagConfig.collect.defaultNamespace };
|
|
263
|
-
if (!config.path) config.path = "";
|
|
264
|
-
if (!config.namespace) config.namespace = langTagConfig.collect.defaultNamespace;
|
|
265
|
-
return config;
|
|
266
|
-
},
|
|
267
|
-
onConflictResolution: async (event) => {
|
|
268
|
-
await event.logger.conflict(event.conflict, true);
|
|
269
|
-
},
|
|
270
|
-
onCollectFinish: (event) => {
|
|
271
|
-
event.exit();
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
import: {
|
|
275
|
-
dir: "src/lang-libraries",
|
|
276
|
-
tagImportPath: 'import { lang } from "@/my-lang-tag-path"',
|
|
277
|
-
onImport: ({ importedRelativePath, fileGenerationData }, actions) => {
|
|
278
|
-
const exportIndex = (fileGenerationData.index || 0) + 1;
|
|
279
|
-
fileGenerationData.index = exportIndex;
|
|
280
|
-
actions.setFile(path$1.basename(importedRelativePath));
|
|
281
|
-
actions.setExportName(`translations${exportIndex}`);
|
|
282
|
-
}
|
|
283
|
-
},
|
|
284
|
-
isLibrary: false,
|
|
285
|
-
language: "en",
|
|
286
|
-
translationArgPosition: 1,
|
|
287
|
-
onConfigGeneration: (params) => void 0
|
|
288
|
-
};
|
|
289
|
-
async function $LT_ReadConfig(projectPath) {
|
|
290
|
-
const configPath = resolve(projectPath, CONFIG_FILE_NAME);
|
|
291
|
-
if (!existsSync(configPath)) {
|
|
292
|
-
throw new Error(`No "${CONFIG_FILE_NAME}" detected`);
|
|
293
|
-
}
|
|
294
|
-
try {
|
|
295
|
-
const configModule = await import(pathToFileURL(configPath).href);
|
|
296
|
-
if (!configModule.default || Object.keys(configModule.default).length === 0) {
|
|
297
|
-
throw new Error(`Config found, but default export is undefined`);
|
|
298
|
-
}
|
|
299
|
-
const userConfig = configModule.default || {};
|
|
300
|
-
const tn = (userConfig.tagName || "").toLowerCase().replace(/[-_\s]/g, "");
|
|
301
|
-
if (tn.includes("langtag")) {
|
|
302
|
-
throw new Error('Custom tagName cannot include "langtag"! (It is not recommended for use with libraries)\n');
|
|
303
|
-
}
|
|
304
|
-
return {
|
|
305
|
-
...LANG_TAG_DEFAULT_CONFIG,
|
|
306
|
-
...userConfig,
|
|
307
|
-
import: {
|
|
308
|
-
...LANG_TAG_DEFAULT_CONFIG.import,
|
|
309
|
-
...userConfig.import
|
|
310
|
-
},
|
|
311
|
-
collect: {
|
|
312
|
-
...LANG_TAG_DEFAULT_CONFIG.collect,
|
|
313
|
-
...userConfig.collect
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
} catch (error) {
|
|
317
|
-
throw error;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
async function $LT_EnsureDirectoryExists(filePath) {
|
|
321
|
-
await mkdir(filePath, { recursive: true });
|
|
322
|
-
}
|
|
323
|
-
async function $LT_RemoveDirectory(dirPath) {
|
|
324
|
-
try {
|
|
325
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
326
|
-
} catch (error) {
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
async function $LT_WriteJSON(filePath, data) {
|
|
330
|
-
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
331
|
-
}
|
|
332
|
-
async function $LT_ReadJSON(filePath) {
|
|
333
|
-
const content = await readFile(filePath, "utf-8");
|
|
334
|
-
return JSON.parse(content);
|
|
335
|
-
}
|
|
336
|
-
async function $LT_WriteFileWithDirs(filePath, content) {
|
|
337
|
-
const dir = dirname(filePath);
|
|
338
|
-
try {
|
|
339
|
-
await mkdir(dir, { recursive: true });
|
|
340
|
-
} catch (error) {
|
|
341
|
-
}
|
|
342
|
-
await writeFile(filePath, content, "utf-8");
|
|
343
|
-
}
|
|
344
|
-
async function $LT_ReadFileContent(relativeFilePath) {
|
|
345
|
-
const cwd = process.cwd();
|
|
346
|
-
const absolutePath = resolve$1(cwd, relativeFilePath);
|
|
347
|
-
return await readFile(absolutePath, "utf-8");
|
|
348
|
-
}
|
|
349
|
-
function parseObjectAST(code) {
|
|
350
|
-
const nodes = [];
|
|
351
|
-
try {
|
|
352
|
-
let walk = function(node, path2 = []) {
|
|
353
|
-
if (node.type === "Property" && node.key) {
|
|
354
|
-
const keyName = node.key.type === "Identifier" ? node.key.name : node.key.type === "Literal" ? node.key.value : null;
|
|
355
|
-
if (keyName) {
|
|
356
|
-
const currentPath = [...path2, keyName];
|
|
357
|
-
nodes.push({
|
|
358
|
-
type: "key",
|
|
359
|
-
start: node.key.start - 1,
|
|
360
|
-
// -1 for wrapper '('
|
|
361
|
-
end: node.key.end - 1,
|
|
362
|
-
value: keyName,
|
|
363
|
-
line: node.key.loc.start.line,
|
|
364
|
-
column: node.key.loc.start.column,
|
|
365
|
-
path: currentPath
|
|
366
|
-
});
|
|
367
|
-
if (node.value && node.value.type === "Literal") {
|
|
368
|
-
nodes.push({
|
|
369
|
-
type: "value",
|
|
370
|
-
start: node.value.start - 1,
|
|
371
|
-
// -1 for wrapper '('
|
|
372
|
-
end: node.value.end - 1,
|
|
373
|
-
value: node.value.value,
|
|
374
|
-
line: node.value.loc.start.line,
|
|
375
|
-
column: node.value.loc.start.column
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
if (node.value && node.value.type === "ObjectExpression") {
|
|
379
|
-
walk(node.value, currentPath);
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
for (const key in node) {
|
|
385
|
-
if (node[key] && typeof node[key] === "object") {
|
|
386
|
-
if (Array.isArray(node[key])) {
|
|
387
|
-
node[key].forEach((child) => walk(child, path2));
|
|
388
|
-
} else {
|
|
389
|
-
walk(node[key], path2);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
const ast = acorn.parse(`(${code})`, {
|
|
395
|
-
ecmaVersion: "latest",
|
|
396
|
-
locations: true
|
|
397
|
-
});
|
|
398
|
-
walk(ast);
|
|
399
|
-
for (let i = 0; i < code.length; i++) {
|
|
400
|
-
const char = code[i];
|
|
401
|
-
if (/[{}[\](),:]/.test(char)) {
|
|
402
|
-
const isCovered = nodes.some((n) => i >= n.start && i < n.end);
|
|
403
|
-
if (!isCovered) {
|
|
404
|
-
const nodeType = char === ":" ? "colon" : "bracket";
|
|
405
|
-
nodes.push({
|
|
406
|
-
type: nodeType,
|
|
407
|
-
start: i,
|
|
408
|
-
end: i + 1,
|
|
409
|
-
value: char,
|
|
410
|
-
line: 1,
|
|
411
|
-
// Will be calculated properly
|
|
412
|
-
column: i + 1
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
nodes.sort((a, b) => a.start - b.start);
|
|
418
|
-
} catch (error) {
|
|
419
|
-
nodes.push({
|
|
420
|
-
type: "error",
|
|
421
|
-
start: 0,
|
|
422
|
-
end: code.length,
|
|
423
|
-
value: code,
|
|
424
|
-
line: 1,
|
|
425
|
-
column: 1
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
return nodes;
|
|
429
|
-
}
|
|
430
|
-
function markConflictNodes(nodes, conflictPath) {
|
|
431
|
-
const conflictKeys = conflictPath.split(".");
|
|
432
|
-
return nodes.map((node) => {
|
|
433
|
-
if (node.type === "key" && node.path) {
|
|
434
|
-
const isConflict = conflictKeys.length > 0 && node.path.length <= conflictKeys.length && node.path.every((key, idx) => key === conflictKeys[idx]);
|
|
435
|
-
if (isConflict) {
|
|
436
|
-
return { ...node, type: "error" };
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return node;
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
const ANSI$1 = {
|
|
443
|
-
reset: "\x1B[0m",
|
|
444
|
-
white: "\x1B[97m",
|
|
445
|
-
brightCyan: "\x1B[96m",
|
|
446
|
-
green: "\x1B[92m",
|
|
447
|
-
gray: "\x1B[90m",
|
|
448
|
-
redBg: "\x1B[41m\x1B[97m",
|
|
449
|
-
bold: "\x1B[1m"
|
|
450
|
-
};
|
|
451
|
-
function colorizeFromAST(code, nodes) {
|
|
452
|
-
const ranges = [];
|
|
453
|
-
for (const node of nodes) {
|
|
454
|
-
const color = getColorForNodeType(node.type);
|
|
455
|
-
const priority = getPriorityForNodeType(node.type);
|
|
456
|
-
ranges.push({
|
|
457
|
-
start: node.start,
|
|
458
|
-
end: node.end,
|
|
459
|
-
color,
|
|
460
|
-
priority
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
ranges.sort((a, b) => {
|
|
464
|
-
if (b.start !== a.start) return b.start - a.start;
|
|
465
|
-
return b.priority - a.priority;
|
|
466
|
-
});
|
|
467
|
-
let colorized = code;
|
|
468
|
-
for (const range of ranges) {
|
|
469
|
-
const before = colorized.substring(0, range.start);
|
|
470
|
-
const text = colorized.substring(range.start, range.end);
|
|
471
|
-
const after = colorized.substring(range.end);
|
|
472
|
-
colorized = before + range.color + text + ANSI$1.reset + after;
|
|
473
|
-
}
|
|
474
|
-
return colorized;
|
|
475
|
-
}
|
|
476
|
-
function getColorForNodeType(type) {
|
|
477
|
-
switch (type) {
|
|
478
|
-
case "key":
|
|
479
|
-
return ANSI$1.brightCyan;
|
|
480
|
-
case "bracket":
|
|
481
|
-
case "colon":
|
|
482
|
-
return ANSI$1.gray;
|
|
483
|
-
case "value":
|
|
484
|
-
return ANSI$1.green;
|
|
485
|
-
case "comment":
|
|
486
|
-
return ANSI$1.gray;
|
|
487
|
-
case "error":
|
|
488
|
-
return ANSI$1.bold + ANSI$1.redBg;
|
|
489
|
-
default:
|
|
490
|
-
return ANSI$1.white;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
function getPriorityForNodeType(type) {
|
|
494
|
-
switch (type) {
|
|
495
|
-
case "error":
|
|
496
|
-
return 3;
|
|
497
|
-
// Highest priority
|
|
498
|
-
case "key":
|
|
499
|
-
return 2;
|
|
500
|
-
case "value":
|
|
501
|
-
return 1;
|
|
502
|
-
case "bracket":
|
|
503
|
-
case "colon":
|
|
504
|
-
case "comment":
|
|
505
|
-
return 0;
|
|
506
|
-
// Lowest priority
|
|
507
|
-
default:
|
|
508
|
-
return 0;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
const ANSI = {
|
|
512
|
-
reset: "\x1B[0m",
|
|
513
|
-
white: "\x1B[97m",
|
|
514
|
-
cyan: "\x1B[96m",
|
|
515
|
-
gray: "\x1B[37m",
|
|
516
|
-
darkGray: "\x1B[90m",
|
|
517
|
-
bold: "\x1B[1m"
|
|
518
|
-
};
|
|
519
|
-
function getVisibleLines(totalLines, errorLines, threshold = 10) {
|
|
520
|
-
if (totalLines <= threshold) {
|
|
521
|
-
return null;
|
|
522
|
-
}
|
|
523
|
-
const visible = /* @__PURE__ */ new Set();
|
|
524
|
-
const contextLines = 2;
|
|
525
|
-
visible.add(0);
|
|
526
|
-
visible.add(1);
|
|
527
|
-
visible.add(totalLines - 2);
|
|
528
|
-
visible.add(totalLines - 1);
|
|
529
|
-
for (const errorLine of errorLines) {
|
|
530
|
-
for (let i = Math.max(0, errorLine - contextLines); i <= Math.min(totalLines - 1, errorLine + contextLines); i++) {
|
|
531
|
-
visible.add(i);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
return visible;
|
|
535
|
-
}
|
|
536
|
-
function printLines(lines, startLineNumber, errorLines = /* @__PURE__ */ new Set(), condense = false) {
|
|
537
|
-
const visibleLines = condense ? getVisibleLines(lines.length, errorLines) : null;
|
|
538
|
-
if (visibleLines === null) {
|
|
539
|
-
lines.forEach((line, i) => {
|
|
540
|
-
const lineNumber = startLineNumber + i;
|
|
541
|
-
const lineNumStr = String(lineNumber).padStart(3, " ");
|
|
542
|
-
console.log(`${ANSI.gray}${lineNumStr}${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${line}`);
|
|
543
|
-
});
|
|
544
|
-
} else {
|
|
545
|
-
let lastPrinted = -2;
|
|
546
|
-
lines.forEach((line, i) => {
|
|
547
|
-
if (visibleLines.has(i)) {
|
|
548
|
-
if (i > lastPrinted + 1 && lastPrinted >= 0) {
|
|
549
|
-
console.log(`${ANSI.gray} -${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${ANSI.gray}...${ANSI.reset}`);
|
|
550
|
-
}
|
|
551
|
-
const lineNumber = startLineNumber + i;
|
|
552
|
-
const lineNumStr = String(lineNumber).padStart(3, " ");
|
|
553
|
-
console.log(`${ANSI.gray}${lineNumStr}${ANSI.reset} ${ANSI.darkGray}│${ANSI.reset} ${line}`);
|
|
554
|
-
lastPrinted = i;
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
async function getLangTagCodeSection(tagInfo) {
|
|
560
|
-
const { tag, relativeFilePath } = tagInfo;
|
|
561
|
-
const fileContent = await $LT_ReadFileContent(relativeFilePath);
|
|
562
|
-
const fileLines = fileContent.split("\n");
|
|
563
|
-
const startLine = tag.line;
|
|
564
|
-
const endLine = tag.line + tag.fullMatch.split("\n").length - 1;
|
|
565
|
-
return fileLines.slice(startLine - 1, endLine).join("\n");
|
|
566
|
-
}
|
|
567
|
-
function stripPrefix(str, prefix) {
|
|
568
|
-
if (!prefix) return str;
|
|
569
|
-
if (str.startsWith(prefix)) {
|
|
570
|
-
if (!prefix.endsWith(".")) {
|
|
571
|
-
prefix += ".";
|
|
572
|
-
}
|
|
573
|
-
return str.slice(prefix.length);
|
|
574
|
-
}
|
|
575
|
-
return str;
|
|
576
|
-
}
|
|
577
|
-
function getErrorLineNumbers(code, nodes) {
|
|
578
|
-
const errorLines = /* @__PURE__ */ new Set();
|
|
579
|
-
const lines = code.split("\n");
|
|
580
|
-
const errorNodes = nodes.filter((n) => n.type === "error");
|
|
581
|
-
for (const errorNode of errorNodes) {
|
|
582
|
-
let currentPos = 0;
|
|
583
|
-
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
584
|
-
const lineLength = lines[lineIndex].length;
|
|
585
|
-
const lineEnd = currentPos + lineLength;
|
|
586
|
-
if (errorNode.start < lineEnd && errorNode.end > currentPos) {
|
|
587
|
-
errorLines.add(lineIndex);
|
|
588
|
-
}
|
|
589
|
-
currentPos = lineEnd + 1;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
return errorLines;
|
|
593
|
-
}
|
|
594
|
-
async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgPosition, condense) {
|
|
595
|
-
const { tag } = tagInfo;
|
|
596
|
-
const filePath = path.join(process.cwd(), tagInfo.relativeFilePath);
|
|
597
|
-
let lineNum = tagInfo.tag.line;
|
|
598
|
-
try {
|
|
599
|
-
const startLine = tag.line;
|
|
600
|
-
const wholeTagCode = await getLangTagCodeSection(tagInfo);
|
|
601
|
-
const translationTagCode = translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text;
|
|
602
|
-
const configTagCode = translationArgPosition === 1 ? tag.parameter2Text : tag.parameter1Text;
|
|
603
|
-
const translationErrorPath = stripPrefix(conflictPath, tag.parameterConfig?.path);
|
|
604
|
-
let colorizedWhole = wholeTagCode;
|
|
605
|
-
let errorLines = /* @__PURE__ */ new Set();
|
|
606
|
-
if (translationTagCode) {
|
|
607
|
-
try {
|
|
608
|
-
const translationNodes = parseObjectAST(translationTagCode);
|
|
609
|
-
const markedTranslationNodes = translationErrorPath ? markConflictNodes(translationNodes, translationErrorPath) : translationNodes;
|
|
610
|
-
const translationErrorLines = getErrorLineNumbers(translationTagCode, markedTranslationNodes);
|
|
611
|
-
const translationStartInWhole = wholeTagCode.indexOf(translationTagCode);
|
|
612
|
-
if (translationStartInWhole >= 0) {
|
|
613
|
-
const linesBeforeTranslation = wholeTagCode.substring(0, translationStartInWhole).split("\n").length - 1;
|
|
614
|
-
translationErrorLines.forEach((lineNum2) => {
|
|
615
|
-
errorLines.add(linesBeforeTranslation + lineNum2);
|
|
616
|
-
});
|
|
617
|
-
if (translationErrorLines.size > 0) {
|
|
618
|
-
const lastTranslationErrorLine = Math.max(...Array.from(translationErrorLines));
|
|
619
|
-
lineNum = startLine + linesBeforeTranslation + lastTranslationErrorLine;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
const colorizedTranslation = colorizeFromAST(translationTagCode, markedTranslationNodes);
|
|
623
|
-
colorizedWhole = colorizedWhole.replace(translationTagCode, colorizedTranslation);
|
|
624
|
-
} catch (error) {
|
|
625
|
-
console.error("Failed to colorize translation:", error);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
if (configTagCode) {
|
|
629
|
-
try {
|
|
630
|
-
const configNodes = parseObjectAST(configTagCode);
|
|
631
|
-
let pathKeyFound = false;
|
|
632
|
-
const markedConfigNodes = configNodes.map((node, index) => {
|
|
633
|
-
if (node.type === "key" && node.value === "path") {
|
|
634
|
-
pathKeyFound = true;
|
|
635
|
-
return node;
|
|
636
|
-
}
|
|
637
|
-
if (pathKeyFound && node.type === "value") {
|
|
638
|
-
if (conflictPath.startsWith(node.value + ".")) {
|
|
639
|
-
pathKeyFound = false;
|
|
640
|
-
return { ...node, type: "error" };
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
return node;
|
|
644
|
-
});
|
|
645
|
-
const configErrorLines = getErrorLineNumbers(configTagCode, markedConfigNodes);
|
|
646
|
-
const configStartInWhole = wholeTagCode.indexOf(configTagCode);
|
|
647
|
-
if (configStartInWhole >= 0) {
|
|
648
|
-
const linesBeforeConfig = wholeTagCode.substring(0, configStartInWhole).split("\n").length - 1;
|
|
649
|
-
configErrorLines.forEach((lineNum2) => {
|
|
650
|
-
errorLines.add(linesBeforeConfig + lineNum2);
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
const colorizedConfig = colorizeFromAST(configTagCode, markedConfigNodes);
|
|
654
|
-
colorizedWhole = colorizedWhole.replace(configTagCode, colorizedConfig);
|
|
655
|
-
} catch (error) {
|
|
656
|
-
console.error("Failed to colorize config:", error);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
console.log(`${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${filePath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`);
|
|
660
|
-
printLines(colorizedWhole.split("\n"), startLine, errorLines, condense);
|
|
661
|
-
} catch (error) {
|
|
662
|
-
console.error("Error displaying conflict:", error);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
async function $LT_LogConflict(conflict, translationArgPosition, condense) {
|
|
666
|
-
const { path: conflictPath, tagA, tagB } = conflict;
|
|
667
|
-
await logTagConflictInfo(tagA, "between", conflictPath, translationArgPosition, condense);
|
|
668
|
-
await logTagConflictInfo(tagB, "and", conflictPath, translationArgPosition, condense);
|
|
669
|
-
}
|
|
670
|
-
const ANSI_COLORS = {
|
|
671
|
-
reset: "\x1B[0m",
|
|
672
|
-
white: "\x1B[37m",
|
|
673
|
-
blue: "\x1B[34m",
|
|
674
|
-
green: "\x1B[32m",
|
|
675
|
-
yellow: "\x1B[33m",
|
|
676
|
-
red: "\x1B[31m",
|
|
677
|
-
gray: "\x1B[90m",
|
|
678
|
-
bold: "\x1B[1m",
|
|
679
|
-
cyan: "\x1B[36m"
|
|
680
|
-
};
|
|
681
|
-
function validateAndInterpolate(message, params) {
|
|
682
|
-
const placeholders = Array.from(message.matchAll(/\{(\w+)\}/g)).map((m) => m[1]);
|
|
683
|
-
const missing = placeholders.filter((p) => !(p in (params || {})));
|
|
684
|
-
if (missing.length) {
|
|
685
|
-
throw new Error(`Missing variables in message: ${missing.join(", ")}`);
|
|
686
|
-
}
|
|
687
|
-
const extra = params ? Object.keys(params).filter((k) => !placeholders.includes(k)) : [];
|
|
688
|
-
if (extra.length) {
|
|
689
|
-
throw new Error(`Extra variables provided not used in message: ${extra.join(", ")}`);
|
|
690
|
-
}
|
|
691
|
-
const parts = [];
|
|
692
|
-
let lastIndex = 0;
|
|
693
|
-
for (const match of message.matchAll(/\{(\w+)\}/g)) {
|
|
694
|
-
const [fullMatch, key] = match;
|
|
695
|
-
const index = match.index;
|
|
696
|
-
if (index > lastIndex) {
|
|
697
|
-
parts.push({ text: message.slice(lastIndex, index), isVar: false });
|
|
698
|
-
}
|
|
699
|
-
parts.push({ text: String(params[key]), isVar: true });
|
|
700
|
-
lastIndex = index + fullMatch.length;
|
|
701
|
-
}
|
|
702
|
-
if (lastIndex < message.length) {
|
|
703
|
-
parts.push({ text: message.slice(lastIndex), isVar: false });
|
|
704
|
-
}
|
|
705
|
-
return parts;
|
|
706
|
-
}
|
|
707
|
-
function log(baseColor, message, params) {
|
|
708
|
-
const parts = validateAndInterpolate(message, params);
|
|
709
|
-
const coloredMessage = parts.map(
|
|
710
|
-
(p) => p.isVar ? `${ANSI_COLORS.bold}${ANSI_COLORS.white}${p.text}${ANSI_COLORS.reset}${baseColor}` : p.text
|
|
711
|
-
).join("");
|
|
712
|
-
const now = /* @__PURE__ */ new Date();
|
|
713
|
-
const time = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}`;
|
|
714
|
-
const prefix = (
|
|
715
|
-
// Static colored "LangTag" prefix
|
|
716
|
-
//`${ANSI_COLORS.bold}${ANSI_COLORS.cyan}LangTag${ANSI_COLORS.reset} ` +
|
|
717
|
-
// Time
|
|
718
|
-
`${ANSI_COLORS.gray}[${time}]${ANSI_COLORS.reset} `
|
|
719
|
-
);
|
|
720
|
-
console.log(`${prefix}${baseColor}${coloredMessage}${ANSI_COLORS.reset}`);
|
|
721
|
-
}
|
|
722
|
-
function $LT_CreateDefaultLogger(debugMode, translationArgPosition = 1) {
|
|
723
|
-
return {
|
|
724
|
-
info: (msg, params) => log(ANSI_COLORS.blue, msg, params),
|
|
725
|
-
success: (msg, params) => log(ANSI_COLORS.green, msg, params),
|
|
726
|
-
warn: (msg, params) => log(ANSI_COLORS.yellow, msg, params),
|
|
727
|
-
error: (msg, params) => log(ANSI_COLORS.red, msg || "empty error message", params),
|
|
728
|
-
debug: (msg, params) => {
|
|
729
|
-
if (!debugMode) return;
|
|
730
|
-
log(ANSI_COLORS.gray, msg, params);
|
|
731
|
-
},
|
|
732
|
-
conflict: async (conflict, condense) => {
|
|
733
|
-
const { path: path2, conflictType, tagA } = conflict;
|
|
734
|
-
console.log();
|
|
735
|
-
console.log(`${ANSI_COLORS.bold}${ANSI_COLORS.red}⚠ Translation Conflict Detected${ANSI_COLORS.reset}`);
|
|
736
|
-
console.log(`${ANSI_COLORS.gray}${"─".repeat(60)}${ANSI_COLORS.reset}`);
|
|
737
|
-
console.log(` ${ANSI_COLORS.cyan}Conflict Type:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${conflictType}${ANSI_COLORS.reset}`);
|
|
738
|
-
console.log(` ${ANSI_COLORS.cyan}Translation Key:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${path2}${ANSI_COLORS.reset}`);
|
|
739
|
-
console.log(` ${ANSI_COLORS.cyan}Namespace:${ANSI_COLORS.reset} ${ANSI_COLORS.white}${tagA.tag.parameterConfig.namespace}${ANSI_COLORS.reset}`);
|
|
740
|
-
console.log(`${ANSI_COLORS.gray}${"─".repeat(60)}${ANSI_COLORS.reset}`);
|
|
741
|
-
await $LT_LogConflict(conflict, translationArgPosition, condense);
|
|
742
|
-
console.log();
|
|
743
|
-
}
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
async function $LT_GetCommandEssentials() {
|
|
747
|
-
const config = await $LT_ReadConfig(process__default.cwd());
|
|
748
|
-
const logger = $LT_CreateDefaultLogger(config.debug, config.translationArgPosition);
|
|
749
|
-
return {
|
|
750
|
-
config,
|
|
751
|
-
logger
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
async function $LT_CMD_RegenerateTags() {
|
|
755
|
-
const { config, logger } = await $LT_GetCommandEssentials();
|
|
756
|
-
const files = await globby(config.includes, {
|
|
757
|
-
cwd: process.cwd(),
|
|
758
|
-
ignore: config.excludes,
|
|
759
|
-
absolute: true
|
|
760
|
-
});
|
|
761
|
-
const charactersToSkip = process.cwd().length + 1;
|
|
762
|
-
let dirty = false;
|
|
763
|
-
for (const file of files) {
|
|
764
|
-
const path2 = file.substring(charactersToSkip);
|
|
765
|
-
const localDirty = await checkAndRegenerateFileLangTags(config, logger, file, path2);
|
|
766
|
-
if (localDirty) {
|
|
767
|
-
logger.info('Lang tag configurations written for file "{path}"', { path: path2 });
|
|
768
|
-
dirty = true;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
if (!dirty) {
|
|
772
|
-
logger.info("No changes were made based on the current configuration and files");
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
function deepMergeTranslations(target, source) {
|
|
776
|
-
if (typeof target !== "object") {
|
|
777
|
-
throw new Error("Target must be an object");
|
|
778
|
-
}
|
|
779
|
-
if (typeof source !== "object") {
|
|
780
|
-
throw new Error("Source must be an object");
|
|
781
|
-
}
|
|
782
|
-
let changed = false;
|
|
783
|
-
for (const key in source) {
|
|
784
|
-
if (!source.hasOwnProperty(key)) {
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
let targetValue = target[key];
|
|
788
|
-
const sourceValue = source[key];
|
|
789
|
-
if (typeof targetValue === "string" && typeof sourceValue === "object") {
|
|
790
|
-
throw new Error(`Trying to write object into target key "${key}" which is translation already`);
|
|
791
|
-
}
|
|
792
|
-
if (Array.isArray(sourceValue)) {
|
|
793
|
-
throw new Error(`Trying to write array into target key "${key}", we do not allow arrays inside translations`);
|
|
794
|
-
}
|
|
795
|
-
if (typeof sourceValue === "object") {
|
|
796
|
-
if (!targetValue) {
|
|
797
|
-
targetValue = {};
|
|
798
|
-
target[key] = targetValue;
|
|
799
|
-
}
|
|
800
|
-
if (deepMergeTranslations(targetValue, sourceValue)) {
|
|
801
|
-
changed = true;
|
|
802
|
-
}
|
|
803
|
-
} else {
|
|
804
|
-
if (target[key] !== sourceValue) {
|
|
805
|
-
changed = true;
|
|
806
|
-
}
|
|
807
|
-
target[key] = sourceValue;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
return changed;
|
|
811
|
-
}
|
|
812
|
-
async function $LT_WriteToNamespaces({ config, namespaces, logger, clean }) {
|
|
813
|
-
const changedNamespaces = [];
|
|
814
|
-
if (clean) {
|
|
815
|
-
logger.info("Cleaning output directory...");
|
|
816
|
-
await $LT_RemoveDirectory(config.outputDir);
|
|
817
|
-
}
|
|
818
|
-
await $LT_EnsureDirectoryExists(config.outputDir);
|
|
819
|
-
for (let namespace of Object.keys(namespaces)) {
|
|
820
|
-
if (!namespace) {
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
const filePath = resolve(
|
|
824
|
-
process__default.cwd(),
|
|
825
|
-
config.outputDir,
|
|
826
|
-
namespace + ".json"
|
|
827
|
-
);
|
|
828
|
-
let originalJSON = {};
|
|
829
|
-
try {
|
|
830
|
-
originalJSON = await $LT_ReadJSON(filePath);
|
|
831
|
-
} catch (e) {
|
|
832
|
-
if (!clean) {
|
|
833
|
-
logger.warn(`Original namespace file "{namespace}.json" not found. A new one will be created.`, { namespace });
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
if (deepMergeTranslations(originalJSON, namespaces[namespace])) {
|
|
837
|
-
changedNamespaces.push(namespace);
|
|
838
|
-
await $LT_WriteJSON(filePath, originalJSON);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
return changedNamespaces;
|
|
842
|
-
}
|
|
843
|
-
async function $LT_CollectCandidateFilesWithTags(props) {
|
|
844
|
-
const { config, logger } = props;
|
|
845
|
-
const processor = new $LT_TagProcessor(config);
|
|
846
|
-
const cwd = process__default.cwd();
|
|
847
|
-
let filesToScan = props.filesToScan;
|
|
848
|
-
if (!filesToScan) {
|
|
849
|
-
filesToScan = await globby(config.includes, { cwd, ignore: config.excludes, absolute: true });
|
|
850
|
-
}
|
|
851
|
-
const candidates = [];
|
|
852
|
-
for (const filePath of filesToScan) {
|
|
853
|
-
const fileContent = readFileSync(filePath, "utf-8");
|
|
854
|
-
let tags = processor.extractTags(fileContent);
|
|
855
|
-
if (!tags.length) {
|
|
856
|
-
continue;
|
|
857
|
-
}
|
|
858
|
-
tags = $LT_FilterInvalidTags(tags, config, logger);
|
|
859
|
-
if (!tags.length) {
|
|
860
|
-
continue;
|
|
861
|
-
}
|
|
862
|
-
for (let tag of tags) {
|
|
863
|
-
tag.parameterConfig = config.collect.onCollectConfigFix({ config: tag.parameterConfig, langTagConfig: config });
|
|
864
|
-
}
|
|
865
|
-
tags = $LT_FilterEmptyNamespaceTags(tags, logger);
|
|
866
|
-
const relativeFilePath = path__default.relative(cwd, filePath);
|
|
867
|
-
candidates.push({ relativeFilePath, tags });
|
|
868
|
-
}
|
|
869
|
-
return candidates;
|
|
870
|
-
}
|
|
871
|
-
async function $LT_WriteAsExportFile({ config, logger, files }) {
|
|
872
|
-
const packageJson = await $LT_ReadJSON(path__default.resolve(process.cwd(), "package.json"));
|
|
873
|
-
if (!packageJson) {
|
|
874
|
-
throw new Error("package.json not found");
|
|
875
|
-
}
|
|
876
|
-
const langTagFiles = {};
|
|
877
|
-
for (const file of files) {
|
|
878
|
-
langTagFiles[file.relativeFilePath] = {
|
|
879
|
-
matches: file.tags.map((tag) => {
|
|
880
|
-
let T = config.translationArgPosition === 1 ? tag.parameter1Text : tag.parameter2Text;
|
|
881
|
-
let C = config.translationArgPosition === 1 ? tag.parameter2Text : tag.parameter1Text;
|
|
882
|
-
if (!T) T = "{}";
|
|
883
|
-
if (!C) C = "{}";
|
|
884
|
-
return {
|
|
885
|
-
translations: T,
|
|
886
|
-
config: C,
|
|
887
|
-
variableName: tag.variableName
|
|
888
|
-
};
|
|
889
|
-
})
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
const data = {
|
|
893
|
-
language: config.language,
|
|
894
|
-
packageName: packageJson.name || "",
|
|
895
|
-
files: langTagFiles
|
|
896
|
-
};
|
|
897
|
-
await $LT_WriteJSON(EXPORTS_FILE_NAME, data);
|
|
898
|
-
logger.success(`Written {file}`, { file: EXPORTS_FILE_NAME });
|
|
899
|
-
}
|
|
900
|
-
async function $LT_GroupTagsToNamespaces({ logger, files, config }) {
|
|
901
|
-
let totalTags = 0;
|
|
902
|
-
const namespaces = {};
|
|
903
|
-
function getTranslations(namespace) {
|
|
904
|
-
const namespaceTranslations = namespaces[namespace] || {};
|
|
905
|
-
if (!(namespace in namespaces)) {
|
|
906
|
-
namespaces[namespace] = namespaceTranslations;
|
|
907
|
-
}
|
|
908
|
-
return namespaceTranslations;
|
|
909
|
-
}
|
|
910
|
-
const allConflicts = [];
|
|
911
|
-
const existingValuesByNamespace = /* @__PURE__ */ new Map();
|
|
912
|
-
for (const file of files) {
|
|
913
|
-
totalTags += file.tags.length;
|
|
914
|
-
for (const tag of file.tags) {
|
|
915
|
-
const tagConfig = tag.parameterConfig;
|
|
916
|
-
const namespaceTranslations = getTranslations(tagConfig.namespace);
|
|
917
|
-
let existingValues = existingValuesByNamespace.get(tagConfig.namespace);
|
|
918
|
-
if (!existingValues) {
|
|
919
|
-
existingValues = /* @__PURE__ */ new Map();
|
|
920
|
-
existingValuesByNamespace.set(tagConfig.namespace, existingValues);
|
|
921
|
-
}
|
|
922
|
-
const valueTracker = {
|
|
923
|
-
get: (path2) => existingValues.get(path2),
|
|
924
|
-
trackValue: (path2, value) => {
|
|
925
|
-
existingValues.set(path2, { tag, relativeFilePath: file.relativeFilePath, value });
|
|
926
|
-
}
|
|
927
|
-
};
|
|
928
|
-
const addConflict = async (path2, tagA, tagBValue, conflictType) => {
|
|
929
|
-
if (conflictType === "path_overwrite" && config.collect?.ignoreConflictsWithMatchingValues !== false && tagA.value === tagBValue) {
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
const conflict = {
|
|
933
|
-
path: path2,
|
|
934
|
-
tagA,
|
|
935
|
-
tagB: {
|
|
936
|
-
tag,
|
|
937
|
-
relativeFilePath: file.relativeFilePath,
|
|
938
|
-
value: tagBValue
|
|
939
|
-
},
|
|
940
|
-
conflictType
|
|
941
|
-
};
|
|
942
|
-
if (config.collect?.onConflictResolution) {
|
|
943
|
-
let shouldContinue = true;
|
|
944
|
-
await config.collect.onConflictResolution({
|
|
945
|
-
conflict,
|
|
946
|
-
logger,
|
|
947
|
-
exit() {
|
|
948
|
-
shouldContinue = false;
|
|
949
|
-
}
|
|
950
|
-
});
|
|
951
|
-
if (!shouldContinue) {
|
|
952
|
-
throw new Error(`LangTagConflictResolution:Processing stopped due to conflict resolution: ${conflict.tagA.tag.parameterConfig.namespace}|${conflict.path}`);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
allConflicts.push(conflict);
|
|
956
|
-
};
|
|
957
|
-
const target = await ensureNestedObject(
|
|
958
|
-
tagConfig.path,
|
|
959
|
-
namespaceTranslations,
|
|
960
|
-
valueTracker,
|
|
961
|
-
addConflict
|
|
962
|
-
);
|
|
963
|
-
await mergeWithConflictDetection(
|
|
964
|
-
target,
|
|
965
|
-
tag.parameterTranslations,
|
|
966
|
-
tagConfig.path || "",
|
|
967
|
-
valueTracker,
|
|
968
|
-
addConflict
|
|
969
|
-
);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
if (allConflicts.length > 0) {
|
|
973
|
-
logger.warn(`Found ${allConflicts.length} conflicts.`);
|
|
974
|
-
if (config.collect?.onCollectFinish) {
|
|
975
|
-
let shouldContinue = true;
|
|
976
|
-
config.collect.onCollectFinish({
|
|
977
|
-
conflicts: allConflicts,
|
|
978
|
-
logger,
|
|
979
|
-
exit() {
|
|
980
|
-
shouldContinue = false;
|
|
981
|
-
}
|
|
982
|
-
});
|
|
983
|
-
if (!shouldContinue) {
|
|
984
|
-
throw new Error(`LangTagConflictResolution:Processing stopped due to collect finish handler`);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
return namespaces;
|
|
989
|
-
}
|
|
990
|
-
async function ensureNestedObject(path2, root, valueTracker, addConflict) {
|
|
991
|
-
if (!path2 || !path2.trim()) return root;
|
|
992
|
-
let current = root;
|
|
993
|
-
let currentPath = "";
|
|
994
|
-
for (const key of path2.split(".")) {
|
|
995
|
-
currentPath = currentPath ? `${currentPath}.${key}` : key;
|
|
996
|
-
if (current[key] !== void 0 && typeof current[key] !== "object") {
|
|
997
|
-
const existingInfo = valueTracker.get(currentPath);
|
|
998
|
-
if (existingInfo) {
|
|
999
|
-
await addConflict(currentPath, existingInfo, {}, "type_mismatch");
|
|
1000
|
-
}
|
|
1001
|
-
return current;
|
|
1002
|
-
}
|
|
1003
|
-
current[key] = current[key] || {};
|
|
1004
|
-
current = current[key];
|
|
1005
|
-
}
|
|
1006
|
-
return current;
|
|
1007
|
-
}
|
|
1008
|
-
async function mergeWithConflictDetection(target, source, basePath = "", valueTracker, addConflict) {
|
|
1009
|
-
if (typeof target !== "object" || typeof source !== "object") {
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
for (const key in source) {
|
|
1013
|
-
if (!source.hasOwnProperty(key)) {
|
|
1014
|
-
continue;
|
|
1015
|
-
}
|
|
1016
|
-
const currentPath = basePath ? `${basePath}.${key}` : key;
|
|
1017
|
-
let targetValue = target[key];
|
|
1018
|
-
const sourceValue = source[key];
|
|
1019
|
-
if (Array.isArray(sourceValue)) {
|
|
1020
|
-
continue;
|
|
1021
|
-
}
|
|
1022
|
-
if (targetValue !== void 0) {
|
|
1023
|
-
const targetType = typeof targetValue;
|
|
1024
|
-
const sourceType = typeof sourceValue;
|
|
1025
|
-
let existingInfo = valueTracker.get(currentPath);
|
|
1026
|
-
if (!existingInfo && targetType === "object" && targetValue !== null && !Array.isArray(targetValue)) {
|
|
1027
|
-
const findNestedInfo = (obj, prefix) => {
|
|
1028
|
-
for (const key2 in obj) {
|
|
1029
|
-
const path2 = prefix ? `${prefix}.${key2}` : key2;
|
|
1030
|
-
const info = valueTracker.get(path2);
|
|
1031
|
-
if (info) {
|
|
1032
|
-
return info;
|
|
1033
|
-
}
|
|
1034
|
-
if (typeof obj[key2] === "object" && obj[key2] !== null && !Array.isArray(obj[key2])) {
|
|
1035
|
-
const nestedInfo = findNestedInfo(obj[key2], path2);
|
|
1036
|
-
if (nestedInfo) {
|
|
1037
|
-
return nestedInfo;
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
return void 0;
|
|
1042
|
-
};
|
|
1043
|
-
existingInfo = findNestedInfo(targetValue, currentPath);
|
|
1044
|
-
}
|
|
1045
|
-
if (targetType !== sourceType) {
|
|
1046
|
-
if (existingInfo) {
|
|
1047
|
-
await addConflict(currentPath, existingInfo, sourceValue, "type_mismatch");
|
|
1048
|
-
}
|
|
1049
|
-
continue;
|
|
1050
|
-
}
|
|
1051
|
-
if (targetType !== "object") {
|
|
1052
|
-
if (existingInfo) {
|
|
1053
|
-
await addConflict(currentPath, existingInfo, sourceValue, "path_overwrite");
|
|
1054
|
-
}
|
|
1055
|
-
if (targetValue !== sourceValue) {
|
|
1056
|
-
continue;
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
if (typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue)) {
|
|
1061
|
-
if (!targetValue) {
|
|
1062
|
-
targetValue = {};
|
|
1063
|
-
target[key] = targetValue;
|
|
1064
|
-
}
|
|
1065
|
-
await mergeWithConflictDetection(
|
|
1066
|
-
targetValue,
|
|
1067
|
-
sourceValue,
|
|
1068
|
-
currentPath,
|
|
1069
|
-
valueTracker,
|
|
1070
|
-
addConflict
|
|
1071
|
-
);
|
|
1072
|
-
} else {
|
|
1073
|
-
target[key] = sourceValue;
|
|
1074
|
-
valueTracker.trackValue(currentPath, sourceValue);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
async function $LT_CMD_Collect(options) {
|
|
1079
|
-
const { config, logger } = await $LT_GetCommandEssentials();
|
|
1080
|
-
logger.info("Collecting translations from source files...");
|
|
1081
|
-
const files = await $LT_CollectCandidateFilesWithTags({ config, logger });
|
|
1082
|
-
if (config.isLibrary) {
|
|
1083
|
-
await $LT_WriteAsExportFile({ config, logger, files });
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
try {
|
|
1087
|
-
const namespaces = await $LT_GroupTagsToNamespaces({ logger, files, config });
|
|
1088
|
-
const totalTags = files.reduce((sum, file) => sum + file.tags.length, 0);
|
|
1089
|
-
logger.debug("Found {totalTags} translation tags", { totalTags });
|
|
1090
|
-
const changedNamespaces = await $LT_WriteToNamespaces({ config, namespaces, logger, clean: options?.clean });
|
|
1091
|
-
if (!changedNamespaces?.length) {
|
|
1092
|
-
logger.info("No changes were made based on the current configuration and files");
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
const n = changedNamespaces.map((n2) => `"${n2}.json"`).join(", ");
|
|
1096
|
-
logger.success("Updated namespaces {outputDir} ({namespaces})", {
|
|
1097
|
-
outputDir: config.outputDir,
|
|
1098
|
-
namespaces: n
|
|
1099
|
-
});
|
|
1100
|
-
} catch (e) {
|
|
1101
|
-
const prefix = "LangTagConflictResolution:";
|
|
1102
|
-
if (e.message.startsWith(prefix)) {
|
|
1103
|
-
logger.error(e.message.substring(prefix.length));
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
throw e;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
function getBasePath(pattern) {
|
|
1110
|
-
const globStartIndex = pattern.indexOf("*");
|
|
1111
|
-
const braceStartIndex = pattern.indexOf("{");
|
|
1112
|
-
let endIndex = -1;
|
|
1113
|
-
if (globStartIndex !== -1 && braceStartIndex !== -1) {
|
|
1114
|
-
endIndex = Math.min(globStartIndex, braceStartIndex);
|
|
1115
|
-
} else if (globStartIndex !== -1) {
|
|
1116
|
-
endIndex = globStartIndex;
|
|
1117
|
-
} else if (braceStartIndex !== -1) {
|
|
1118
|
-
endIndex = braceStartIndex;
|
|
1119
|
-
}
|
|
1120
|
-
if (endIndex === -1) {
|
|
1121
|
-
const lastSlashIndex = pattern.lastIndexOf("/");
|
|
1122
|
-
return lastSlashIndex !== -1 ? pattern.substring(0, lastSlashIndex) : ".";
|
|
1123
|
-
}
|
|
1124
|
-
const lastSeparatorIndex = pattern.lastIndexOf("/", endIndex);
|
|
1125
|
-
return lastSeparatorIndex === -1 ? "." : pattern.substring(0, lastSeparatorIndex);
|
|
1126
|
-
}
|
|
1127
|
-
function $LT_CreateChokidarWatcher(config) {
|
|
1128
|
-
const cwd = process.cwd();
|
|
1129
|
-
const baseDirsToWatch = [
|
|
1130
|
-
...new Set(config.includes.map((pattern) => getBasePath(pattern)))
|
|
1131
|
-
];
|
|
1132
|
-
const finalDirsToWatch = baseDirsToWatch.map((dir) => dir === "." ? cwd : dir);
|
|
1133
|
-
const ignored = [...config.excludes, "**/.git/**"];
|
|
1134
|
-
return chokidar.watch(finalDirsToWatch, {
|
|
1135
|
-
// Watch base directories
|
|
1136
|
-
cwd,
|
|
1137
|
-
ignored,
|
|
1138
|
-
persistent: true,
|
|
1139
|
-
ignoreInitial: true,
|
|
1140
|
-
awaitWriteFinish: {
|
|
1141
|
-
stabilityThreshold: 300,
|
|
1142
|
-
pollInterval: 100
|
|
1143
|
-
}
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
async function $LT_WatchTranslations() {
|
|
1147
|
-
const { config, logger } = await $LT_GetCommandEssentials();
|
|
1148
|
-
await $LT_CMD_Collect();
|
|
1149
|
-
const watcher = $LT_CreateChokidarWatcher(config);
|
|
1150
|
-
logger.info("Starting watch mode for translations...");
|
|
1151
|
-
logger.info("Watching for changes...");
|
|
1152
|
-
logger.info("Press Ctrl+C to stop watching");
|
|
1153
|
-
watcher.on("change", async (filePath) => await handleFile(config, logger, filePath)).on("add", async (filePath) => await handleFile(config, logger, filePath)).on("error", (error) => {
|
|
1154
|
-
logger.error("Error in file watcher: {error}", { error });
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
|
-
async function handleFile(config, logger, cwdRelativeFilePath, event) {
|
|
1158
|
-
if (!micromatch.isMatch(cwdRelativeFilePath, config.includes)) {
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
const cwd = process.cwd();
|
|
1162
|
-
const absoluteFilePath = path__default.join(cwd, cwdRelativeFilePath);
|
|
1163
|
-
const dirty = await checkAndRegenerateFileLangTags(config, logger, absoluteFilePath, cwdRelativeFilePath);
|
|
1164
|
-
if (dirty) {
|
|
1165
|
-
logger.info(`Lang tag configurations written for file "{filePath}"`, { filePath: cwdRelativeFilePath });
|
|
1166
|
-
}
|
|
1167
|
-
const files = await $LT_CollectCandidateFilesWithTags({ filesToScan: [cwdRelativeFilePath], config, logger });
|
|
1168
|
-
const namespaces = await $LT_GroupTagsToNamespaces({ logger, files, config });
|
|
1169
|
-
const changedNamespaces = await $LT_WriteToNamespaces({ config, namespaces, logger });
|
|
1170
|
-
if (changedNamespaces.length > 0) {
|
|
1171
|
-
const n = changedNamespaces.map((n2) => `"${n2}.json"`).join(", ");
|
|
1172
|
-
logger.success("Updated namespaces {outputDir} ({namespaces})", {
|
|
1173
|
-
outputDir: config.outputDir,
|
|
1174
|
-
namespaces: n
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
async function detectModuleSystem() {
|
|
1179
|
-
const packageJsonPath = join(process.cwd(), "package.json");
|
|
1180
|
-
if (!existsSync(packageJsonPath)) {
|
|
1181
|
-
return "cjs";
|
|
1182
|
-
}
|
|
1183
|
-
try {
|
|
1184
|
-
const content = await readFile(packageJsonPath, "utf-8");
|
|
1185
|
-
const packageJson = JSON.parse(content);
|
|
1186
|
-
if (packageJson.type === "module") {
|
|
1187
|
-
return "esm";
|
|
1188
|
-
}
|
|
1189
|
-
return "cjs";
|
|
1190
|
-
} catch (error) {
|
|
1191
|
-
return "cjs";
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
function getExportStatement(moduleSystem) {
|
|
1195
|
-
return moduleSystem === "esm" ? "export default config;" : "module.exports = config;";
|
|
1196
|
-
}
|
|
1197
|
-
async function generateDefaultConfig() {
|
|
1198
|
-
const moduleSystem = await detectModuleSystem();
|
|
1199
|
-
const exportStatement = getExportStatement(moduleSystem);
|
|
1200
|
-
return `/** @type {import('lang-tag/cli/config').LangTagCLIConfig} */
|
|
1201
|
-
const config = {
|
|
1202
|
-
tagName: 'lang',
|
|
1203
|
-
isLibrary: false,
|
|
1204
|
-
includes: ['src/**/*.{js,ts,jsx,tsx}'],
|
|
1205
|
-
excludes: ['node_modules', 'dist', 'build', '**/*.test.ts'],
|
|
1206
|
-
outputDir: 'public/locales/en',
|
|
1207
|
-
onConfigGeneration: (params) => {
|
|
1208
|
-
// We do not modify imported configurations
|
|
1209
|
-
if (params.isImportedLibrary) return undefined;
|
|
1210
|
-
|
|
1211
|
-
//if (!params.config.path) {
|
|
1212
|
-
// params.config.path = 'test';
|
|
1213
|
-
// params.config.namespace = 'testNamespace';
|
|
1214
|
-
//}
|
|
1215
|
-
|
|
1216
|
-
return undefined
|
|
1217
|
-
},
|
|
1218
|
-
collect: {
|
|
1219
|
-
defaultNamespace: 'common',
|
|
1220
|
-
onConflictResolution: async event => {
|
|
1221
|
-
await event.logger.conflict(event.conflict, true);
|
|
1222
|
-
// By default, continue processing even if conflicts occur
|
|
1223
|
-
// Call event.exit(); to terminate the process upon the first conflict
|
|
1224
|
-
},
|
|
1225
|
-
onCollectFinish: event => {
|
|
1226
|
-
event.exit(); // Stop the process to avoid merging on conflict
|
|
1227
|
-
}
|
|
1228
|
-
},
|
|
1229
|
-
translationArgPosition: 1,
|
|
1230
|
-
debug: false,
|
|
1231
|
-
};
|
|
1232
|
-
|
|
1233
|
-
${exportStatement}`;
|
|
1234
|
-
}
|
|
1235
|
-
async function $LT_CMD_InitConfig() {
|
|
1236
|
-
const logger = $LT_CreateDefaultLogger();
|
|
1237
|
-
if (existsSync(CONFIG_FILE_NAME)) {
|
|
1238
|
-
logger.success("Configuration file already exists. Please remove the existing configuration file before creating a new default one");
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
try {
|
|
1242
|
-
const configContent = await generateDefaultConfig();
|
|
1243
|
-
await writeFile(CONFIG_FILE_NAME, configContent, "utf-8");
|
|
1244
|
-
logger.success("Configuration file created successfully");
|
|
1245
|
-
} catch (error) {
|
|
1246
|
-
logger.error(error?.message);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
function $LT_CollectNodeModulesExportFilePaths(logger) {
|
|
1250
|
-
const nodeModulesPath = path$1.join(process__default.cwd(), "node_modules");
|
|
1251
|
-
if (!fs.existsSync(nodeModulesPath)) {
|
|
1252
|
-
logger.error('"node_modules" directory not found');
|
|
1253
|
-
return [];
|
|
1254
|
-
}
|
|
1255
|
-
function findExportJson(dir, depth = 0, maxDepth = 3) {
|
|
1256
|
-
if (depth > maxDepth) return [];
|
|
1257
|
-
let results = [];
|
|
1258
|
-
try {
|
|
1259
|
-
const files = fs.readdirSync(dir);
|
|
1260
|
-
for (const file of files) {
|
|
1261
|
-
const fullPath = path$1.join(dir, file);
|
|
1262
|
-
const stat = fs.statSync(fullPath);
|
|
1263
|
-
if (stat.isDirectory()) {
|
|
1264
|
-
results = results.concat(findExportJson(fullPath, depth + 1, maxDepth));
|
|
1265
|
-
} else if (file === EXPORTS_FILE_NAME) {
|
|
1266
|
-
results.push(fullPath);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
} catch (error) {
|
|
1270
|
-
logger.error('Error reading directory "{dir}": {error}', {
|
|
1271
|
-
dir,
|
|
1272
|
-
error: String(error)
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
1275
|
-
return results;
|
|
1276
|
-
}
|
|
1277
|
-
return findExportJson(nodeModulesPath);
|
|
1278
|
-
}
|
|
1279
|
-
async function $LT_ImportLibraries(config, logger) {
|
|
1280
|
-
const files = $LT_CollectNodeModulesExportFilePaths(logger);
|
|
1281
|
-
const generationFiles = {};
|
|
1282
|
-
for (const filePath of files) {
|
|
1283
|
-
const exportData = await $LT_ReadJSON(filePath);
|
|
1284
|
-
for (let langTagFilePath in exportData.files) {
|
|
1285
|
-
const fileGenerationData = {};
|
|
1286
|
-
const matches = exportData.files[langTagFilePath].matches;
|
|
1287
|
-
for (let match of matches) {
|
|
1288
|
-
let parsedTranslations = typeof match.translations === "string" ? JSON5.parse(match.translations) : match.translations;
|
|
1289
|
-
let parsedConfig = typeof match.config === "string" ? JSON5.parse(match.config) : match.config === void 0 ? {} : match.config;
|
|
1290
|
-
let file = langTagFilePath;
|
|
1291
|
-
let exportName = match.variableName || "";
|
|
1292
|
-
config.import.onImport({
|
|
1293
|
-
packageName: exportData.packageName,
|
|
1294
|
-
importedRelativePath: langTagFilePath,
|
|
1295
|
-
originalExportName: match.variableName,
|
|
1296
|
-
translations: parsedTranslations,
|
|
1297
|
-
config: parsedConfig,
|
|
1298
|
-
fileGenerationData
|
|
1299
|
-
}, {
|
|
1300
|
-
setFile: (f) => {
|
|
1301
|
-
file = f;
|
|
1302
|
-
},
|
|
1303
|
-
setExportName: (name) => {
|
|
1304
|
-
exportName = name;
|
|
1305
|
-
},
|
|
1306
|
-
setConfig: (newConfig) => {
|
|
1307
|
-
parsedConfig = newConfig;
|
|
1308
|
-
}
|
|
1309
|
-
});
|
|
1310
|
-
if (!file || !exportName) {
|
|
1311
|
-
throw new Error(`[lang-tag] onImport did not set fileName or exportName for package: ${exportData.packageName}, file: '${file}' (original: '${langTagFilePath}'), exportName: '${exportName}' (original: ${match.variableName})`);
|
|
1312
|
-
}
|
|
1313
|
-
let exports = generationFiles[file];
|
|
1314
|
-
if (!exports) {
|
|
1315
|
-
exports = {};
|
|
1316
|
-
generationFiles[file] = exports;
|
|
1317
|
-
}
|
|
1318
|
-
const param1 = config.translationArgPosition === 1 ? parsedTranslations : parsedConfig;
|
|
1319
|
-
const param2 = config.translationArgPosition === 1 ? parsedConfig : parsedTranslations;
|
|
1320
|
-
exports[exportName] = `${config.tagName}(${JSON5.stringify(param1, void 0, 4)}, ${JSON5.stringify(param2, void 0, 4)})`;
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
for (let fileName of Object.keys(generationFiles)) {
|
|
1325
|
-
const filePath = resolve(
|
|
1326
|
-
process$1.cwd(),
|
|
1327
|
-
config.import.dir,
|
|
1328
|
-
fileName
|
|
1329
|
-
);
|
|
1330
|
-
const exports = Object.entries(generationFiles[fileName]).map(([name, tag]) => {
|
|
1331
|
-
return `export const ${name} = ${tag};`;
|
|
1332
|
-
}).join("\n\n");
|
|
1333
|
-
const content = `${config.import.tagImportPath}
|
|
1334
|
-
|
|
1335
|
-
${exports}`;
|
|
1336
|
-
await $LT_EnsureDirectoryExists(dirname$1(filePath));
|
|
1337
|
-
await writeFile(filePath, content, "utf-8");
|
|
1338
|
-
logger.success('Imported node_modules file: "{fileName}"', { fileName });
|
|
1339
|
-
}
|
|
1340
|
-
if (config.import.onImportFinish) config.import.onImportFinish();
|
|
1341
|
-
}
|
|
1342
|
-
async function $LT_ImportTranslations() {
|
|
1343
|
-
const { config, logger } = await $LT_GetCommandEssentials();
|
|
1344
|
-
await $LT_EnsureDirectoryExists(config.import.dir);
|
|
1345
|
-
logger.info("Importing translations from libraries...");
|
|
1346
|
-
await $LT_ImportLibraries(config, logger);
|
|
1347
|
-
logger.success("Successfully imported translations from libraries.");
|
|
1348
|
-
}
|
|
1349
|
-
function renderTemplate(template, data) {
|
|
1350
|
-
return mustache.render(template, data, {}, { escape: (text) => text });
|
|
1351
|
-
}
|
|
1352
|
-
function loadTemplate(templateName) {
|
|
1353
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
1354
|
-
const __dirname = dirname(__filename);
|
|
1355
|
-
const templatePath = join(__dirname, "template", `${templateName}.mustache`);
|
|
1356
|
-
try {
|
|
1357
|
-
return readFileSync(templatePath, "utf-8");
|
|
1358
|
-
} catch (error) {
|
|
1359
|
-
throw new Error(`Failed to load template ${templateName}: ${error}`);
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
function prepareTemplateData(options) {
|
|
1363
|
-
return {
|
|
1364
|
-
...options,
|
|
1365
|
-
tmpVariables: {
|
|
1366
|
-
key: "{{key}}",
|
|
1367
|
-
username: "{{username}}",
|
|
1368
|
-
processRegex: "{{(.*?)}}"
|
|
1369
|
-
}
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
function renderInitTagTemplates(options) {
|
|
1373
|
-
const baseTemplateName = options.isLibrary ? "base-library" : "base-app";
|
|
1374
|
-
const baseTemplate = loadTemplate(baseTemplateName);
|
|
1375
|
-
const placeholderTemplate = loadTemplate("placeholder");
|
|
1376
|
-
const templateData = prepareTemplateData(options);
|
|
1377
|
-
const renderedBase = renderTemplate(baseTemplate, templateData);
|
|
1378
|
-
const renderedPlaceholders = renderTemplate(placeholderTemplate, templateData);
|
|
1379
|
-
return renderedBase + "\n\n" + renderedPlaceholders;
|
|
1380
|
-
}
|
|
1381
|
-
async function readPackageJson() {
|
|
1382
|
-
const packageJsonPath = join(process.cwd(), "package.json");
|
|
1383
|
-
if (!existsSync(packageJsonPath)) {
|
|
1384
|
-
return null;
|
|
1385
|
-
}
|
|
1386
|
-
try {
|
|
1387
|
-
const content = await readFile(packageJsonPath, "utf-8");
|
|
1388
|
-
return JSON.parse(content);
|
|
1389
|
-
} catch (error) {
|
|
1390
|
-
return null;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
function detectTypeScript(packageJson) {
|
|
1394
|
-
if (!packageJson) return false;
|
|
1395
|
-
const hasTypeScript = packageJson.devDependencies?.typescript || packageJson.dependencies?.typescript;
|
|
1396
|
-
const hasTsConfig = existsSync(join(process.cwd(), "tsconfig.json"));
|
|
1397
|
-
return Boolean(hasTypeScript || hasTsConfig);
|
|
1398
|
-
}
|
|
1399
|
-
function detectReact(packageJson) {
|
|
1400
|
-
if (!packageJson) return false;
|
|
1401
|
-
return Boolean(
|
|
1402
|
-
packageJson.dependencies?.react || packageJson.devDependencies?.react || packageJson.dependencies?.["@types/react"] || packageJson.devDependencies?.["@types/react"]
|
|
1403
|
-
);
|
|
1404
|
-
}
|
|
1405
|
-
async function detectInitTagOptions(options, config) {
|
|
1406
|
-
const packageJson = await readPackageJson();
|
|
1407
|
-
const isTypeScript = options.typescript !== void 0 ? options.typescript : detectTypeScript(packageJson);
|
|
1408
|
-
const isReact = options.react !== void 0 ? options.react : detectReact(packageJson);
|
|
1409
|
-
const isLibrary = options.library !== void 0 ? options.library : config.isLibrary;
|
|
1410
|
-
const tagName = options.name || config.tagName || "lang";
|
|
1411
|
-
const fileExtension = isLibrary && isReact ? isTypeScript ? "tsx" : "jsx" : isTypeScript ? "ts" : "js";
|
|
1412
|
-
return {
|
|
1413
|
-
tagName,
|
|
1414
|
-
isLibrary,
|
|
1415
|
-
isReact,
|
|
1416
|
-
isTypeScript,
|
|
1417
|
-
fileExtension,
|
|
1418
|
-
packageName: packageJson?.name || "my-project",
|
|
1419
|
-
packageVersion: packageJson?.version || "1.0.0"
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
1422
|
-
async function $LT_CMD_InitTagFile(options = {}) {
|
|
1423
|
-
const { config, logger } = await $LT_GetCommandEssentials();
|
|
1424
|
-
const renderOptions = await detectInitTagOptions(options, config);
|
|
1425
|
-
const outputPath = options.output || `${renderOptions.tagName}.${renderOptions.fileExtension}`;
|
|
1426
|
-
logger.info("Initializing lang-tag with the following options:");
|
|
1427
|
-
logger.info(" Tag name: {tagName}", { tagName: renderOptions.tagName });
|
|
1428
|
-
logger.info(" Library mode: {isLibrary}", { isLibrary: renderOptions.isLibrary ? "Yes" : "No" });
|
|
1429
|
-
logger.info(" React: {isReact}", { isReact: renderOptions.isReact ? "Yes" : "No" });
|
|
1430
|
-
logger.info(" TypeScript: {isTypeScript}", { isTypeScript: renderOptions.isTypeScript ? "Yes" : "No" });
|
|
1431
|
-
logger.info(" Output path: {outputPath}", { outputPath });
|
|
1432
|
-
let renderedContent;
|
|
1433
|
-
try {
|
|
1434
|
-
renderedContent = renderInitTagTemplates(renderOptions);
|
|
1435
|
-
} catch (error) {
|
|
1436
|
-
logger.error("Failed to render templates: {error}", { error: error?.message });
|
|
1437
|
-
return;
|
|
1438
|
-
}
|
|
1439
|
-
if (existsSync(outputPath)) {
|
|
1440
|
-
logger.warn("File already exists: {outputPath}", { outputPath });
|
|
1441
|
-
logger.info("Use --output to specify a different path or remove the existing file");
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
try {
|
|
1445
|
-
await $LT_WriteFileWithDirs(outputPath, renderedContent);
|
|
1446
|
-
logger.success("Lang-tag file created successfully: {outputPath}", { outputPath });
|
|
1447
|
-
logger.info("Next steps:");
|
|
1448
|
-
logger.info("1. Import the {tagName} function in your files:", { tagName: renderOptions.tagName });
|
|
1449
|
-
logger.info(" import { {tagName} } from './{importPath}';", {
|
|
1450
|
-
tagName: renderOptions.tagName,
|
|
1451
|
-
importPath: outputPath.replace(/^src\//, "")
|
|
1452
|
-
});
|
|
1453
|
-
logger.info("2. Create your translation objects and use the tag function");
|
|
1454
|
-
logger.info('3. Run "lang-tag collect" to extract translations');
|
|
1455
|
-
} catch (error) {
|
|
1456
|
-
logger.error("Failed to write file: {error}", { error: error?.message });
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
function createCli() {
|
|
1460
|
-
program.name("lang-tag").description("CLI to manage language translations").version("0.1.0");
|
|
1461
|
-
program.command("collect").alias("c").description("Collect translations from source files").option("-c, --clean", "Remove output directory before collecting").action($LT_CMD_Collect);
|
|
1462
|
-
program.command("import").alias("i").description("Import translations from libraries in node_modules").action($LT_ImportTranslations);
|
|
1463
|
-
program.command("regenerate-tags").alias("rt").description("Regenerate configuration for language tags").action($LT_CMD_RegenerateTags);
|
|
1464
|
-
program.command("watch").alias("w").description("Watch for changes in source files and automatically collect translations").action($LT_WatchTranslations);
|
|
1465
|
-
program.command("init").description("Initialize project with default configuration").action($LT_CMD_InitConfig);
|
|
1466
|
-
program.command("init-tag").description("Initialize a new lang-tag function file").option("-n, --name <name>", "Name of the tag function (default: from config)").option("-l, --library", "Generate library-style tag (default: from config)").option("-r, --react", "Include React-specific optimizations (default: auto-detect)").option("-t, --typescript", "Force TypeScript output (default: auto-detect)").option("-o, --output <path>", "Output file path (default: auto-generated)").action(async (options) => {
|
|
1467
|
-
await $LT_CMD_InitTagFile(options);
|
|
1468
|
-
});
|
|
1469
|
-
return program;
|
|
1470
|
-
}
|
|
1471
|
-
createCli().parse();
|
|
1472
|
-
export {
|
|
1473
|
-
createCli
|
|
1474
|
-
};
|