@mxml3gend/gloss 0.1.2 → 0.1.3
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 +22 -0
- package/dist/baseline.js +118 -0
- package/dist/cache.js +78 -0
- package/dist/cacheMetrics.js +120 -0
- package/dist/check.js +153 -25
- package/dist/config.js +79 -4
- package/dist/fs.js +105 -6
- package/dist/hooks.js +101 -0
- package/dist/index.js +262 -6
- package/dist/server.js +140 -10
- package/dist/translationTree.js +20 -0
- package/dist/ui/assets/index-BCr07xD_.js +21 -0
- package/dist/ui/assets/index-CjmLcA1x.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/usage.js +107 -6
- package/dist/usageScanner.js +108 -15
- package/dist/xliff.js +92 -0
- package/package.json +3 -2
- package/dist/ui/assets/index-CgyZVU2h.css +0 -1
- package/dist/ui/assets/index-DfgO64nU.js +0 -12
package/dist/index.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
import open from "open";
|
|
7
|
+
import { resetIssueBaseline, updateIssueBaseline } from "./baseline.js";
|
|
8
|
+
import { clearGlossCaches, getCacheStatus } from "./cache.js";
|
|
5
9
|
import { printGlossCheck, runGlossCheck } from "./check.js";
|
|
6
|
-
import { GlossConfigError, loadGlossConfig } from "./config.js";
|
|
10
|
+
import { GlossConfigError, discoverLocaleDirectoryCandidates, loadGlossConfig, } from "./config.js";
|
|
11
|
+
import { installPreCommitHooks } from "./hooks.js";
|
|
7
12
|
import { startServer } from "./server.js";
|
|
8
13
|
import { generateKeyTypes } from "./typegen.js";
|
|
9
14
|
const DEFAULT_PORT = 5179;
|
|
15
|
+
const GENERATED_CONFIG_FILE = "gloss.config.cjs";
|
|
16
|
+
const projectRoot = () => process.env.INIT_CWD || process.cwd();
|
|
10
17
|
const getVersion = async () => {
|
|
11
18
|
const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
12
19
|
const raw = await fs.readFile(packagePath, "utf8");
|
|
@@ -20,6 +27,10 @@ Usage:
|
|
|
20
27
|
gloss [options]
|
|
21
28
|
gloss check [options]
|
|
22
29
|
gloss gen-types [options]
|
|
30
|
+
gloss init-hooks [options]
|
|
31
|
+
gloss baseline reset
|
|
32
|
+
gloss cache status
|
|
33
|
+
gloss cache clear
|
|
23
34
|
gloss open key <translation-key> [options]
|
|
24
35
|
|
|
25
36
|
Options:
|
|
@@ -28,21 +39,85 @@ Options:
|
|
|
28
39
|
|
|
29
40
|
Serve options:
|
|
30
41
|
--no-open Do not open browser automatically
|
|
42
|
+
--no-cache Bypass scanner caches for API responses
|
|
31
43
|
-p, --port Set server port (default: ${DEFAULT_PORT})
|
|
32
44
|
|
|
33
45
|
Check options:
|
|
34
46
|
--format <human|json|both> Output format (default: human)
|
|
35
47
|
--json Shortcut for --format json
|
|
48
|
+
--no-cache Force full rescan without reading/writing scanner cache
|
|
36
49
|
|
|
37
50
|
Type generation options:
|
|
38
51
|
--out <path> Output file for generated key types (default: i18n-keys.d.ts)
|
|
39
52
|
|
|
53
|
+
Hook options:
|
|
54
|
+
gloss init-hooks Install pre-commit hooks for gloss check
|
|
55
|
+
|
|
56
|
+
Baseline options:
|
|
57
|
+
gloss baseline reset Remove the local issue baseline (.gloss/baseline.json)
|
|
58
|
+
|
|
59
|
+
Cache options:
|
|
60
|
+
gloss cache status Show scanner cache status
|
|
61
|
+
gloss cache clear Clear in-memory scanner cache and .gloss/cache-metrics.json
|
|
62
|
+
|
|
40
63
|
Open key options:
|
|
41
64
|
gloss open key <key> Open Gloss focused on a translation key
|
|
65
|
+
--no-cache Bypass scanner caches while serving
|
|
42
66
|
`);
|
|
43
67
|
};
|
|
44
68
|
const parseArgs = (args) => {
|
|
45
69
|
const firstArg = args[0];
|
|
70
|
+
if (firstArg === "baseline") {
|
|
71
|
+
const commandArgs = args.slice(1);
|
|
72
|
+
const options = {
|
|
73
|
+
command: "baseline-reset",
|
|
74
|
+
help: false,
|
|
75
|
+
version: false,
|
|
76
|
+
};
|
|
77
|
+
for (const arg of commandArgs) {
|
|
78
|
+
if (arg === "-h" || arg === "--help") {
|
|
79
|
+
options.help = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg === "-v" || arg === "--version") {
|
|
83
|
+
options.version = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg === "reset") {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Unknown argument for gloss baseline: ${arg}`);
|
|
90
|
+
}
|
|
91
|
+
if (!options.help && commandArgs[0] !== "reset") {
|
|
92
|
+
throw new Error("Usage: gloss baseline reset");
|
|
93
|
+
}
|
|
94
|
+
return options;
|
|
95
|
+
}
|
|
96
|
+
if (firstArg === "cache") {
|
|
97
|
+
const commandArgs = args.slice(1);
|
|
98
|
+
const action = commandArgs[0];
|
|
99
|
+
const restArgs = commandArgs.slice(1);
|
|
100
|
+
if (action !== "status" && action !== "clear") {
|
|
101
|
+
throw new Error("Usage: gloss cache <status|clear>");
|
|
102
|
+
}
|
|
103
|
+
const options = {
|
|
104
|
+
command: action === "status" ? "cache-status" : "cache-clear",
|
|
105
|
+
help: false,
|
|
106
|
+
version: false,
|
|
107
|
+
};
|
|
108
|
+
for (const arg of restArgs) {
|
|
109
|
+
if (arg === "-h" || arg === "--help") {
|
|
110
|
+
options.help = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (arg === "-v" || arg === "--version") {
|
|
114
|
+
options.version = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`Unknown argument for gloss cache: ${arg}`);
|
|
118
|
+
}
|
|
119
|
+
return options;
|
|
120
|
+
}
|
|
46
121
|
if (firstArg === "open") {
|
|
47
122
|
const commandArgs = args.slice(1);
|
|
48
123
|
const base = { help: false, version: false };
|
|
@@ -50,6 +125,7 @@ const parseArgs = (args) => {
|
|
|
50
125
|
command: "open-key",
|
|
51
126
|
...base,
|
|
52
127
|
noOpen: false,
|
|
128
|
+
noCache: false,
|
|
53
129
|
port: DEFAULT_PORT,
|
|
54
130
|
key: "",
|
|
55
131
|
};
|
|
@@ -88,6 +164,10 @@ const parseArgs = (args) => {
|
|
|
88
164
|
options.noOpen = true;
|
|
89
165
|
continue;
|
|
90
166
|
}
|
|
167
|
+
if (arg === "--no-cache") {
|
|
168
|
+
options.noCache = true;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
91
171
|
if (arg === "-p" || arg === "--port") {
|
|
92
172
|
const nextValue = commandArgs[index + 1];
|
|
93
173
|
if (!nextValue) {
|
|
@@ -106,7 +186,9 @@ const parseArgs = (args) => {
|
|
|
106
186
|
return options;
|
|
107
187
|
}
|
|
108
188
|
const isCommand = firstArg && !firstArg.startsWith("-");
|
|
109
|
-
const command = firstArg === "check" || firstArg === "gen-types"
|
|
189
|
+
const command = firstArg === "check" || firstArg === "gen-types" || firstArg === "init-hooks"
|
|
190
|
+
? firstArg
|
|
191
|
+
: "serve";
|
|
110
192
|
const commandArgs = command === "serve" ? args : args.slice(1);
|
|
111
193
|
const base = {
|
|
112
194
|
help: false,
|
|
@@ -117,6 +199,7 @@ const parseArgs = (args) => {
|
|
|
117
199
|
command,
|
|
118
200
|
...base,
|
|
119
201
|
format: "human",
|
|
202
|
+
noCache: false,
|
|
120
203
|
};
|
|
121
204
|
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
122
205
|
const arg = commandArgs[index];
|
|
@@ -132,6 +215,10 @@ const parseArgs = (args) => {
|
|
|
132
215
|
options.format = "json";
|
|
133
216
|
continue;
|
|
134
217
|
}
|
|
218
|
+
if (arg === "--no-cache") {
|
|
219
|
+
options.noCache = true;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
135
222
|
if (arg === "--format") {
|
|
136
223
|
const nextValue = commandArgs[index + 1];
|
|
137
224
|
if (!nextValue) {
|
|
@@ -176,6 +263,24 @@ const parseArgs = (args) => {
|
|
|
176
263
|
}
|
|
177
264
|
return options;
|
|
178
265
|
}
|
|
266
|
+
if (command === "init-hooks") {
|
|
267
|
+
const options = {
|
|
268
|
+
command,
|
|
269
|
+
...base,
|
|
270
|
+
};
|
|
271
|
+
for (const arg of commandArgs) {
|
|
272
|
+
if (arg === "-h" || arg === "--help") {
|
|
273
|
+
options.help = true;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (arg === "-v" || arg === "--version") {
|
|
277
|
+
options.version = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
throw new Error(`Unknown argument for gloss init-hooks: ${arg}`);
|
|
281
|
+
}
|
|
282
|
+
return options;
|
|
283
|
+
}
|
|
179
284
|
if (isCommand && command === "serve" && firstArg !== undefined) {
|
|
180
285
|
throw new Error(`Unknown command: ${firstArg}`);
|
|
181
286
|
}
|
|
@@ -183,6 +288,7 @@ const parseArgs = (args) => {
|
|
|
183
288
|
command: "serve",
|
|
184
289
|
...base,
|
|
185
290
|
noOpen: false,
|
|
291
|
+
noCache: false,
|
|
186
292
|
port: DEFAULT_PORT,
|
|
187
293
|
};
|
|
188
294
|
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
@@ -199,6 +305,10 @@ const parseArgs = (args) => {
|
|
|
199
305
|
options.noOpen = true;
|
|
200
306
|
continue;
|
|
201
307
|
}
|
|
308
|
+
if (arg === "--no-cache") {
|
|
309
|
+
options.noCache = true;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
202
312
|
if (arg === "-p" || arg === "--port") {
|
|
203
313
|
const nextValue = commandArgs[index + 1];
|
|
204
314
|
if (!nextValue) {
|
|
@@ -231,6 +341,87 @@ const printConfigError = (error) => {
|
|
|
231
341
|
console.error("Gloss could not start: invalid gloss.config.ts.");
|
|
232
342
|
console.error(error.message);
|
|
233
343
|
};
|
|
344
|
+
const renderGeneratedConfig = (candidatePath, locales) => {
|
|
345
|
+
const defaultLocale = locales.includes("en") ? "en" : locales[0] ?? "en";
|
|
346
|
+
const localeList = locales.map((locale) => JSON.stringify(locale)).join(", ");
|
|
347
|
+
const pathLiteral = JSON.stringify(candidatePath);
|
|
348
|
+
return `module.exports = {
|
|
349
|
+
locales: [${localeList}],
|
|
350
|
+
defaultLocale: ${JSON.stringify(defaultLocale)},
|
|
351
|
+
path: ${pathLiteral},
|
|
352
|
+
format: "json",
|
|
353
|
+
};
|
|
354
|
+
`;
|
|
355
|
+
};
|
|
356
|
+
const chooseCandidateInteractive = async (candidates) => {
|
|
357
|
+
if (candidates.length === 1) {
|
|
358
|
+
return candidates[0];
|
|
359
|
+
}
|
|
360
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
361
|
+
return candidates[0];
|
|
362
|
+
}
|
|
363
|
+
const rl = createInterface({
|
|
364
|
+
input: process.stdin,
|
|
365
|
+
output: process.stdout,
|
|
366
|
+
});
|
|
367
|
+
try {
|
|
368
|
+
console.log("Gloss setup: multiple locale directories were found.");
|
|
369
|
+
candidates.forEach((candidate, index) => {
|
|
370
|
+
const marker = index === 0 ? " (recommended)" : "";
|
|
371
|
+
console.log(` ${index + 1}. ${candidate.path} -> [${candidate.locales.join(", ")}]${marker}`);
|
|
372
|
+
});
|
|
373
|
+
while (true) {
|
|
374
|
+
const answer = (await rl.question(`Choose a locale directory [1-${candidates.length}] (default 1): `)).trim();
|
|
375
|
+
if (!answer) {
|
|
376
|
+
return candidates[0];
|
|
377
|
+
}
|
|
378
|
+
const selection = Number.parseInt(answer, 10);
|
|
379
|
+
if (Number.isFinite(selection) && selection >= 1 && selection <= candidates.length) {
|
|
380
|
+
return candidates[selection - 1];
|
|
381
|
+
}
|
|
382
|
+
console.log(`Please enter a number between 1 and ${candidates.length}.`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
rl.close();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const bootstrapConfigIfMissing = async () => {
|
|
390
|
+
const cwd = projectRoot();
|
|
391
|
+
const candidates = await discoverLocaleDirectoryCandidates(cwd);
|
|
392
|
+
if (candidates.length === 0) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
console.log("No Gloss config found. Starting first-run setup.");
|
|
396
|
+
const selected = await chooseCandidateInteractive(candidates);
|
|
397
|
+
const configFilePath = path.join(cwd, GENERATED_CONFIG_FILE);
|
|
398
|
+
const content = renderGeneratedConfig(selected.path, selected.locales);
|
|
399
|
+
await fs.writeFile(configFilePath, content, "utf8");
|
|
400
|
+
console.log(`Created ${GENERATED_CONFIG_FILE} using ${selected.path}.`);
|
|
401
|
+
console.log("Starting Gloss with the generated config.");
|
|
402
|
+
return true;
|
|
403
|
+
};
|
|
404
|
+
const formatBytes = (value) => {
|
|
405
|
+
if (value < 1024) {
|
|
406
|
+
return `${value} B`;
|
|
407
|
+
}
|
|
408
|
+
if (value < 1024 * 1024) {
|
|
409
|
+
return `${(value / 1024).toFixed(1)} KB`;
|
|
410
|
+
}
|
|
411
|
+
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
|
412
|
+
};
|
|
413
|
+
const formatAge = (ageMs) => {
|
|
414
|
+
if (ageMs === null) {
|
|
415
|
+
return "n/a";
|
|
416
|
+
}
|
|
417
|
+
const totalSeconds = Math.max(0, Math.floor(ageMs / 1000));
|
|
418
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
419
|
+
const seconds = totalSeconds % 60;
|
|
420
|
+
if (minutes === 0) {
|
|
421
|
+
return `${seconds}s`;
|
|
422
|
+
}
|
|
423
|
+
return `${minutes}m ${seconds}s`;
|
|
424
|
+
};
|
|
234
425
|
async function main() {
|
|
235
426
|
const options = parseArgs(process.argv.slice(2));
|
|
236
427
|
if (options.help) {
|
|
@@ -241,21 +432,86 @@ async function main() {
|
|
|
241
432
|
console.log(await getVersion());
|
|
242
433
|
return;
|
|
243
434
|
}
|
|
244
|
-
|
|
435
|
+
if (options.command === "init-hooks") {
|
|
436
|
+
const result = await installPreCommitHooks(projectRoot());
|
|
437
|
+
console.log("Gloss hook installation");
|
|
438
|
+
for (const message of result.messages) {
|
|
439
|
+
console.log(`- ${message}`);
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (options.command === "baseline-reset") {
|
|
444
|
+
const result = await resetIssueBaseline(projectRoot());
|
|
445
|
+
if (result.existed) {
|
|
446
|
+
console.log(`Removed baseline at ${result.baselinePath}`);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
console.log(`No baseline found at ${result.baselinePath}`);
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (options.command === "cache-clear") {
|
|
454
|
+
const result = await clearGlossCaches(projectRoot());
|
|
455
|
+
console.log("Gloss cache clear");
|
|
456
|
+
console.log(`- Usage scanner cache: ${result.usage.fileCount} files across ${result.usage.bucketCount} buckets removed`);
|
|
457
|
+
console.log(`- Key usage cache: ${result.keyUsage.fileCount} files across ${result.keyUsage.bucketCount} buckets removed`);
|
|
458
|
+
console.log(`- Cache metrics file: ${result.metrics.existed ? "removed" : "not found"} (${result.metrics.path})`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
let cfg;
|
|
462
|
+
try {
|
|
463
|
+
cfg = await loadGlossConfig();
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
if (error instanceof GlossConfigError && error.code === "MISSING_CONFIG") {
|
|
467
|
+
const generated = await bootstrapConfigIfMissing();
|
|
468
|
+
if (generated) {
|
|
469
|
+
cfg = await loadGlossConfig();
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
245
479
|
if (options.command === "check") {
|
|
246
|
-
const result = await runGlossCheck(cfg
|
|
247
|
-
|
|
480
|
+
const result = await runGlossCheck(cfg, {
|
|
481
|
+
useCache: !options.noCache,
|
|
482
|
+
});
|
|
483
|
+
let baseline;
|
|
484
|
+
try {
|
|
485
|
+
baseline = await updateIssueBaseline(projectRoot(), result.summary);
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
console.error(`Warning: failed to update issue baseline: ${error.message}`);
|
|
489
|
+
}
|
|
490
|
+
printGlossCheck(result, options.format, baseline);
|
|
248
491
|
if (!result.ok) {
|
|
249
492
|
process.exit(1);
|
|
250
493
|
}
|
|
251
494
|
return;
|
|
252
495
|
}
|
|
496
|
+
if (options.command === "cache-status") {
|
|
497
|
+
const status = await getCacheStatus(projectRoot(), cfg);
|
|
498
|
+
console.log("Gloss cache status");
|
|
499
|
+
console.log(`- Metrics file: ${status.metricsFileFound ? "found" : "not found"}${status.metricsUpdatedAt ? ` (updated ${status.metricsUpdatedAt})` : ""}`);
|
|
500
|
+
console.log(`- Total cached files: ${status.totalCachedFiles} (${formatBytes(status.totalCachedSizeBytes)})`);
|
|
501
|
+
console.log(`- Oldest cache entry age: ${formatAge(status.oldestEntryAgeMs)}`);
|
|
502
|
+
console.log(`- Stale relative to config: ${status.staleRelativeToConfig ? "yes" : "no"}`);
|
|
503
|
+
console.log(`- Usage scanner: ${status.usageScanner.fileCount} files, ${formatBytes(status.usageScanner.totalSizeBytes)}, source=${status.usageScanner.source}`);
|
|
504
|
+
console.log(`- Key usage: ${status.keyUsage.fileCount} files, ${formatBytes(status.keyUsage.totalSizeBytes)}, source=${status.keyUsage.source}`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
253
507
|
if (options.command === "gen-types") {
|
|
254
508
|
const result = await generateKeyTypes(cfg, { outFile: options.outFile });
|
|
255
509
|
console.log(`Generated ${result.keyCount} keys in ${result.outFile}`);
|
|
256
510
|
return;
|
|
257
511
|
}
|
|
258
|
-
const { port } = await startServer(cfg, options.port
|
|
512
|
+
const { port } = await startServer(cfg, options.port, {
|
|
513
|
+
useCache: !options.noCache,
|
|
514
|
+
});
|
|
259
515
|
const url = options.command === "open-key"
|
|
260
516
|
? `http://localhost:${port}/?key=${encodeURIComponent(options.key)}`
|
|
261
517
|
: `http://localhost:${port}`;
|
package/dist/server.js
CHANGED
|
@@ -3,11 +3,14 @@ import path from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import express from "express";
|
|
5
5
|
import cors from "cors";
|
|
6
|
+
import { updateIssueBaseline } from "./baseline.js";
|
|
6
7
|
import { runGlossCheck } from "./check.js";
|
|
7
|
-
import { readAllTranslations, writeAllTranslations } from "./fs.js";
|
|
8
|
+
import { readAllTranslations, WriteLockError, writeAllTranslations } from "./fs.js";
|
|
8
9
|
import { buildGitKeyDiff } from "./gitDiff.js";
|
|
10
|
+
import { flattenObject, unflattenObject } from "./translationTree.js";
|
|
9
11
|
import { buildKeyUsageMap } from "./usage.js";
|
|
10
12
|
import { inferUsageRoot, scanUsage } from "./usageScanner.js";
|
|
13
|
+
import { buildXliffDocument, parseXliffTargets } from "./xliff.js";
|
|
11
14
|
import { renameKeyUsage } from "./renameKeyUsage.js";
|
|
12
15
|
const resolveUiDistPath = () => {
|
|
13
16
|
const runtimeDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -25,24 +28,57 @@ const resolveUiDistPath = () => {
|
|
|
25
28
|
return null;
|
|
26
29
|
};
|
|
27
30
|
export function createServerApp(cfg) {
|
|
31
|
+
return createServerAppWithOptions(cfg);
|
|
32
|
+
}
|
|
33
|
+
const shouldBypassCache = (value) => {
|
|
34
|
+
if (typeof value !== "string") {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return value === "1" || value.toLowerCase() === "true";
|
|
38
|
+
};
|
|
39
|
+
export function createServerAppWithOptions(cfg, runtimeOptions = {}) {
|
|
28
40
|
const app = express();
|
|
29
41
|
app.use(cors());
|
|
30
42
|
app.use(express.json({ limit: "5mb" }));
|
|
43
|
+
const defaultUseCache = runtimeOptions.useCache !== false;
|
|
44
|
+
const requestUseCache = (req) => {
|
|
45
|
+
if (!defaultUseCache) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const noCacheValue = req.query.noCache;
|
|
49
|
+
if (Array.isArray(noCacheValue)) {
|
|
50
|
+
return !noCacheValue.some((entry) => shouldBypassCache(entry));
|
|
51
|
+
}
|
|
52
|
+
return !shouldBypassCache(noCacheValue);
|
|
53
|
+
};
|
|
31
54
|
app.get("/api/config", (_req, res) => res.json(cfg));
|
|
32
55
|
app.get("/api/translations", async (_req, res) => {
|
|
33
56
|
const data = await readAllTranslations(cfg);
|
|
34
57
|
res.json(data);
|
|
35
58
|
});
|
|
36
59
|
app.get("/api/usage", async (_req, res) => {
|
|
37
|
-
const usage = await scanUsage(inferUsageRoot(cfg), cfg.scan
|
|
60
|
+
const usage = await scanUsage(inferUsageRoot(cfg), cfg.scan, {
|
|
61
|
+
useCache: requestUseCache(_req),
|
|
62
|
+
});
|
|
38
63
|
res.json(usage);
|
|
39
64
|
});
|
|
40
|
-
app.get("/api/key-usage", async (
|
|
41
|
-
const usage = await buildKeyUsageMap(cfg
|
|
65
|
+
app.get("/api/key-usage", async (req, res) => {
|
|
66
|
+
const usage = await buildKeyUsageMap(cfg, {
|
|
67
|
+
useCache: requestUseCache(req),
|
|
68
|
+
});
|
|
42
69
|
res.json(usage);
|
|
43
70
|
});
|
|
44
71
|
app.get("/api/check", async (req, res) => {
|
|
45
|
-
const result = await runGlossCheck(cfg
|
|
72
|
+
const result = await runGlossCheck(cfg, {
|
|
73
|
+
useCache: requestUseCache(req),
|
|
74
|
+
});
|
|
75
|
+
let baseline = null;
|
|
76
|
+
try {
|
|
77
|
+
baseline = await updateIssueBaseline(process.env.INIT_CWD || process.cwd(), result.summary);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
baseline = null;
|
|
81
|
+
}
|
|
46
82
|
const summaryValue = typeof req.query.summary === "string" ? req.query.summary : "";
|
|
47
83
|
const summaryOnly = summaryValue === "1" || summaryValue === "true";
|
|
48
84
|
if (summaryOnly) {
|
|
@@ -51,10 +87,11 @@ export function createServerApp(cfg) {
|
|
|
51
87
|
generatedAt: result.generatedAt,
|
|
52
88
|
summary: result.summary,
|
|
53
89
|
hardcodedTexts: result.hardcodedTexts.slice(0, 20),
|
|
90
|
+
baseline,
|
|
54
91
|
});
|
|
55
92
|
return;
|
|
56
93
|
}
|
|
57
|
-
res.json(result);
|
|
94
|
+
res.json({ ...result, baseline });
|
|
58
95
|
});
|
|
59
96
|
app.get("/api/git-diff", async (req, res) => {
|
|
60
97
|
const base = typeof req.query.base === "string" && req.query.base.trim()
|
|
@@ -63,10 +100,103 @@ export function createServerApp(cfg) {
|
|
|
63
100
|
const diff = await buildGitKeyDiff(cfg, base);
|
|
64
101
|
res.json(diff);
|
|
65
102
|
});
|
|
103
|
+
app.get("/api/xliff/export", async (req, res) => {
|
|
104
|
+
const locale = typeof req.query.locale === "string" ? req.query.locale.trim() : "";
|
|
105
|
+
const sourceLocale = typeof req.query.sourceLocale === "string" && req.query.sourceLocale.trim()
|
|
106
|
+
? req.query.sourceLocale.trim()
|
|
107
|
+
: cfg.defaultLocale;
|
|
108
|
+
if (!locale || !cfg.locales.includes(locale)) {
|
|
109
|
+
res.status(400).json({
|
|
110
|
+
ok: false,
|
|
111
|
+
error: "Query parameter `locale` is required and must be one of configured locales.",
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!cfg.locales.includes(sourceLocale)) {
|
|
116
|
+
res.status(400).json({
|
|
117
|
+
ok: false,
|
|
118
|
+
error: "Query parameter `sourceLocale` must be one of configured locales when provided.",
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const data = await readAllTranslations(cfg);
|
|
123
|
+
const xml = buildXliffDocument({
|
|
124
|
+
translations: data,
|
|
125
|
+
locales: cfg.locales,
|
|
126
|
+
sourceLocale,
|
|
127
|
+
targetLocale: locale,
|
|
128
|
+
});
|
|
129
|
+
res.setHeader("Content-Type", "application/xliff+xml; charset=utf-8");
|
|
130
|
+
res.setHeader("Content-Disposition", `attachment; filename="gloss-${locale}.xlf"`);
|
|
131
|
+
res.send(xml);
|
|
132
|
+
});
|
|
133
|
+
app.post("/api/xliff/import", async (req, res) => {
|
|
134
|
+
const locale = typeof req.query.locale === "string" ? req.query.locale.trim() : "";
|
|
135
|
+
if (!locale || !cfg.locales.includes(locale)) {
|
|
136
|
+
res.status(400).json({
|
|
137
|
+
ok: false,
|
|
138
|
+
error: "Query parameter `locale` is required and must be one of configured locales.",
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const body = req.body;
|
|
143
|
+
if (typeof body.content !== "string" || body.content.trim().length === 0) {
|
|
144
|
+
res.status(400).json({
|
|
145
|
+
ok: false,
|
|
146
|
+
error: "Request body must include non-empty `content` string.",
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const parsedTargets = parseXliffTargets(body.content);
|
|
152
|
+
const updates = Object.entries(parsedTargets).filter(([key]) => key.trim().length > 0);
|
|
153
|
+
if (updates.length === 0) {
|
|
154
|
+
res.status(400).json({
|
|
155
|
+
ok: false,
|
|
156
|
+
error: "No translatable units found in XLIFF content.",
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const data = await readAllTranslations(cfg);
|
|
161
|
+
const localeFlat = flattenObject(data[locale] ?? {});
|
|
162
|
+
for (const [key, value] of updates) {
|
|
163
|
+
localeFlat[key] = value;
|
|
164
|
+
}
|
|
165
|
+
data[locale] = unflattenObject(localeFlat);
|
|
166
|
+
await writeAllTranslations(cfg, data);
|
|
167
|
+
res.json({
|
|
168
|
+
ok: true,
|
|
169
|
+
locale,
|
|
170
|
+
updated: updates.length,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (error instanceof WriteLockError) {
|
|
175
|
+
res.status(409).json({ ok: false, error: error.message });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
res.status(400).json({
|
|
179
|
+
ok: false,
|
|
180
|
+
error: error.message || "Failed to parse XLIFF content.",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
66
184
|
app.post("/api/translations", async (req, res) => {
|
|
67
185
|
const data = req.body;
|
|
68
|
-
|
|
69
|
-
|
|
186
|
+
try {
|
|
187
|
+
await writeAllTranslations(cfg, data);
|
|
188
|
+
res.json({ ok: true });
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error instanceof WriteLockError) {
|
|
192
|
+
res.status(409).json({ ok: false, error: error.message });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
res.status(500).json({
|
|
196
|
+
ok: false,
|
|
197
|
+
error: "Failed to write translation files.",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
70
200
|
});
|
|
71
201
|
app.post("/api/rename-key", async (req, res) => {
|
|
72
202
|
const body = req.body;
|
|
@@ -105,8 +235,8 @@ export function createServerApp(cfg) {
|
|
|
105
235
|
}
|
|
106
236
|
return app;
|
|
107
237
|
}
|
|
108
|
-
export async function startServer(cfg, port = 5179) {
|
|
109
|
-
const app =
|
|
238
|
+
export async function startServer(cfg, port = 5179, runtimeOptions = {}) {
|
|
239
|
+
const app = createServerAppWithOptions(cfg, runtimeOptions);
|
|
110
240
|
const server = await new Promise((resolve) => {
|
|
111
241
|
const nextServer = app.listen(port, () => resolve(nextServer));
|
|
112
242
|
});
|
package/dist/translationTree.js
CHANGED
|
@@ -20,3 +20,23 @@ export const flattenObject = (obj) => {
|
|
|
20
20
|
visit(obj, "");
|
|
21
21
|
return result;
|
|
22
22
|
};
|
|
23
|
+
export const unflattenObject = (flat) => {
|
|
24
|
+
const root = {};
|
|
25
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
26
|
+
const parts = key.split(".").filter((part) => part.length > 0);
|
|
27
|
+
if (parts.length === 0) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
let cursor = root;
|
|
31
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
32
|
+
const part = parts[index];
|
|
33
|
+
const existing = cursor[part];
|
|
34
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
|
35
|
+
cursor[part] = {};
|
|
36
|
+
}
|
|
37
|
+
cursor = cursor[part];
|
|
38
|
+
}
|
|
39
|
+
cursor[parts[parts.length - 1]] = value;
|
|
40
|
+
}
|
|
41
|
+
return root;
|
|
42
|
+
};
|