glotfile 0.2.0 → 0.3.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 +4 -3
- package/dist/server/cli.js +640 -231
- package/dist/server/server.js +540 -189
- package/dist/ui/assets/en-BZRN_IpI.svg +25 -0
- package/dist/ui/assets/index-DVTJ7ZX_.css +1 -0
- package/dist/ui/assets/index-LjEnW4jC.js +1847 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -2
- package/dist/ui/assets/index-BqcYDTXL.css +0 -1
- package/dist/ui/assets/index-DB5e5FME.js +0 -1819
package/dist/server/cli.js
CHANGED
|
@@ -31,6 +31,32 @@ var init_format = __esm({
|
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
// src/server/atomic-write.ts
|
|
35
|
+
import { writeFileSync, renameSync, mkdirSync, rmSync } from "fs";
|
|
36
|
+
import { dirname, join } from "path";
|
|
37
|
+
function writeFileAtomic(path, data) {
|
|
38
|
+
const dir = dirname(path);
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
const tmp = join(dir, `.${process.pid}.${counter++}.tmp`);
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(tmp, data);
|
|
43
|
+
renameSync(tmp, path);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
try {
|
|
46
|
+
rmSync(tmp, { force: true });
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
var counter;
|
|
53
|
+
var init_atomic_write = __esm({
|
|
54
|
+
"src/server/atomic-write.ts"() {
|
|
55
|
+
"use strict";
|
|
56
|
+
counter = 0;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
34
60
|
// src/server/lint/registry.ts
|
|
35
61
|
var RULE_IDS, DEFAULT_SEVERITY;
|
|
36
62
|
var init_registry = __esm({
|
|
@@ -117,17 +143,6 @@ function validate(raw) {
|
|
|
117
143
|
}
|
|
118
144
|
}
|
|
119
145
|
}
|
|
120
|
-
if (!isObject(config.ai)) fail("config.ai must be an object");
|
|
121
|
-
const ai = config.ai;
|
|
122
|
-
if (typeof ai.provider !== "string" || !PROVIDERS.includes(ai.provider)) {
|
|
123
|
-
fail(`config.ai.provider must be one of: ${PROVIDERS.join(", ")}`);
|
|
124
|
-
}
|
|
125
|
-
if (typeof ai.model !== "string") fail("config.ai.model must be a string");
|
|
126
|
-
if (!(ai.endpoint === null || typeof ai.endpoint === "string")) fail("config.ai.endpoint must be a string or null");
|
|
127
|
-
if (!(ai.region === void 0 || ai.region === null || typeof ai.region === "string")) {
|
|
128
|
-
fail("config.ai.region must be a string or null");
|
|
129
|
-
}
|
|
130
|
-
if (typeof ai.batchSize !== "number") fail("config.ai.batchSize must be a number");
|
|
131
146
|
if (!isObject(config.format)) fail("config.format must be an object");
|
|
132
147
|
const fmt = config.format;
|
|
133
148
|
if (typeof fmt.indent !== "number") fail("config.format.indent must be a number");
|
|
@@ -258,7 +273,6 @@ function defaultState() {
|
|
|
258
273
|
{ adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" },
|
|
259
274
|
{ adapter: "laravel-php", path: "lang/{locale}/{namespace}.php" }
|
|
260
275
|
],
|
|
261
|
-
ai: { provider: "anthropic", model: "claude-haiku-4-5-20251001", endpoint: null, region: null, batchSize: 25 },
|
|
262
276
|
format: { indent: 2, sortKeys: true, finalNewline: true },
|
|
263
277
|
spelling: { customWords: [] },
|
|
264
278
|
autoExport: true
|
|
@@ -277,7 +291,7 @@ var init_schema = __esm({
|
|
|
277
291
|
PLURAL_CATEGORIES = ["zero", "one", "two", "few", "many", "other"];
|
|
278
292
|
EXACT_SELECTOR_RE = /^=\d+$/;
|
|
279
293
|
LOCALE_CASES = ["lower-hyphen", "lower-underscore", "bcp47-hyphen", "bcp47-underscore"];
|
|
280
|
-
PROVIDERS = ["anthropic", "openai", "bedrock", "openrouter"];
|
|
294
|
+
PROVIDERS = ["anthropic", "openai", "bedrock", "openrouter", "ollama", "claude-code"];
|
|
281
295
|
GlotfileError = class extends Error {
|
|
282
296
|
};
|
|
283
297
|
}
|
|
@@ -402,13 +416,13 @@ var init_plurals = __esm({
|
|
|
402
416
|
});
|
|
403
417
|
|
|
404
418
|
// src/server/storage.ts
|
|
405
|
-
import { existsSync, readFileSync,
|
|
406
|
-
import { join
|
|
419
|
+
import { existsSync, readFileSync, mkdirSync as mkdirSync2, readdirSync, rmSync as rmSync2 } from "fs";
|
|
420
|
+
import { join as join2 } from "path";
|
|
407
421
|
function splitDirFor(statePath) {
|
|
408
422
|
return statePath.replace(/\.json$/i, "");
|
|
409
423
|
}
|
|
410
424
|
function detectFormat(statePath) {
|
|
411
|
-
if (existsSync(
|
|
425
|
+
if (existsSync(join2(splitDirFor(statePath), "config.json"))) return "split";
|
|
412
426
|
if (existsSync(statePath)) return "single";
|
|
413
427
|
return "none";
|
|
414
428
|
}
|
|
@@ -438,15 +452,15 @@ function assemble(parts) {
|
|
|
438
452
|
return { ...parts.manifest, keys };
|
|
439
453
|
}
|
|
440
454
|
function loadSplit(splitDir) {
|
|
441
|
-
const
|
|
442
|
-
const manifest =
|
|
443
|
-
const keysPath =
|
|
444
|
-
const keys = existsSync(keysPath) ?
|
|
445
|
-
const localesDir =
|
|
455
|
+
const readJson3 = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
456
|
+
const manifest = readJson3(join2(splitDir, "config.json"));
|
|
457
|
+
const keysPath = join2(splitDir, "keys.json");
|
|
458
|
+
const keys = existsSync(keysPath) ? readJson3(keysPath) : {};
|
|
459
|
+
const localesDir = join2(splitDir, "locales");
|
|
446
460
|
const locales = {};
|
|
447
461
|
if (existsSync(localesDir)) {
|
|
448
462
|
for (const name of readdirSync(localesDir)) {
|
|
449
|
-
if (name.endsWith(".json")) locales[name.slice(0, -5)] =
|
|
463
|
+
if (name.endsWith(".json")) locales[name.slice(0, -5)] = readJson3(join2(localesDir, name));
|
|
450
464
|
}
|
|
451
465
|
}
|
|
452
466
|
return assemble({ manifest, keys, locales });
|
|
@@ -458,15 +472,14 @@ function writeIfChanged(path, contents) {
|
|
|
458
472
|
} catch {
|
|
459
473
|
}
|
|
460
474
|
if (current === contents) return false;
|
|
461
|
-
|
|
462
|
-
writeFileSync(path, contents, "utf8");
|
|
475
|
+
writeFileAtomic(path, contents);
|
|
463
476
|
return true;
|
|
464
477
|
}
|
|
465
478
|
function saveSplit(splitDir, state) {
|
|
466
479
|
const fmt = state.config.format;
|
|
467
480
|
const parts = disassemble(state);
|
|
468
|
-
const localesDir =
|
|
469
|
-
|
|
481
|
+
const localesDir = join2(splitDir, "locales");
|
|
482
|
+
mkdirSync2(localesDir, { recursive: true });
|
|
470
483
|
let written = 0;
|
|
471
484
|
let skipped = 0;
|
|
472
485
|
let deleted = 0;
|
|
@@ -474,15 +487,15 @@ function saveSplit(splitDir, state) {
|
|
|
474
487
|
if (changed) written++;
|
|
475
488
|
else skipped++;
|
|
476
489
|
};
|
|
477
|
-
track(writeIfChanged(
|
|
478
|
-
track(writeIfChanged(
|
|
490
|
+
track(writeIfChanged(join2(splitDir, "config.json"), serializeJson(parts.manifest, fmt)));
|
|
491
|
+
track(writeIfChanged(join2(splitDir, "keys.json"), serializeJson(parts.keys, fmt)));
|
|
479
492
|
for (const [locale, entries] of Object.entries(parts.locales)) {
|
|
480
|
-
track(writeIfChanged(
|
|
493
|
+
track(writeIfChanged(join2(localesDir, `${locale}.json`), serializeJson(entries, fmt)));
|
|
481
494
|
}
|
|
482
495
|
const live = new Set(Object.keys(parts.locales).map((l) => `${l}.json`));
|
|
483
496
|
for (const name of readdirSync(localesDir)) {
|
|
484
497
|
if (name.endsWith(".json") && !live.has(name)) {
|
|
485
|
-
|
|
498
|
+
rmSync2(join2(localesDir, name));
|
|
486
499
|
deleted++;
|
|
487
500
|
}
|
|
488
501
|
}
|
|
@@ -492,6 +505,7 @@ var init_storage = __esm({
|
|
|
492
505
|
"src/server/storage.ts"() {
|
|
493
506
|
"use strict";
|
|
494
507
|
init_format();
|
|
508
|
+
init_atomic_write();
|
|
495
509
|
}
|
|
496
510
|
});
|
|
497
511
|
|
|
@@ -506,8 +520,7 @@ var init_normalize = __esm({
|
|
|
506
520
|
});
|
|
507
521
|
|
|
508
522
|
// src/server/state.ts
|
|
509
|
-
import { readFileSync as readFileSync2,
|
|
510
|
-
import { dirname as dirname2 } from "path";
|
|
523
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, rmSync as rmSync3 } from "fs";
|
|
511
524
|
import { randomUUID } from "crypto";
|
|
512
525
|
function canonLocale(locale) {
|
|
513
526
|
return locale.trim().toLowerCase().replace(/_/g, "-");
|
|
@@ -552,10 +565,9 @@ function saveState(path, state) {
|
|
|
552
565
|
normalizeState(state);
|
|
553
566
|
if (state.config.storage === "split") {
|
|
554
567
|
saveSplit(splitDirFor(path), state);
|
|
555
|
-
if (existsSync2(path))
|
|
568
|
+
if (existsSync2(path)) rmSync3(path);
|
|
556
569
|
} else {
|
|
557
|
-
|
|
558
|
-
writeFileSync2(path, serializeJson(state, state.config.format), "utf8");
|
|
570
|
+
writeFileAtomic(path, serializeJson(state, state.config.format));
|
|
559
571
|
}
|
|
560
572
|
}
|
|
561
573
|
function requireKey(state, key) {
|
|
@@ -703,6 +715,9 @@ function setMetadata(state, key, partial) {
|
|
|
703
715
|
delete entry.contextAt;
|
|
704
716
|
}
|
|
705
717
|
Object.assign(entry, safe);
|
|
718
|
+
if ("context" in safe && !entry.context) delete entry.context;
|
|
719
|
+
if ("tags" in safe && !entry.tags?.length) delete entry.tags;
|
|
720
|
+
if ("maxLength" in safe && !entry.maxLength) delete entry.maxLength;
|
|
706
721
|
}
|
|
707
722
|
function addNote(state, key, text, clock = systemClock) {
|
|
708
723
|
const entry = requireKey(state, key);
|
|
@@ -761,6 +776,7 @@ var init_state = __esm({
|
|
|
761
776
|
"src/server/state.ts"() {
|
|
762
777
|
"use strict";
|
|
763
778
|
init_format();
|
|
779
|
+
init_atomic_write();
|
|
764
780
|
init_schema();
|
|
765
781
|
init_plurals();
|
|
766
782
|
init_storage();
|
|
@@ -1494,8 +1510,8 @@ var init_adapters = __esm({
|
|
|
1494
1510
|
});
|
|
1495
1511
|
|
|
1496
1512
|
// src/server/export-run.ts
|
|
1497
|
-
import {
|
|
1498
|
-
import {
|
|
1513
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1514
|
+
import { resolve } from "path";
|
|
1499
1515
|
function effectiveLocales(config) {
|
|
1500
1516
|
const limit = config.exportLocales;
|
|
1501
1517
|
if (!limit || limit.length === 0) return config.locales;
|
|
@@ -1532,8 +1548,7 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
1532
1548
|
skipped++;
|
|
1533
1549
|
continue;
|
|
1534
1550
|
}
|
|
1535
|
-
|
|
1536
|
-
writeFileSync3(abs, f.contents, "utf8");
|
|
1551
|
+
writeFileAtomic(abs, f.contents);
|
|
1537
1552
|
written++;
|
|
1538
1553
|
}
|
|
1539
1554
|
}
|
|
@@ -1543,6 +1558,7 @@ var init_export_run = __esm({
|
|
|
1543
1558
|
"src/server/export-run.ts"() {
|
|
1544
1559
|
"use strict";
|
|
1545
1560
|
init_adapters();
|
|
1561
|
+
init_atomic_write();
|
|
1546
1562
|
}
|
|
1547
1563
|
});
|
|
1548
1564
|
|
|
@@ -1636,7 +1652,7 @@ function validateTranslation(req, translation) {
|
|
|
1636
1652
|
return { id: req.id, error: "Placeholder mismatch between source and translation." };
|
|
1637
1653
|
}
|
|
1638
1654
|
if (req.maxLength !== void 0 && translation.length > req.maxLength) {
|
|
1639
|
-
return { id: req.id, error: `Exceeds maxLength (${translation.length} > ${req.maxLength}).` };
|
|
1655
|
+
return { id: req.id, translation, error: `Exceeds maxLength (${translation.length} > ${req.maxLength}).` };
|
|
1640
1656
|
}
|
|
1641
1657
|
return { id: req.id, translation };
|
|
1642
1658
|
}
|
|
@@ -1888,7 +1904,7 @@ var init_bedrock = __esm({
|
|
|
1888
1904
|
}
|
|
1889
1905
|
const region = config.region ?? process.env.AWS_REGION;
|
|
1890
1906
|
if (!region) {
|
|
1891
|
-
throw new Error("AWS region is not set. Set
|
|
1907
|
+
throw new Error("AWS region is not set. Set the Region in your local AI settings (.glotfile/settings.json) or the AWS_REGION environment variable for the bedrock provider.");
|
|
1892
1908
|
}
|
|
1893
1909
|
const require2 = createRequire2(import.meta.url);
|
|
1894
1910
|
let sdk;
|
|
@@ -2016,9 +2032,131 @@ var init_openrouter = __esm({
|
|
|
2016
2032
|
}
|
|
2017
2033
|
});
|
|
2018
2034
|
|
|
2035
|
+
// src/server/ai/ollama.ts
|
|
2036
|
+
function ollamaClientOptions(config) {
|
|
2037
|
+
return {
|
|
2038
|
+
apiKey: process.env.OLLAMA_API_KEY ?? "ollama",
|
|
2039
|
+
baseURL: config.endpoint ?? OLLAMA_BASE_URL
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
var OLLAMA_BASE_URL, OllamaProvider;
|
|
2043
|
+
var init_ollama = __esm({
|
|
2044
|
+
"src/server/ai/ollama.ts"() {
|
|
2045
|
+
"use strict";
|
|
2046
|
+
init_openai();
|
|
2047
|
+
OLLAMA_BASE_URL = "http://localhost:11434/v1";
|
|
2048
|
+
OllamaProvider = class extends OpenAIProvider {
|
|
2049
|
+
constructor(config, client) {
|
|
2050
|
+
super(config, client ?? loadOpenAIClient(ollamaClientOptions(config)));
|
|
2051
|
+
}
|
|
2052
|
+
supportsVision() {
|
|
2053
|
+
return this.config.vision === true;
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
// src/server/ai/claudecode.ts
|
|
2060
|
+
import { spawn } from "child_process";
|
|
2061
|
+
function stripFences(s) {
|
|
2062
|
+
return s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
|
2063
|
+
}
|
|
2064
|
+
function defaultSpawn(prompt, systemPrompt, model) {
|
|
2065
|
+
return new Promise((resolve12, reject) => {
|
|
2066
|
+
const args = [
|
|
2067
|
+
"--print",
|
|
2068
|
+
"--output-format",
|
|
2069
|
+
"json",
|
|
2070
|
+
"--system-prompt",
|
|
2071
|
+
systemPrompt,
|
|
2072
|
+
// Only pass --model when explicitly configured; otherwise use the session default.
|
|
2073
|
+
...model ? ["--model", model] : []
|
|
2074
|
+
];
|
|
2075
|
+
const child = spawn("claude", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
2076
|
+
let stdout = "";
|
|
2077
|
+
let stderr = "";
|
|
2078
|
+
child.stdout.on("data", (chunk2) => {
|
|
2079
|
+
stdout += chunk2.toString();
|
|
2080
|
+
});
|
|
2081
|
+
child.stderr.on("data", (chunk2) => {
|
|
2082
|
+
stderr += chunk2.toString();
|
|
2083
|
+
});
|
|
2084
|
+
child.stdin.write(prompt);
|
|
2085
|
+
child.stdin.end();
|
|
2086
|
+
child.on("close", (code) => {
|
|
2087
|
+
if (code !== 0) {
|
|
2088
|
+
reject(new Error(`claude exited with code ${code}: ${stderr.trim() || stdout.trim()}`));
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
try {
|
|
2092
|
+
const envelope = JSON.parse(stdout.trim());
|
|
2093
|
+
if (envelope.is_error) {
|
|
2094
|
+
reject(new Error(`claude error: ${envelope.result ?? "unknown error"}`));
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
resolve12(envelope.result ?? "");
|
|
2098
|
+
} catch {
|
|
2099
|
+
reject(new Error(`Failed to parse claude JSON output: ${stdout.slice(0, 200)}`));
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
child.on("error", (err) => {
|
|
2103
|
+
reject(new Error(`Failed to spawn claude: ${err.message}. Is Claude Code installed?`));
|
|
2104
|
+
});
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
var ClaudeCodeProvider;
|
|
2108
|
+
var init_claudecode = __esm({
|
|
2109
|
+
"src/server/ai/claudecode.ts"() {
|
|
2110
|
+
"use strict";
|
|
2111
|
+
init_provider();
|
|
2112
|
+
init_batch();
|
|
2113
|
+
ClaudeCodeProvider = class {
|
|
2114
|
+
constructor(config, spawnFn) {
|
|
2115
|
+
this.config = config;
|
|
2116
|
+
this.spawnFn = spawnFn ?? defaultSpawn;
|
|
2117
|
+
}
|
|
2118
|
+
config;
|
|
2119
|
+
spawnFn;
|
|
2120
|
+
supportsVision() {
|
|
2121
|
+
return false;
|
|
2122
|
+
}
|
|
2123
|
+
translate(reqs, onBatchComplete, signal) {
|
|
2124
|
+
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal);
|
|
2125
|
+
}
|
|
2126
|
+
async complete(req) {
|
|
2127
|
+
const systemParts = [req.system, `Respond with valid JSON matching this schema: ${JSON.stringify(req.schema)}`];
|
|
2128
|
+
const textBlocks = req.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n");
|
|
2129
|
+
const result = await this.spawnFn(textBlocks, systemParts.join("\n\n"), this.config.model);
|
|
2130
|
+
try {
|
|
2131
|
+
return JSON.parse(stripFences(result));
|
|
2132
|
+
} catch {
|
|
2133
|
+
return {};
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
async callBatch(batch, signal) {
|
|
2137
|
+
if (signal?.aborted) return [];
|
|
2138
|
+
const prompt = buildBatchPrompt(batch);
|
|
2139
|
+
let result;
|
|
2140
|
+
try {
|
|
2141
|
+
result = await this.spawnFn(prompt, buildSystemPrompt(), this.config.model);
|
|
2142
|
+
} catch (err) {
|
|
2143
|
+
if (signal?.aborted) return [];
|
|
2144
|
+
throw err;
|
|
2145
|
+
}
|
|
2146
|
+
if (signal?.aborted) return [];
|
|
2147
|
+
try {
|
|
2148
|
+
const parsed = JSON.parse(stripFences(result));
|
|
2149
|
+
return parsed.items ?? [];
|
|
2150
|
+
} catch {
|
|
2151
|
+
return [];
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2019
2158
|
// src/server/ai/index.ts
|
|
2020
|
-
function makeProvider(
|
|
2021
|
-
const ai = config.ai;
|
|
2159
|
+
function makeProvider(ai) {
|
|
2022
2160
|
switch (ai.provider) {
|
|
2023
2161
|
case "anthropic":
|
|
2024
2162
|
return new AnthropicProvider(ai);
|
|
@@ -2028,8 +2166,12 @@ function makeProvider(config) {
|
|
|
2028
2166
|
return new BedrockProvider(ai);
|
|
2029
2167
|
case "openrouter":
|
|
2030
2168
|
return new OpenRouterProvider(ai);
|
|
2169
|
+
case "ollama":
|
|
2170
|
+
return new OllamaProvider(ai);
|
|
2171
|
+
case "claude-code":
|
|
2172
|
+
return new ClaudeCodeProvider(ai);
|
|
2031
2173
|
default:
|
|
2032
|
-
throw new Error(`Unknown AI provider "${String(ai.provider)}". Supported: anthropic, openai, bedrock, openrouter.`);
|
|
2174
|
+
throw new Error(`Unknown AI provider "${String(ai.provider)}". Supported: anthropic, openai, bedrock, openrouter, ollama, claude-code.`);
|
|
2033
2175
|
}
|
|
2034
2176
|
}
|
|
2035
2177
|
var init_ai = __esm({
|
|
@@ -2039,6 +2181,96 @@ var init_ai = __esm({
|
|
|
2039
2181
|
init_openai();
|
|
2040
2182
|
init_bedrock();
|
|
2041
2183
|
init_openrouter();
|
|
2184
|
+
init_ollama();
|
|
2185
|
+
init_claudecode();
|
|
2186
|
+
}
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
// src/server/glotfile-dir.ts
|
|
2190
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2191
|
+
import { resolve as resolve2 } from "path";
|
|
2192
|
+
function ensureGlotfileDir(projectRoot) {
|
|
2193
|
+
const dir = resolve2(projectRoot, ".glotfile");
|
|
2194
|
+
mkdirSync3(dir, { recursive: true });
|
|
2195
|
+
const ignore = resolve2(dir, ".gitignore");
|
|
2196
|
+
if (!existsSync3(ignore)) {
|
|
2197
|
+
try {
|
|
2198
|
+
writeFileSync2(ignore, "*\n");
|
|
2199
|
+
} catch {
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
return dir;
|
|
2203
|
+
}
|
|
2204
|
+
var init_glotfile_dir = __esm({
|
|
2205
|
+
"src/server/glotfile-dir.ts"() {
|
|
2206
|
+
"use strict";
|
|
2207
|
+
}
|
|
2208
|
+
});
|
|
2209
|
+
|
|
2210
|
+
// src/server/local-settings.ts
|
|
2211
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2212
|
+
import { resolve as resolve3 } from "path";
|
|
2213
|
+
function readJson(path) {
|
|
2214
|
+
try {
|
|
2215
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
2216
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
2217
|
+
} catch {
|
|
2218
|
+
return {};
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
function coerceAi(raw) {
|
|
2222
|
+
const a = raw && typeof raw === "object" ? raw : {};
|
|
2223
|
+
return {
|
|
2224
|
+
provider: PROVIDERS.includes(a.provider) ? a.provider : DEFAULT_AI.provider,
|
|
2225
|
+
model: typeof a.model === "string" && a.model ? a.model : DEFAULT_AI.model,
|
|
2226
|
+
endpoint: typeof a.endpoint === "string" ? a.endpoint : null,
|
|
2227
|
+
region: typeof a.region === "string" ? a.region : null,
|
|
2228
|
+
batchSize: typeof a.batchSize === "number" && a.batchSize > 0 ? a.batchSize : DEFAULT_AI.batchSize,
|
|
2229
|
+
vision: typeof a.vision === "boolean" ? a.vision : void 0
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
function loadLocalSettings(projectRoot) {
|
|
2233
|
+
const raw = readJson(settingsPath(projectRoot));
|
|
2234
|
+
return { ai: coerceAi(raw.ai), editor: isEditorId(raw.editor) ? raw.editor : DEFAULT_EDITOR };
|
|
2235
|
+
}
|
|
2236
|
+
function saveLocalSettings(projectRoot, patch) {
|
|
2237
|
+
const path = settingsPath(projectRoot);
|
|
2238
|
+
const merged = { ...readJson(path) };
|
|
2239
|
+
if (patch.ai !== void 0) merged.ai = patch.ai;
|
|
2240
|
+
if (patch.editor !== void 0) merged.editor = patch.editor;
|
|
2241
|
+
ensureGlotfileDir(projectRoot);
|
|
2242
|
+
writeFileAtomic(path, JSON.stringify(merged, null, 2) + "\n");
|
|
2243
|
+
}
|
|
2244
|
+
function aiConfigError(ai) {
|
|
2245
|
+
if (!ai || typeof ai !== "object") return "ai must be an object";
|
|
2246
|
+
const a = ai;
|
|
2247
|
+
if (typeof a.provider !== "string" || !PROVIDERS.includes(a.provider)) {
|
|
2248
|
+
return `ai.provider must be one of: ${PROVIDERS.join(", ")}`;
|
|
2249
|
+
}
|
|
2250
|
+
if (typeof a.model !== "string") return "ai.model must be a string";
|
|
2251
|
+
if (!(a.endpoint === null || a.endpoint === void 0 || typeof a.endpoint === "string")) return "ai.endpoint must be a string or null";
|
|
2252
|
+
if (!(a.region === void 0 || a.region === null || typeof a.region === "string")) return "ai.region must be a string or null";
|
|
2253
|
+
if (typeof a.batchSize !== "number") return "ai.batchSize must be a number";
|
|
2254
|
+
return null;
|
|
2255
|
+
}
|
|
2256
|
+
var EDITOR_IDS, isEditorId, DEFAULT_AI, DEFAULT_EDITOR, settingsPath;
|
|
2257
|
+
var init_local_settings = __esm({
|
|
2258
|
+
"src/server/local-settings.ts"() {
|
|
2259
|
+
"use strict";
|
|
2260
|
+
init_atomic_write();
|
|
2261
|
+
init_glotfile_dir();
|
|
2262
|
+
init_schema();
|
|
2263
|
+
EDITOR_IDS = ["vscode", "zed", "phpstorm"];
|
|
2264
|
+
isEditorId = (v) => EDITOR_IDS.includes(v);
|
|
2265
|
+
DEFAULT_AI = {
|
|
2266
|
+
provider: "anthropic",
|
|
2267
|
+
model: "claude-haiku-4-5-20251001",
|
|
2268
|
+
endpoint: null,
|
|
2269
|
+
region: null,
|
|
2270
|
+
batchSize: 25
|
|
2271
|
+
};
|
|
2272
|
+
DEFAULT_EDITOR = "vscode";
|
|
2273
|
+
settingsPath = (projectRoot) => resolve3(projectRoot, ".glotfile", "settings.json");
|
|
2042
2274
|
}
|
|
2043
2275
|
});
|
|
2044
2276
|
|
|
@@ -2054,8 +2286,8 @@ var init_glob = __esm({
|
|
|
2054
2286
|
});
|
|
2055
2287
|
|
|
2056
2288
|
// src/server/ai/run.ts
|
|
2057
|
-
import { readFileSync as
|
|
2058
|
-
import { resolve as
|
|
2289
|
+
import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
|
|
2290
|
+
import { resolve as resolve4, extname } from "path";
|
|
2059
2291
|
function selectRequests(state, opts) {
|
|
2060
2292
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
2061
2293
|
const keyRe = opts.keyGlob ? globToRegExp(opts.keyGlob) : null;
|
|
@@ -2134,11 +2366,11 @@ function attachScreenshots(reqs, state, projectRoot) {
|
|
|
2134
2366
|
const mediaType = MEDIA_TYPES[extname(screenshot).toLowerCase()];
|
|
2135
2367
|
if (!mediaType) continue;
|
|
2136
2368
|
if (!cache2.has(screenshot)) {
|
|
2137
|
-
const abs =
|
|
2138
|
-
if (!
|
|
2369
|
+
const abs = resolve4(projectRoot, screenshot);
|
|
2370
|
+
if (!existsSync4(abs)) {
|
|
2139
2371
|
cache2.set(screenshot, null);
|
|
2140
2372
|
} else {
|
|
2141
|
-
const buf =
|
|
2373
|
+
const buf = readFileSync5(abs);
|
|
2142
2374
|
cache2.set(screenshot, buf.length > MAX_IMAGE_BYTES ? null : { mediaType, base64: buf.toString("base64") });
|
|
2143
2375
|
}
|
|
2144
2376
|
}
|
|
@@ -2154,7 +2386,7 @@ function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision)
|
|
|
2154
2386
|
const keys = new Set(reqs.filter((r) => state.keys[r.key]?.screenshot).map((r) => r.key));
|
|
2155
2387
|
return { skipped: keys.size };
|
|
2156
2388
|
}
|
|
2157
|
-
async function runLocaleParallel(reqs, provider,
|
|
2389
|
+
async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal) {
|
|
2158
2390
|
if (!reqs.length) return [];
|
|
2159
2391
|
const byLocale = /* @__PURE__ */ new Map();
|
|
2160
2392
|
for (const req of reqs) {
|
|
@@ -2174,11 +2406,14 @@ async function runLocaleParallel(reqs, provider, onBatchComplete, concurrency =
|
|
|
2174
2406
|
while (next < groups.length) {
|
|
2175
2407
|
if (signal?.aborted) break;
|
|
2176
2408
|
const group = groups[next++];
|
|
2409
|
+
const locale = group[0].targetLocale;
|
|
2410
|
+
hooks.onLocaleStart?.(locale);
|
|
2177
2411
|
const localeResults = await provider.translate(group, (_localeDone, _localeTotal, batchResults) => {
|
|
2178
2412
|
done += batchResults.length;
|
|
2179
|
-
onBatchComplete?.(done, total, batchResults);
|
|
2413
|
+
hooks.onBatchComplete?.(done, total, batchResults, locale);
|
|
2180
2414
|
}, signal);
|
|
2181
2415
|
allResults.push(...localeResults);
|
|
2416
|
+
if (!signal?.aborted) hooks.onLocaleDone?.(locale);
|
|
2182
2417
|
}
|
|
2183
2418
|
}
|
|
2184
2419
|
const workers = Array.from({ length: Math.min(concurrency, groups.length) }, worker);
|
|
@@ -2200,10 +2435,11 @@ function applyResults(state, reqs, results, clock = systemClock, force = false)
|
|
|
2200
2435
|
if (applyMachineTranslationForms(state, req.key, req.targetLocale, res.forms, clock, force)) written++;
|
|
2201
2436
|
continue;
|
|
2202
2437
|
}
|
|
2203
|
-
if (res.
|
|
2438
|
+
if (res.translation === void 0) {
|
|
2204
2439
|
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
2205
2440
|
continue;
|
|
2206
2441
|
}
|
|
2442
|
+
if (res.error) errors.push({ key: req.key, locale: req.targetLocale, error: res.error });
|
|
2207
2443
|
if (applyMachineTranslation(state, req.key, req.targetLocale, res.translation, clock, force)) written++;
|
|
2208
2444
|
}
|
|
2209
2445
|
return { written, errors };
|
|
@@ -2228,49 +2464,46 @@ var init_run = __esm({
|
|
|
2228
2464
|
}
|
|
2229
2465
|
});
|
|
2230
2466
|
|
|
2231
|
-
// src/server/
|
|
2232
|
-
import { appendFileSync, readFileSync as
|
|
2233
|
-
import { resolve as
|
|
2467
|
+
// src/server/log.ts
|
|
2468
|
+
import { appendFileSync, readFileSync as readFileSync6, existsSync as existsSync5 } from "fs";
|
|
2469
|
+
import { resolve as resolve5 } from "path";
|
|
2234
2470
|
function logPath(projectRoot) {
|
|
2235
|
-
return
|
|
2471
|
+
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
2236
2472
|
}
|
|
2237
|
-
function
|
|
2238
|
-
|
|
2473
|
+
function appendLog(projectRoot, entry) {
|
|
2474
|
+
ensureGlotfileDir(projectRoot);
|
|
2239
2475
|
appendFileSync(logPath(projectRoot), JSON.stringify(entry) + "\n", "utf8");
|
|
2240
2476
|
}
|
|
2241
|
-
function
|
|
2477
|
+
function readLog(projectRoot, limit = 100) {
|
|
2242
2478
|
const path = logPath(projectRoot);
|
|
2243
|
-
if (!
|
|
2244
|
-
const lines =
|
|
2245
|
-
const entries = lines.map((l) =>
|
|
2246
|
-
const e = JSON.parse(l);
|
|
2247
|
-
e.kind ??= "translate";
|
|
2248
|
-
return e;
|
|
2249
|
-
});
|
|
2479
|
+
if (!existsSync5(path)) return [];
|
|
2480
|
+
const lines = readFileSync6(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
2481
|
+
const entries = lines.map((l) => JSON.parse(l));
|
|
2250
2482
|
return entries.reverse().slice(0, limit);
|
|
2251
2483
|
}
|
|
2252
2484
|
var init_log = __esm({
|
|
2253
|
-
"src/server/
|
|
2485
|
+
"src/server/log.ts"() {
|
|
2254
2486
|
"use strict";
|
|
2487
|
+
init_glotfile_dir();
|
|
2255
2488
|
}
|
|
2256
2489
|
});
|
|
2257
2490
|
|
|
2258
2491
|
// src/server/scan.ts
|
|
2259
|
-
import { existsSync as
|
|
2260
|
-
import { resolve as
|
|
2492
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
2493
|
+
import { resolve as resolve6 } from "path";
|
|
2261
2494
|
function loadUsageCache(projectRoot) {
|
|
2262
|
-
const path =
|
|
2263
|
-
if (!
|
|
2495
|
+
const path = resolve6(projectRoot, ".glotfile", "usage.json");
|
|
2496
|
+
if (!existsSync6(path)) return null;
|
|
2264
2497
|
try {
|
|
2265
|
-
return JSON.parse(
|
|
2498
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
2266
2499
|
} catch {
|
|
2267
2500
|
return null;
|
|
2268
2501
|
}
|
|
2269
2502
|
}
|
|
2270
2503
|
function saveUsageCache(projectRoot, cache2) {
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2504
|
+
ensureGlotfileDir(projectRoot);
|
|
2505
|
+
const path = resolve6(projectRoot, ".glotfile", "usage.json");
|
|
2506
|
+
writeFileAtomic(path, JSON.stringify(cache2, null, 2) + "\n");
|
|
2274
2507
|
}
|
|
2275
2508
|
function findMissing(state) {
|
|
2276
2509
|
const targets = state.config.locales.filter((l) => l !== state.config.sourceLocale).sort();
|
|
@@ -2297,12 +2530,14 @@ function computeUsedKeys(state, cache2) {
|
|
|
2297
2530
|
var init_scan = __esm({
|
|
2298
2531
|
"src/server/scan.ts"() {
|
|
2299
2532
|
"use strict";
|
|
2533
|
+
init_atomic_write();
|
|
2534
|
+
init_glotfile_dir();
|
|
2300
2535
|
}
|
|
2301
2536
|
});
|
|
2302
2537
|
|
|
2303
2538
|
// src/server/scanner.ts
|
|
2304
|
-
import { readdirSync as readdirSync2, statSync, readFileSync as
|
|
2305
|
-
import { join as
|
|
2539
|
+
import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync8 } from "fs";
|
|
2540
|
+
import { join as join3, extname as extname2, relative } from "path";
|
|
2306
2541
|
function scannerForExt(ext) {
|
|
2307
2542
|
return EXT_SCANNER[ext] ?? null;
|
|
2308
2543
|
}
|
|
@@ -2428,7 +2663,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
2428
2663
|
}
|
|
2429
2664
|
for (const name of entries) {
|
|
2430
2665
|
if (ALWAYS_EXCLUDE.has(name)) continue;
|
|
2431
|
-
const abs =
|
|
2666
|
+
const abs = join3(dir, name);
|
|
2432
2667
|
const rel = relative(root, abs);
|
|
2433
2668
|
let st;
|
|
2434
2669
|
try {
|
|
@@ -2458,7 +2693,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
2458
2693
|
const ext = extname2(relPath);
|
|
2459
2694
|
const scanner = scannerForExt(ext);
|
|
2460
2695
|
if (!scanner) continue;
|
|
2461
|
-
const abs =
|
|
2696
|
+
const abs = join3(projectRoot, relPath);
|
|
2462
2697
|
let st;
|
|
2463
2698
|
try {
|
|
2464
2699
|
st = statSync(abs);
|
|
@@ -2474,7 +2709,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
2474
2709
|
}
|
|
2475
2710
|
let content;
|
|
2476
2711
|
try {
|
|
2477
|
-
content =
|
|
2712
|
+
content = readFileSync8(abs, "utf8");
|
|
2478
2713
|
} catch {
|
|
2479
2714
|
continue;
|
|
2480
2715
|
}
|
|
@@ -2579,8 +2814,8 @@ var init_scanner = __esm({
|
|
|
2579
2814
|
});
|
|
2580
2815
|
|
|
2581
2816
|
// src/server/ai/context.ts
|
|
2582
|
-
import { existsSync as
|
|
2583
|
-
import { resolve as
|
|
2817
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
|
|
2818
|
+
import { resolve as resolve7 } from "path";
|
|
2584
2819
|
function globToRegExp2(glob) {
|
|
2585
2820
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
2586
2821
|
return new RegExp(`^${escaped}$`);
|
|
@@ -2592,10 +2827,10 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
2592
2827
|
const extraRefs = filtered.length > MAX_SNIPPETS ? filtered.length - MAX_SNIPPETS : 0;
|
|
2593
2828
|
const snippets = [];
|
|
2594
2829
|
for (const ref of selected) {
|
|
2595
|
-
const absPath =
|
|
2830
|
+
const absPath = resolve7(projectRoot, ref.file);
|
|
2596
2831
|
if (!fileCache.has(ref.file)) {
|
|
2597
|
-
if (!
|
|
2598
|
-
const content =
|
|
2832
|
+
if (!existsSync7(absPath)) continue;
|
|
2833
|
+
const content = readFileSync9(absPath, "utf8");
|
|
2599
2834
|
fileCache.set(ref.file, content.split("\n"));
|
|
2600
2835
|
}
|
|
2601
2836
|
const lines = fileCache.get(ref.file);
|
|
@@ -2740,8 +2975,8 @@ var init_context = __esm({
|
|
|
2740
2975
|
});
|
|
2741
2976
|
|
|
2742
2977
|
// src/server/import/detect.ts
|
|
2743
|
-
import { existsSync as
|
|
2744
|
-
import { join as
|
|
2978
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
2979
|
+
import { join as join4 } from "path";
|
|
2745
2980
|
function safeIsDir(p) {
|
|
2746
2981
|
try {
|
|
2747
2982
|
return statSync2(p).isDirectory();
|
|
@@ -2750,7 +2985,7 @@ function safeIsDir(p) {
|
|
|
2750
2985
|
}
|
|
2751
2986
|
}
|
|
2752
2987
|
function listDirs(dir) {
|
|
2753
|
-
return readdirSync3(dir).filter((e) => safeIsDir(
|
|
2988
|
+
return readdirSync3(dir).filter((e) => safeIsDir(join4(dir, e)));
|
|
2754
2989
|
}
|
|
2755
2990
|
function fileCount(dir) {
|
|
2756
2991
|
try {
|
|
@@ -2764,22 +2999,22 @@ function pickSource(locales, sizeOf) {
|
|
|
2764
2999
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
2765
3000
|
}
|
|
2766
3001
|
function detectLaravel(root) {
|
|
2767
|
-
const localeRoot = [
|
|
3002
|
+
const localeRoot = [join4(root, "resources", "lang"), join4(root, "lang")].find(safeIsDir);
|
|
2768
3003
|
if (!localeRoot) return null;
|
|
2769
3004
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
2770
3005
|
if (locales.length === 0) return null;
|
|
2771
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
3006
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join4(localeRoot, loc)));
|
|
2772
3007
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
2773
3008
|
}
|
|
2774
3009
|
function detectVue(root) {
|
|
2775
3010
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
2776
|
-
const localeRoot =
|
|
3011
|
+
const localeRoot = join4(root, rel);
|
|
2777
3012
|
if (!safeIsDir(localeRoot)) continue;
|
|
2778
3013
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
2779
3014
|
if (locales.length >= 2) {
|
|
2780
3015
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
2781
3016
|
try {
|
|
2782
|
-
return statSync2(
|
|
3017
|
+
return statSync2(join4(localeRoot, `${loc}.json`)).size;
|
|
2783
3018
|
} catch {
|
|
2784
3019
|
return 0;
|
|
2785
3020
|
}
|
|
@@ -2791,7 +3026,7 @@ function detectVue(root) {
|
|
|
2791
3026
|
}
|
|
2792
3027
|
function detectArb(root) {
|
|
2793
3028
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
2794
|
-
const localeRoot =
|
|
3029
|
+
const localeRoot = join4(root, rel);
|
|
2795
3030
|
if (!safeIsDir(localeRoot)) continue;
|
|
2796
3031
|
const locales = readdirSync3(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
2797
3032
|
if (locales.length >= 1) {
|
|
@@ -2801,7 +3036,7 @@ function detectArb(root) {
|
|
|
2801
3036
|
return null;
|
|
2802
3037
|
}
|
|
2803
3038
|
function detect(root, formatOverride) {
|
|
2804
|
-
if (!
|
|
3039
|
+
if (!existsSync9(root)) return null;
|
|
2805
3040
|
if (formatOverride) {
|
|
2806
3041
|
const fn = BY_FORMAT[formatOverride];
|
|
2807
3042
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -2856,8 +3091,8 @@ var init_flatten = __esm({
|
|
|
2856
3091
|
});
|
|
2857
3092
|
|
|
2858
3093
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
2859
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
2860
|
-
import { join as
|
|
3094
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
|
|
3095
|
+
import { join as join5 } from "path";
|
|
2861
3096
|
var LOCALE_RE2, vueI18nJson2;
|
|
2862
3097
|
var init_vue_i18n_json2 = __esm({
|
|
2863
3098
|
"src/server/import/parsers/vue-i18n-json.ts"() {
|
|
@@ -2877,7 +3112,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
2877
3112
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
2878
3113
|
let data;
|
|
2879
3114
|
try {
|
|
2880
|
-
data = JSON.parse(
|
|
3115
|
+
data = JSON.parse(readFileSync11(join5(localeRoot, file), "utf8"));
|
|
2881
3116
|
} catch (e) {
|
|
2882
3117
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
2883
3118
|
continue;
|
|
@@ -2905,16 +3140,16 @@ var init_placeholders2 = __esm({
|
|
|
2905
3140
|
|
|
2906
3141
|
// src/server/import/parsers/laravel-php.ts
|
|
2907
3142
|
import { readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
2908
|
-
import { join as
|
|
3143
|
+
import { join as join6, relative as relative2 } from "path";
|
|
2909
3144
|
import { execFileSync } from "child_process";
|
|
2910
3145
|
function listDirs2(dir) {
|
|
2911
|
-
return readdirSync5(dir).filter((e) => statSync3(
|
|
3146
|
+
return readdirSync5(dir).filter((e) => statSync3(join6(dir, e)).isDirectory());
|
|
2912
3147
|
}
|
|
2913
3148
|
function listPhpFiles(dir) {
|
|
2914
3149
|
const out = [];
|
|
2915
3150
|
const walk = (d) => {
|
|
2916
3151
|
for (const e of readdirSync5(d)) {
|
|
2917
|
-
const full =
|
|
3152
|
+
const full = join6(d, e);
|
|
2918
3153
|
if (statSync3(full).isDirectory()) walk(full);
|
|
2919
3154
|
else if (e.endsWith(".php")) out.push(full);
|
|
2920
3155
|
}
|
|
@@ -2957,7 +3192,7 @@ var init_laravel_php2 = __esm({
|
|
|
2957
3192
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
2958
3193
|
if (locale === "vendor") continue;
|
|
2959
3194
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
2960
|
-
const localeDir =
|
|
3195
|
+
const localeDir = join6(localeRoot, locale);
|
|
2961
3196
|
locales.push(locale);
|
|
2962
3197
|
for (const file of listPhpFiles(localeDir)) {
|
|
2963
3198
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -2982,8 +3217,8 @@ var init_laravel_php2 = __esm({
|
|
|
2982
3217
|
});
|
|
2983
3218
|
|
|
2984
3219
|
// src/server/import/parsers/flutter-arb.ts
|
|
2985
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
2986
|
-
import { join as
|
|
3220
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync12 } from "fs";
|
|
3221
|
+
import { join as join7 } from "path";
|
|
2987
3222
|
function localeFromArbName(file) {
|
|
2988
3223
|
const m = file.match(/^(.+)\.arb$/);
|
|
2989
3224
|
if (!m) return null;
|
|
@@ -3023,7 +3258,7 @@ var init_flutter_arb2 = __esm({
|
|
|
3023
3258
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
3024
3259
|
let data;
|
|
3025
3260
|
try {
|
|
3026
|
-
data = JSON.parse(
|
|
3261
|
+
data = JSON.parse(readFileSync12(join7(localeRoot, file), "utf8"));
|
|
3027
3262
|
} catch (e) {
|
|
3028
3263
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
3029
3264
|
continue;
|
|
@@ -3127,7 +3362,6 @@ function assemble2(parsed, opts) {
|
|
|
3127
3362
|
sourceLocale,
|
|
3128
3363
|
locales,
|
|
3129
3364
|
outputs: [output],
|
|
3130
|
-
ai: { provider: "anthropic", model: "claude-opus-4-8", endpoint: null, batchSize: 25 },
|
|
3131
3365
|
format: { indent: 2, sortKeys: true, finalNewline: true },
|
|
3132
3366
|
spelling: { customWords: [] }
|
|
3133
3367
|
},
|
|
@@ -3554,16 +3788,48 @@ var init_checks = __esm({
|
|
|
3554
3788
|
}
|
|
3555
3789
|
});
|
|
3556
3790
|
|
|
3791
|
+
// src/server/ui-prefs.ts
|
|
3792
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
3793
|
+
import { homedir } from "os";
|
|
3794
|
+
import { join as join8 } from "path";
|
|
3795
|
+
function readJson2(path) {
|
|
3796
|
+
try {
|
|
3797
|
+
const parsed = JSON.parse(readFileSync13(path, "utf8"));
|
|
3798
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3799
|
+
} catch {
|
|
3800
|
+
return {};
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
function loadUiPrefs(path) {
|
|
3804
|
+
const raw = readJson2(path);
|
|
3805
|
+
return { theme: isThemeMode(raw.theme) ? raw.theme : DEFAULTS.theme };
|
|
3806
|
+
}
|
|
3807
|
+
function saveUiPrefs(path, prefs) {
|
|
3808
|
+
const merged = { ...readJson2(path), ...prefs };
|
|
3809
|
+
writeFileAtomic(path, JSON.stringify(merged, null, 2) + "\n");
|
|
3810
|
+
}
|
|
3811
|
+
var THEMES, isThemeMode, defaultUiPrefsPath, DEFAULTS;
|
|
3812
|
+
var init_ui_prefs = __esm({
|
|
3813
|
+
"src/server/ui-prefs.ts"() {
|
|
3814
|
+
"use strict";
|
|
3815
|
+
init_atomic_write();
|
|
3816
|
+
THEMES = ["system", "light", "dark"];
|
|
3817
|
+
isThemeMode = (v) => THEMES.includes(v);
|
|
3818
|
+
defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
|
|
3819
|
+
DEFAULTS = { theme: "system" };
|
|
3820
|
+
}
|
|
3821
|
+
});
|
|
3822
|
+
|
|
3557
3823
|
// src/server/api.ts
|
|
3558
3824
|
import { Hono } from "hono";
|
|
3559
3825
|
import { streamSSE } from "hono/streaming";
|
|
3560
|
-
import {
|
|
3561
|
-
import { dirname as
|
|
3826
|
+
import { readFileSync as readFileSync14, existsSync as existsSync10, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "fs";
|
|
3827
|
+
import { dirname as dirname2, resolve as resolve9, basename, relative as relative3, sep } from "path";
|
|
3562
3828
|
function projectName(root) {
|
|
3563
|
-
const nameFile =
|
|
3564
|
-
if (
|
|
3829
|
+
const nameFile = resolve9(root, ".idea", ".name");
|
|
3830
|
+
if (existsSync10(nameFile)) {
|
|
3565
3831
|
try {
|
|
3566
|
-
const name =
|
|
3832
|
+
const name = readFileSync14(nameFile, "utf8").trim();
|
|
3567
3833
|
if (name) return name;
|
|
3568
3834
|
} catch {
|
|
3569
3835
|
}
|
|
@@ -3573,7 +3839,7 @@ function projectName(root) {
|
|
|
3573
3839
|
function createApi(deps) {
|
|
3574
3840
|
const app = new Hono();
|
|
3575
3841
|
const load = () => loadState(deps.statePath);
|
|
3576
|
-
const projectRoot =
|
|
3842
|
+
const projectRoot = dirname2(resolve9(deps.statePath));
|
|
3577
3843
|
let translateQueue = Promise.resolve();
|
|
3578
3844
|
const withTranslateLock = (fn) => {
|
|
3579
3845
|
const next = translateQueue.then(fn, fn);
|
|
@@ -3597,42 +3863,92 @@ function createApi(deps) {
|
|
|
3597
3863
|
saveState(deps.statePath, s);
|
|
3598
3864
|
scheduleAutoExport(s);
|
|
3599
3865
|
};
|
|
3866
|
+
const logChange = (entry) => appendLog(projectRoot, { ...entry, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3867
|
+
const valueText = (s, key, locale) => s.keys[key]?.values[locale]?.value;
|
|
3868
|
+
const uiPrefsPath = deps.uiPrefsPath ?? defaultUiPrefsPath();
|
|
3600
3869
|
app.get("/state", (c) => c.json(load()));
|
|
3870
|
+
app.get("/ui-prefs", (c) => c.json(loadUiPrefs(uiPrefsPath)));
|
|
3871
|
+
app.put("/ui-prefs", async (c) => {
|
|
3872
|
+
const { theme } = await c.req.json();
|
|
3873
|
+
if (!isThemeMode(theme)) return c.json({ error: "theme must be system, light, or dark" }, 400);
|
|
3874
|
+
saveUiPrefs(uiPrefsPath, { theme });
|
|
3875
|
+
return c.json({ ok: true });
|
|
3876
|
+
});
|
|
3877
|
+
app.get("/local-settings", (c) => c.json(loadLocalSettings(projectRoot)));
|
|
3878
|
+
app.put("/local-settings", async (c) => {
|
|
3879
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3880
|
+
const patch = {};
|
|
3881
|
+
if (body.ai !== void 0) {
|
|
3882
|
+
const err = aiConfigError(body.ai);
|
|
3883
|
+
if (err) return c.json({ error: err }, 400);
|
|
3884
|
+
patch.ai = body.ai;
|
|
3885
|
+
}
|
|
3886
|
+
if (body.editor !== void 0) {
|
|
3887
|
+
if (!isEditorId(body.editor)) return c.json({ error: "editor must be one of: vscode, zed, phpstorm" }, 400);
|
|
3888
|
+
patch.editor = body.editor;
|
|
3889
|
+
}
|
|
3890
|
+
if (patch.ai === void 0 && patch.editor === void 0) {
|
|
3891
|
+
return c.json({ error: "provide ai and/or editor" }, 400);
|
|
3892
|
+
}
|
|
3893
|
+
saveLocalSettings(projectRoot, patch);
|
|
3894
|
+
return c.json({ ok: true });
|
|
3895
|
+
});
|
|
3601
3896
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
3602
3897
|
app.get("/files", (c) => {
|
|
3603
3898
|
const found = /* @__PURE__ */ new Map();
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
}
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
abs = resolve7(projectRoot, `${name}.json`);
|
|
3614
|
-
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
3615
|
-
abs = resolve7(projectRoot, name);
|
|
3616
|
-
} else {
|
|
3617
|
-
continue;
|
|
3618
|
-
}
|
|
3619
|
-
if (found.has(abs)) continue;
|
|
3899
|
+
const activeRel = relative3(projectRoot, deps.statePath);
|
|
3900
|
+
found.set(deps.statePath, {
|
|
3901
|
+
name: basename(deps.statePath),
|
|
3902
|
+
path: deps.statePath,
|
|
3903
|
+
relDir: activeRel !== basename(activeRel) ? dirname2(activeRel) : void 0
|
|
3904
|
+
});
|
|
3905
|
+
function walk(dir, depth) {
|
|
3906
|
+
if (depth > 4) return;
|
|
3907
|
+
let entries = [];
|
|
3620
3908
|
try {
|
|
3621
|
-
|
|
3622
|
-
found.set(abs, { name: basename(abs), path: abs });
|
|
3909
|
+
entries = readdirSync7(dir);
|
|
3623
3910
|
} catch {
|
|
3911
|
+
return;
|
|
3912
|
+
}
|
|
3913
|
+
for (const name of entries) {
|
|
3914
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3915
|
+
const abs = resolve9(dir, name);
|
|
3916
|
+
let filePath = null;
|
|
3917
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync10(resolve9(abs, "config.json"))) {
|
|
3918
|
+
filePath = resolve9(dir, `${name}.json`);
|
|
3919
|
+
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
3920
|
+
filePath = abs;
|
|
3921
|
+
} else {
|
|
3922
|
+
try {
|
|
3923
|
+
if (statSync4(abs).isDirectory()) walk(abs, depth + 1);
|
|
3924
|
+
} catch {
|
|
3925
|
+
}
|
|
3926
|
+
continue;
|
|
3927
|
+
}
|
|
3928
|
+
if (found.has(filePath)) continue;
|
|
3929
|
+
try {
|
|
3930
|
+
loadState(filePath);
|
|
3931
|
+
const rel = relative3(projectRoot, filePath);
|
|
3932
|
+
found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname2(rel) : void 0 });
|
|
3933
|
+
} catch {
|
|
3934
|
+
}
|
|
3624
3935
|
}
|
|
3625
3936
|
}
|
|
3626
|
-
|
|
3937
|
+
walk(projectRoot, 0);
|
|
3938
|
+
const files = [...found.values()].sort((a, b) => {
|
|
3939
|
+
const ka = a.relDir ? `${a.relDir}/${a.name}` : a.name;
|
|
3940
|
+
const kb = b.relDir ? `${b.relDir}/${b.name}` : b.name;
|
|
3941
|
+
return ka.localeCompare(kb);
|
|
3942
|
+
});
|
|
3627
3943
|
return c.json(files);
|
|
3628
3944
|
});
|
|
3629
3945
|
app.post("/file", async (c) => {
|
|
3630
3946
|
const { path } = await c.req.json();
|
|
3631
3947
|
if (typeof path !== "string") return c.json({ error: "path must be a string" }, 400);
|
|
3632
|
-
const resolved =
|
|
3948
|
+
const resolved = resolve9(projectRoot, path);
|
|
3633
3949
|
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep);
|
|
3634
3950
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
3635
|
-
if (!
|
|
3951
|
+
if (!existsSync10(resolved)) return c.json({ error: "file not found" }, 400);
|
|
3636
3952
|
loadState(resolved);
|
|
3637
3953
|
deps.statePath = resolved;
|
|
3638
3954
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -3647,6 +3963,7 @@ function createApi(deps) {
|
|
|
3647
3963
|
const s = load();
|
|
3648
3964
|
createKey(s, key, value, void 0, plural ? { plural: { arg: plural.arg } } : {});
|
|
3649
3965
|
persist(s);
|
|
3966
|
+
logChange({ kind: "key", summary: `Created key ${key}`, key, after: value });
|
|
3650
3967
|
console.log(`[key] created ${key}`);
|
|
3651
3968
|
return c.json({ ok: true });
|
|
3652
3969
|
});
|
|
@@ -3656,37 +3973,45 @@ function createApi(deps) {
|
|
|
3656
3973
|
const s = load();
|
|
3657
3974
|
addCustomWord(s, word);
|
|
3658
3975
|
persist(s);
|
|
3976
|
+
logChange({ kind: "dictionary", summary: `Added "${word}" to dictionary`, after: word });
|
|
3659
3977
|
return c.json({ ok: true });
|
|
3660
3978
|
});
|
|
3661
3979
|
app.delete("/dictionary/:word", (c) => {
|
|
3662
3980
|
const s = load();
|
|
3663
|
-
|
|
3981
|
+
const word = c.req.param("word");
|
|
3982
|
+
removeCustomWord(s, word);
|
|
3664
3983
|
persist(s);
|
|
3984
|
+
logChange({ kind: "dictionary", summary: `Removed "${word}" from dictionary`, before: word });
|
|
3665
3985
|
return c.json({ ok: true });
|
|
3666
3986
|
});
|
|
3667
3987
|
app.patch("/keys/:key", async (c) => {
|
|
3668
3988
|
const key = c.req.param("key");
|
|
3669
3989
|
const body = await c.req.json();
|
|
3670
3990
|
const s = load();
|
|
3991
|
+
const beforeSource = typeof body.source === "string" ? valueText(s, key, s.config.sourceLocale) : void 0;
|
|
3671
3992
|
if (typeof body.rename === "string") renameKey(s, key, body.rename);
|
|
3672
3993
|
const target = typeof body.rename === "string" ? body.rename : key;
|
|
3673
3994
|
if (body.metadata) setMetadata(s, target, body.metadata);
|
|
3674
3995
|
if (typeof body.source === "string") setSourceValue(s, target, body.source);
|
|
3675
3996
|
if (typeof body.pluralArg === "string" && body.pluralArg.trim()) setPluralArg(s, target, body.pluralArg.trim());
|
|
3676
3997
|
persist(s);
|
|
3998
|
+
if (typeof body.rename === "string") logChange({ kind: "key", summary: `Renamed ${key} \u2192 ${body.rename}`, key: target, before: key, after: body.rename });
|
|
3999
|
+
if (body.metadata) logChange({ kind: "metadata", summary: `Updated metadata of ${target}`, key: target, after: body.metadata });
|
|
4000
|
+
if (typeof body.source === "string") logChange({ kind: "translation", summary: `Set source value of ${target}`, key: target, locale: s.config.sourceLocale, before: beforeSource, after: body.source });
|
|
4001
|
+
if (typeof body.pluralArg === "string" && body.pluralArg.trim()) logChange({ kind: "key", summary: `Changed plural arg of ${target}`, key: target, after: body.pluralArg.trim() });
|
|
3677
4002
|
if (typeof body.rename === "string") console.log(`[key] renamed ${key} \u2192 ${body.rename}`);
|
|
3678
4003
|
return c.json({ ok: true });
|
|
3679
4004
|
});
|
|
3680
4005
|
function removeOrphanScreenshot(s, screenshot) {
|
|
3681
4006
|
if (!screenshot) return;
|
|
3682
4007
|
for (const e of Object.values(s.keys)) if (e.screenshot === screenshot) return;
|
|
3683
|
-
const root =
|
|
3684
|
-
const abs =
|
|
4008
|
+
const root = dirname2(resolve9(deps.statePath));
|
|
4009
|
+
const abs = resolve9(root, screenshot);
|
|
3685
4010
|
const rel = relative3(root, abs);
|
|
3686
4011
|
const seg0 = rel.split(sep)[0] ?? "";
|
|
3687
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
4012
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync10(abs)) {
|
|
3688
4013
|
try {
|
|
3689
|
-
|
|
4014
|
+
rmSync4(abs);
|
|
3690
4015
|
} catch {
|
|
3691
4016
|
}
|
|
3692
4017
|
}
|
|
@@ -3695,9 +4020,11 @@ function createApi(deps) {
|
|
|
3695
4020
|
const s = load();
|
|
3696
4021
|
const key = c.req.param("key");
|
|
3697
4022
|
const shot = s.keys[key]?.screenshot;
|
|
4023
|
+
const before = valueText(s, key, s.config.sourceLocale);
|
|
3698
4024
|
deleteKey(s, key);
|
|
3699
4025
|
removeOrphanScreenshot(s, shot);
|
|
3700
4026
|
persist(s);
|
|
4027
|
+
logChange({ kind: "key", summary: `Deleted key ${key}`, key, before });
|
|
3701
4028
|
console.log(`[key] deleted ${key}`);
|
|
3702
4029
|
return c.json({ ok: true });
|
|
3703
4030
|
});
|
|
@@ -3721,6 +4048,7 @@ function createApi(deps) {
|
|
|
3721
4048
|
}
|
|
3722
4049
|
}
|
|
3723
4050
|
persist(s);
|
|
4051
|
+
if (cleared) logChange({ kind: "translation", summary: `Cleared ${cleared} value(s) across ${keys.length} key(s)`, after: { locales } });
|
|
3724
4052
|
console.log(`[bulk] cleared ${cleared} value(s)`);
|
|
3725
4053
|
return c.json({ cleared });
|
|
3726
4054
|
});
|
|
@@ -3738,6 +4066,7 @@ function createApi(deps) {
|
|
|
3738
4066
|
}
|
|
3739
4067
|
for (const shot of shots) removeOrphanScreenshot(s, shot);
|
|
3740
4068
|
persist(s);
|
|
4069
|
+
if (removed.length) logChange({ kind: "key", summary: `Deleted ${removed.length} key(s)`, before: removed });
|
|
3741
4070
|
console.log(`[bulk] deleted ${removed.length} key(s)`);
|
|
3742
4071
|
return c.json({ removed });
|
|
3743
4072
|
});
|
|
@@ -3763,6 +4092,7 @@ function createApi(deps) {
|
|
|
3763
4092
|
updated++;
|
|
3764
4093
|
}
|
|
3765
4094
|
persist(s);
|
|
4095
|
+
if (updated) logChange({ kind: "metadata", summary: `Updated metadata on ${updated} key(s)` });
|
|
3766
4096
|
console.log(`[bulk] updated metadata on ${updated} key(s)`);
|
|
3767
4097
|
return c.json({ updated });
|
|
3768
4098
|
});
|
|
@@ -3786,6 +4116,7 @@ function createApi(deps) {
|
|
|
3786
4116
|
}
|
|
3787
4117
|
}
|
|
3788
4118
|
persist(s);
|
|
4119
|
+
if (updated) logChange({ kind: "translation", summary: `Marked ${updated} value(s) as ${next}`, after: next });
|
|
3789
4120
|
console.log(`[bulk] set state ${next} on ${updated} value(s)`);
|
|
3790
4121
|
return c.json({ updated });
|
|
3791
4122
|
});
|
|
@@ -3795,15 +4126,21 @@ function createApi(deps) {
|
|
|
3795
4126
|
const s = load();
|
|
3796
4127
|
const key = c.req.param("key");
|
|
3797
4128
|
const locale = c.req.param("locale");
|
|
4129
|
+
const before = valueText(s, key, locale);
|
|
3798
4130
|
if (locale === s.config.sourceLocale) setSourceValue(s, key, value);
|
|
3799
4131
|
else setTargetValue(s, key, locale, value);
|
|
3800
4132
|
persist(s);
|
|
4133
|
+
logChange({ kind: "translation", summary: `Set ${locale} value of ${key}`, key, locale, before, after: value });
|
|
3801
4134
|
return c.json({ ok: true });
|
|
3802
4135
|
});
|
|
3803
4136
|
app.delete("/keys/:key/values/:locale", (c) => {
|
|
3804
4137
|
const s = load();
|
|
3805
|
-
|
|
4138
|
+
const key = c.req.param("key");
|
|
4139
|
+
const locale = c.req.param("locale");
|
|
4140
|
+
const before = valueText(s, key, locale);
|
|
4141
|
+
clearValue(s, key, locale);
|
|
3806
4142
|
persist(s);
|
|
4143
|
+
logChange({ kind: "translation", summary: `Cleared ${locale} value of ${key}`, key, locale, before });
|
|
3807
4144
|
return c.json({ ok: true });
|
|
3808
4145
|
});
|
|
3809
4146
|
app.put("/keys/:key/plural/:locale", async (c) => {
|
|
@@ -3812,52 +4149,68 @@ function createApi(deps) {
|
|
|
3812
4149
|
const s = load();
|
|
3813
4150
|
const key = c.req.param("key");
|
|
3814
4151
|
const locale = c.req.param("locale");
|
|
4152
|
+
const before = s.keys[key]?.values[locale]?.forms;
|
|
3815
4153
|
if (locale === s.config.sourceLocale) setSourcePluralForms(s, key, forms);
|
|
3816
4154
|
else setPluralForms(s, key, locale, forms);
|
|
3817
4155
|
persist(s);
|
|
4156
|
+
logChange({ kind: "translation", summary: `Set ${locale} plural forms of ${key}`, key, locale, before, after: forms });
|
|
3818
4157
|
return c.json({ ok: true });
|
|
3819
4158
|
});
|
|
3820
4159
|
app.post("/keys/:key/plural", async (c) => {
|
|
3821
4160
|
const { arg } = await c.req.json();
|
|
3822
4161
|
if (typeof arg !== "string" || !arg.trim()) return c.json({ error: "arg is required" }, 400);
|
|
3823
4162
|
const s = load();
|
|
3824
|
-
|
|
4163
|
+
const key = c.req.param("key");
|
|
4164
|
+
convertToPlural(s, key, arg);
|
|
3825
4165
|
persist(s);
|
|
4166
|
+
logChange({ kind: "key", summary: `Converted ${key} to plural`, key, after: arg });
|
|
3826
4167
|
return c.json({ ok: true });
|
|
3827
4168
|
});
|
|
3828
4169
|
app.delete("/keys/:key/plural", (c) => {
|
|
3829
4170
|
const s = load();
|
|
3830
|
-
|
|
4171
|
+
const key = c.req.param("key");
|
|
4172
|
+
convertToScalar(s, key);
|
|
3831
4173
|
persist(s);
|
|
4174
|
+
logChange({ kind: "key", summary: `Converted ${key} to scalar`, key });
|
|
3832
4175
|
return c.json({ ok: true });
|
|
3833
4176
|
});
|
|
3834
4177
|
app.put("/keys/:key/values/:locale/state", async (c) => {
|
|
3835
4178
|
const { state } = await c.req.json();
|
|
3836
4179
|
const s = load();
|
|
3837
|
-
|
|
4180
|
+
const key = c.req.param("key");
|
|
4181
|
+
const locale = c.req.param("locale");
|
|
4182
|
+
const before = s.keys[key]?.values[locale]?.state;
|
|
4183
|
+
setKeyState(s, key, locale, state);
|
|
3838
4184
|
persist(s);
|
|
4185
|
+
logChange({ kind: "translation", summary: `Marked ${key} ${locale} as ${state}`, key, locale, before, after: state });
|
|
3839
4186
|
return c.json({ ok: true });
|
|
3840
4187
|
});
|
|
3841
4188
|
app.post("/keys/:key/notes", async (c) => {
|
|
3842
4189
|
const { text } = await c.req.json();
|
|
3843
4190
|
if (typeof text !== "string" || !text.trim()) return c.json({ error: "note text is required" }, 400);
|
|
3844
4191
|
const s = load();
|
|
3845
|
-
const
|
|
4192
|
+
const key = c.req.param("key");
|
|
4193
|
+
const note = addNote(s, key, text);
|
|
3846
4194
|
persist(s);
|
|
4195
|
+
logChange({ kind: "note", summary: `Added note to ${key}`, key, after: text });
|
|
3847
4196
|
return c.json(note);
|
|
3848
4197
|
});
|
|
3849
4198
|
app.put("/keys/:key/notes/:id", async (c) => {
|
|
3850
4199
|
const { text } = await c.req.json();
|
|
3851
4200
|
if (typeof text !== "string" || !text.trim()) return c.json({ error: "note text is required" }, 400);
|
|
3852
4201
|
const s = load();
|
|
3853
|
-
|
|
4202
|
+
const key = c.req.param("key");
|
|
4203
|
+
editNote(s, key, c.req.param("id"), text);
|
|
3854
4204
|
persist(s);
|
|
4205
|
+
logChange({ kind: "note", summary: `Edited note on ${key}`, key, after: text });
|
|
3855
4206
|
return c.json({ ok: true });
|
|
3856
4207
|
});
|
|
3857
4208
|
app.delete("/keys/:key/notes/:id", (c) => {
|
|
3858
4209
|
const s = load();
|
|
3859
|
-
|
|
4210
|
+
const key = c.req.param("key");
|
|
4211
|
+
deleteNote(s, key, c.req.param("id"));
|
|
3860
4212
|
persist(s);
|
|
4213
|
+
logChange({ kind: "note", summary: `Deleted note on ${key}`, key });
|
|
3861
4214
|
return c.json({ ok: true });
|
|
3862
4215
|
});
|
|
3863
4216
|
app.put("/config", async (c) => {
|
|
@@ -3866,6 +4219,7 @@ function createApi(deps) {
|
|
|
3866
4219
|
return c.json({ error: "config.locales must be an array" }, 400);
|
|
3867
4220
|
}
|
|
3868
4221
|
const s = load();
|
|
4222
|
+
const beforeCfg = { locales: s.config.locales };
|
|
3869
4223
|
const removed = s.config.locales.filter((l) => !newConfig.locales.includes(l));
|
|
3870
4224
|
for (const l of removed) {
|
|
3871
4225
|
for (const e of Object.values(s.keys)) delete e.values[l];
|
|
@@ -3873,7 +4227,8 @@ function createApi(deps) {
|
|
|
3873
4227
|
s.config = newConfig;
|
|
3874
4228
|
validate(s);
|
|
3875
4229
|
persist(s);
|
|
3876
|
-
|
|
4230
|
+
logChange({ kind: "config", summary: `Saved config (${newConfig.locales.length} locale(s))`, before: beforeCfg, after: { locales: newConfig.locales } });
|
|
4231
|
+
console.log(`[config] saved \u2014 ${newConfig.locales.length} locale(s)`);
|
|
3877
4232
|
return c.json({ ok: true });
|
|
3878
4233
|
});
|
|
3879
4234
|
app.get("/glossary", (c) => c.json(load().glossary));
|
|
@@ -3881,14 +4236,19 @@ function createApi(deps) {
|
|
|
3881
4236
|
const entry = await c.req.json();
|
|
3882
4237
|
if (typeof entry?.term !== "string") return c.json({ error: "term must be a string" }, 400);
|
|
3883
4238
|
const s = load();
|
|
4239
|
+
const before = s.glossary.find((g) => g.term === entry.term);
|
|
3884
4240
|
upsertGlossaryEntry(s, entry);
|
|
3885
4241
|
persist(s);
|
|
4242
|
+
logChange({ kind: "glossary", summary: `${before ? "Updated" : "Added"} glossary term "${entry.term}"`, before, after: entry });
|
|
3886
4243
|
return c.json({ ok: true });
|
|
3887
4244
|
});
|
|
3888
4245
|
app.delete("/glossary/:term", (c) => {
|
|
3889
4246
|
const s = load();
|
|
3890
|
-
|
|
4247
|
+
const term = decodeURIComponent(c.req.param("term"));
|
|
4248
|
+
const before = s.glossary.find((g) => g.term === term);
|
|
4249
|
+
deleteGlossaryEntry(s, term);
|
|
3891
4250
|
persist(s);
|
|
4251
|
+
logChange({ kind: "glossary", summary: `Deleted glossary term "${term}"`, before });
|
|
3892
4252
|
return c.json({ ok: true });
|
|
3893
4253
|
});
|
|
3894
4254
|
app.post("/keys/:key/screenshot", async (c) => {
|
|
@@ -3896,18 +4256,18 @@ function createApi(deps) {
|
|
|
3896
4256
|
const body = await c.req.parseBody();
|
|
3897
4257
|
const file = body["file"];
|
|
3898
4258
|
if (!file || typeof file === "string") return c.json({ error: "no file uploaded" }, 400);
|
|
3899
|
-
const root =
|
|
4259
|
+
const root = dirname2(resolve9(deps.statePath));
|
|
3900
4260
|
const dirName = screenshotDirName(deps.statePath);
|
|
3901
|
-
const dir =
|
|
3902
|
-
mkdirSync6(dir, { recursive: true });
|
|
4261
|
+
const dir = resolve9(root, dirName);
|
|
3903
4262
|
const filename = `${sanitize(key)}__${sanitize(file.name)}`;
|
|
3904
|
-
|
|
4263
|
+
writeFileAtomic(resolve9(dir, filename), Buffer.from(await file.arrayBuffer()));
|
|
3905
4264
|
const path = `${dirName}/${filename}`;
|
|
3906
4265
|
const s = load();
|
|
3907
4266
|
const prev = s.keys[key]?.screenshot;
|
|
3908
4267
|
setMetadata(s, key, { screenshot: path });
|
|
3909
4268
|
if (prev && prev !== path) removeOrphanScreenshot(s, prev);
|
|
3910
4269
|
persist(s);
|
|
4270
|
+
logChange({ kind: "metadata", summary: `${prev ? "Replaced" : "Added"} screenshot on ${key}`, key, before: prev, after: path });
|
|
3911
4271
|
return c.json({ path });
|
|
3912
4272
|
});
|
|
3913
4273
|
app.delete("/keys/:key/screenshot", (c) => {
|
|
@@ -3917,6 +4277,7 @@ function createApi(deps) {
|
|
|
3917
4277
|
setMetadata(s, key, { screenshot: void 0 });
|
|
3918
4278
|
removeOrphanScreenshot(s, shot);
|
|
3919
4279
|
persist(s);
|
|
4280
|
+
logChange({ kind: "metadata", summary: `Removed screenshot from ${key}`, key, before: shot });
|
|
3920
4281
|
return c.json({ ok: true });
|
|
3921
4282
|
});
|
|
3922
4283
|
app.get("/export/preview", (c) => {
|
|
@@ -3961,12 +4322,13 @@ function createApi(deps) {
|
|
|
3961
4322
|
return c.json({ error: e.message }, 400);
|
|
3962
4323
|
}
|
|
3963
4324
|
persist(result.state);
|
|
4325
|
+
logChange({ kind: "import", summary: `Imported ${result.keyCount} key(s) across ${result.localeCount} locale(s)` });
|
|
3964
4326
|
console.log(`[import] ${result.keyCount} key(s) across ${result.localeCount} locale(s)${result.warnings.length ? `, ${result.warnings.length} warning(s)` : ""}`);
|
|
3965
4327
|
return c.json({ keyCount: result.keyCount, localeCount: result.localeCount, warnings: result.warnings });
|
|
3966
4328
|
});
|
|
3967
4329
|
app.post("/export", (c) => {
|
|
3968
4330
|
const s = narrowForExport(load());
|
|
3969
|
-
const root =
|
|
4331
|
+
const root = dirname2(resolve9(deps.statePath));
|
|
3970
4332
|
const warnings = [];
|
|
3971
4333
|
let count = 0;
|
|
3972
4334
|
for (const output of s.config.outputs) {
|
|
@@ -3974,9 +4336,8 @@ function createApi(deps) {
|
|
|
3974
4336
|
const result = adapter.export(s, output);
|
|
3975
4337
|
warnings.push(...result.warnings);
|
|
3976
4338
|
for (const f of result.files) {
|
|
3977
|
-
const abs =
|
|
3978
|
-
|
|
3979
|
-
writeFileSync5(abs, f.contents, "utf8");
|
|
4339
|
+
const abs = resolve9(root, f.path);
|
|
4340
|
+
writeFileAtomic(abs, f.contents);
|
|
3980
4341
|
count++;
|
|
3981
4342
|
}
|
|
3982
4343
|
}
|
|
@@ -3995,41 +4356,63 @@ function createApi(deps) {
|
|
|
3995
4356
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: 0, errors: [] }) });
|
|
3996
4357
|
return;
|
|
3997
4358
|
}
|
|
4359
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
3998
4360
|
let provider;
|
|
3999
4361
|
try {
|
|
4000
|
-
provider = deps.makeProvider ? deps.makeProvider(
|
|
4362
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
4001
4363
|
} catch (e) {
|
|
4002
4364
|
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
4003
4365
|
return;
|
|
4004
4366
|
}
|
|
4005
4367
|
const { skipped } = attachScreenshotsForProvider(reqs, s, projectRoot, provider.supportsVision());
|
|
4006
|
-
if (skipped) console.warn(`Model "${
|
|
4007
|
-
console.log(`[translate] ${reqs.length} string(s) \u2192 ${
|
|
4368
|
+
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4369
|
+
console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
|
|
4008
4370
|
let totalWritten = 0;
|
|
4009
4371
|
const allErrors = [];
|
|
4010
4372
|
const system = buildSystemPrompt();
|
|
4011
4373
|
const reqById = new Map(reqs.map((r) => [r.id, r]));
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4374
|
+
const localeTotals = /* @__PURE__ */ new Map();
|
|
4375
|
+
for (const r of reqs) localeTotals.set(r.targetLocale, (localeTotals.get(r.targetLocale) ?? 0) + 1);
|
|
4376
|
+
const localeDone = /* @__PURE__ */ new Map();
|
|
4377
|
+
await stream.writeSSE({
|
|
4378
|
+
event: "start",
|
|
4379
|
+
data: JSON.stringify({ total: reqs.length, locales: [...localeTotals].map(([locale, total]) => ({ locale, total })) })
|
|
4380
|
+
});
|
|
4381
|
+
await runLocaleParallel(reqs, provider, {
|
|
4382
|
+
// Announce a language the moment a worker picks it up — this is the
|
|
4383
|
+
// signal that "something is happening" during the long first LLM call.
|
|
4384
|
+
onLocaleStart: (locale) => {
|
|
4385
|
+
void stream.writeSSE({ event: "locale-start", data: JSON.stringify({ locale }) });
|
|
4386
|
+
},
|
|
4387
|
+
onBatchComplete: (done, total, batchResults, locale) => {
|
|
4388
|
+
const fresh = load();
|
|
4389
|
+
const { written, errors } = applyResults(fresh, reqs, batchResults);
|
|
4390
|
+
persist(fresh);
|
|
4391
|
+
totalWritten += written;
|
|
4392
|
+
allErrors.push(...errors);
|
|
4393
|
+
appendLog(projectRoot, {
|
|
4394
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4395
|
+
kind: "translate",
|
|
4396
|
+
summary: `Translated ${batchResults.length} item(s)`,
|
|
4397
|
+
model: aiCfg.model,
|
|
4398
|
+
system,
|
|
4399
|
+
items: batchResults.map((r) => {
|
|
4400
|
+
const req = reqById.get(r.id);
|
|
4401
|
+
return { id: r.id, key: req?.key ?? "", source: req?.source ?? "", targetLocale: req?.targetLocale, context: req?.context, glossary: req?.glossary, screenshot: req ? s.keys[req.key]?.screenshot : void 0 };
|
|
4402
|
+
}),
|
|
4403
|
+
results: batchResults
|
|
4404
|
+
});
|
|
4405
|
+
const ld = (localeDone.get(locale) ?? 0) + batchResults.length;
|
|
4406
|
+
localeDone.set(locale, ld);
|
|
4407
|
+
console.log(`[translate] ${done}/${total}`);
|
|
4408
|
+
void stream.writeSSE({
|
|
4409
|
+
event: "progress",
|
|
4410
|
+
data: JSON.stringify({ done, total, written: totalWritten, errors, locale, localeDone: ld, localeTotal: localeTotals.get(locale) ?? 0 })
|
|
4411
|
+
});
|
|
4412
|
+
},
|
|
4413
|
+
onLocaleDone: (locale) => {
|
|
4414
|
+
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
4415
|
+
}
|
|
4033
4416
|
}, void 0, signal);
|
|
4034
4417
|
if (!signal?.aborted) {
|
|
4035
4418
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
@@ -4052,19 +4435,22 @@ function createApi(deps) {
|
|
|
4052
4435
|
let written = 0;
|
|
4053
4436
|
let errors = [];
|
|
4054
4437
|
if (toTranslate.length) {
|
|
4438
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
4055
4439
|
let provider;
|
|
4056
4440
|
try {
|
|
4057
|
-
provider = deps.makeProvider ? deps.makeProvider(
|
|
4441
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
4058
4442
|
} catch (e) {
|
|
4059
4443
|
return c.json({ error: e.message }, 400);
|
|
4060
4444
|
}
|
|
4061
4445
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, projectRoot, provider.supportsVision());
|
|
4062
|
-
if (skipped) console.warn(`Model "${
|
|
4446
|
+
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4063
4447
|
const results = await runLocaleParallel(toTranslate, provider);
|
|
4064
4448
|
({ written, errors } = applyResults(s, toTranslate, results, void 0, force));
|
|
4065
4449
|
const entry = {
|
|
4066
4450
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4067
|
-
|
|
4451
|
+
kind: "translate",
|
|
4452
|
+
summary: `Translated ${toTranslate.length} item(s)`,
|
|
4453
|
+
model: aiCfg.model,
|
|
4068
4454
|
system: buildSystemPrompt(),
|
|
4069
4455
|
// Log the screenshot PATH only — never the image bytes.
|
|
4070
4456
|
items: toTranslate.map((r) => ({
|
|
@@ -4078,12 +4464,12 @@ function createApi(deps) {
|
|
|
4078
4464
|
})),
|
|
4079
4465
|
results
|
|
4080
4466
|
};
|
|
4081
|
-
|
|
4467
|
+
appendLog(projectRoot, entry);
|
|
4082
4468
|
}
|
|
4083
4469
|
persist(s);
|
|
4084
4470
|
return c.json({ requested: reqs.length, written, errors });
|
|
4085
4471
|
}));
|
|
4086
|
-
app.get("/
|
|
4472
|
+
app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
|
|
4087
4473
|
app.post("/scan", async (c) => {
|
|
4088
4474
|
const s = load();
|
|
4089
4475
|
const existing = loadUsageCache(projectRoot);
|
|
@@ -4107,7 +4493,7 @@ function createApi(deps) {
|
|
|
4107
4493
|
const refs = [];
|
|
4108
4494
|
const prefixRefs = [];
|
|
4109
4495
|
for (const [file, entry] of Object.entries(cache2.files)) {
|
|
4110
|
-
const abs =
|
|
4496
|
+
const abs = resolve9(projectRoot, file);
|
|
4111
4497
|
for (const r of entry.refs) {
|
|
4112
4498
|
if (r.key === key) refs.push({ file, abs, line: r.line, col: r.col, scanner: r.scanner });
|
|
4113
4499
|
}
|
|
@@ -4148,9 +4534,10 @@ function createApi(deps) {
|
|
|
4148
4534
|
keys: body.keys
|
|
4149
4535
|
}, cache2, body.lastRunAt);
|
|
4150
4536
|
if (!targets.length) return c.json({ requested: 0, written: 0, errors: [] });
|
|
4537
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
4151
4538
|
let provider;
|
|
4152
4539
|
try {
|
|
4153
|
-
provider = deps.makeProvider ? deps.makeProvider(
|
|
4540
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
4154
4541
|
} catch (e) {
|
|
4155
4542
|
return c.json({ error: e.message }, 400);
|
|
4156
4543
|
}
|
|
@@ -4172,10 +4559,11 @@ function createApi(deps) {
|
|
|
4172
4559
|
const raw = await provider.complete({ system, content: [{ type: "text", text: prompt }], schema: CONTEXT_BATCH_SCHEMA });
|
|
4173
4560
|
const batch = raw;
|
|
4174
4561
|
const { written, errors } = applyContext(s, targets, batch.items ?? []);
|
|
4175
|
-
|
|
4562
|
+
appendLog(projectRoot, {
|
|
4176
4563
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4177
4564
|
kind: "context",
|
|
4178
|
-
|
|
4565
|
+
summary: `Generated context for ${targets.length} key(s)`,
|
|
4566
|
+
model: aiCfg.model,
|
|
4179
4567
|
system,
|
|
4180
4568
|
items: targets.map((t) => ({ id: t.id, key: t.key, source: t.source })),
|
|
4181
4569
|
results: (batch.items ?? []).map((r) => ({ id: r.id, value: r.context, error: r.error }))
|
|
@@ -4207,6 +4595,9 @@ var init_api = __esm({
|
|
|
4207
4595
|
init_schema();
|
|
4208
4596
|
init_run2();
|
|
4209
4597
|
init_export_run();
|
|
4598
|
+
init_ui_prefs();
|
|
4599
|
+
init_local_settings();
|
|
4600
|
+
init_atomic_write();
|
|
4210
4601
|
sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
4211
4602
|
screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
4212
4603
|
}
|
|
@@ -4221,7 +4612,7 @@ __export(server_exports, {
|
|
|
4221
4612
|
import { Hono as Hono2 } from "hono";
|
|
4222
4613
|
import { serve } from "@hono/node-server";
|
|
4223
4614
|
import { fileURLToPath } from "url";
|
|
4224
|
-
import { dirname as
|
|
4615
|
+
import { dirname as dirname3, join as join9, resolve as resolve10, extname as extname3, sep as sep2 } from "path";
|
|
4225
4616
|
import { readFile, stat } from "fs/promises";
|
|
4226
4617
|
import { createServer } from "net";
|
|
4227
4618
|
import open from "open";
|
|
@@ -4239,14 +4630,14 @@ async function readFileResponse(absPath) {
|
|
|
4239
4630
|
function buildApp(opts) {
|
|
4240
4631
|
const app = new Hono2();
|
|
4241
4632
|
app.route("/api", createApi({ statePath: opts.statePath, autoExport: true }));
|
|
4242
|
-
const projectRoot =
|
|
4633
|
+
const projectRoot = dirname3(resolve10(opts.statePath));
|
|
4243
4634
|
app.get("/:dir/*", async (c, next) => {
|
|
4244
4635
|
const dirSeg = c.req.param("dir");
|
|
4245
4636
|
if (!dirSeg.endsWith("-screenshots")) return next();
|
|
4246
|
-
const shotsRoot =
|
|
4637
|
+
const shotsRoot = resolve10(projectRoot, dirSeg);
|
|
4247
4638
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4248
4639
|
const rest = pathname.slice(`/${dirSeg}`.length);
|
|
4249
|
-
const target =
|
|
4640
|
+
const target = resolve10(shotsRoot, "." + rest);
|
|
4250
4641
|
const inside = target === shotsRoot || target.startsWith(shotsRoot + sep2);
|
|
4251
4642
|
if (inside) {
|
|
4252
4643
|
const file = await readFileResponse(target);
|
|
@@ -4255,19 +4646,21 @@ function buildApp(opts) {
|
|
|
4255
4646
|
return c.notFound();
|
|
4256
4647
|
});
|
|
4257
4648
|
if (!opts.dev) {
|
|
4258
|
-
const root =
|
|
4649
|
+
const root = resolve10(opts.uiDir ?? DEFAULT_UI_DIR);
|
|
4259
4650
|
app.get("/*", async (c) => {
|
|
4260
4651
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4261
|
-
const target =
|
|
4652
|
+
const target = resolve10(root, "." + pathname);
|
|
4262
4653
|
const inside = target === root || target.startsWith(root + sep2);
|
|
4263
4654
|
if (inside && pathname !== "/") {
|
|
4264
4655
|
const file = await readFileResponse(target);
|
|
4265
4656
|
if (file) return file;
|
|
4266
4657
|
}
|
|
4267
|
-
const index = await readFileResponse(
|
|
4658
|
+
const index = await readFileResponse(join9(root, "index.html"));
|
|
4268
4659
|
if (index) return index;
|
|
4269
4660
|
return c.notFound();
|
|
4270
4661
|
});
|
|
4662
|
+
} else {
|
|
4663
|
+
app.get("/", (c) => c.html(DEV_LANDING_PAGE));
|
|
4271
4664
|
}
|
|
4272
4665
|
return app;
|
|
4273
4666
|
}
|
|
@@ -4299,7 +4692,7 @@ async function startServer(opts) {
|
|
|
4299
4692
|
});
|
|
4300
4693
|
}
|
|
4301
4694
|
function backgroundScan(statePath) {
|
|
4302
|
-
const projectRoot =
|
|
4695
|
+
const projectRoot = dirname3(resolve10(statePath));
|
|
4303
4696
|
Promise.resolve().then(() => {
|
|
4304
4697
|
const state = loadState(statePath);
|
|
4305
4698
|
const existing = loadUsageCache(projectRoot);
|
|
@@ -4311,7 +4704,7 @@ function backgroundScan(statePath) {
|
|
|
4311
4704
|
console.warn("[scan] failed:", err instanceof Error ? err.message : String(err));
|
|
4312
4705
|
});
|
|
4313
4706
|
}
|
|
4314
|
-
var here, DEFAULT_UI_DIR, MIME, DEFAULT_PORT, DEV_PORT;
|
|
4707
|
+
var here, DEFAULT_UI_DIR, MIME, DEFAULT_PORT, DEV_PORT, DEV_UI_URL, DEV_LANDING_PAGE;
|
|
4315
4708
|
var init_server = __esm({
|
|
4316
4709
|
"src/server/server.ts"() {
|
|
4317
4710
|
"use strict";
|
|
@@ -4319,8 +4712,8 @@ var init_server = __esm({
|
|
|
4319
4712
|
init_state();
|
|
4320
4713
|
init_scan();
|
|
4321
4714
|
init_scanner();
|
|
4322
|
-
here =
|
|
4323
|
-
DEFAULT_UI_DIR =
|
|
4715
|
+
here = dirname3(fileURLToPath(import.meta.url));
|
|
4716
|
+
DEFAULT_UI_DIR = join9(here, "..", "ui");
|
|
4324
4717
|
MIME = {
|
|
4325
4718
|
".html": "text/html; charset=utf-8",
|
|
4326
4719
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -4337,6 +4730,15 @@ var init_server = __esm({
|
|
|
4337
4730
|
};
|
|
4338
4731
|
DEFAULT_PORT = 3e3;
|
|
4339
4732
|
DEV_PORT = 8787;
|
|
4733
|
+
DEV_UI_URL = "http://localhost:5173";
|
|
4734
|
+
DEV_LANDING_PAGE = `<!doctype html>
|
|
4735
|
+
<html lang="en"><head><meta charset="utf-8"><title>Glotfile \u2014 dev API</title>
|
|
4736
|
+
<style>body{font:16px/1.6 system-ui,sans-serif;max-width:34rem;margin:16vh auto;padding:0 1.5rem;color:#1f2937}h1{font-size:1.4rem}a{color:#2563eb}code{background:#f3f4f6;padding:.1em .35em;border-radius:.3em}</style>
|
|
4737
|
+
</head><body>
|
|
4738
|
+
<h1>Glotfile \u2014 dev API server</h1>
|
|
4739
|
+
<p>This port serves the <strong>API only</strong>. In dev, the app is served by Vite.</p>
|
|
4740
|
+
<p>Open the app \u2192 <a href="${DEV_UI_URL}">${DEV_UI_URL}</a> (the <code>[ui] Local:</code> URL in your terminal).</p>
|
|
4741
|
+
</body></html>`;
|
|
4340
4742
|
}
|
|
4341
4743
|
});
|
|
4342
4744
|
|
|
@@ -4345,14 +4747,15 @@ init_state();
|
|
|
4345
4747
|
init_export_run();
|
|
4346
4748
|
init_storage();
|
|
4347
4749
|
init_ai();
|
|
4750
|
+
init_local_settings();
|
|
4348
4751
|
init_run();
|
|
4349
4752
|
init_provider();
|
|
4350
4753
|
init_log();
|
|
4351
4754
|
init_scan();
|
|
4352
4755
|
init_scanner();
|
|
4353
4756
|
init_context();
|
|
4354
|
-
import { resolve as
|
|
4355
|
-
import { readFileSync as
|
|
4757
|
+
import { resolve as resolve11, dirname as dirname4 } from "path";
|
|
4758
|
+
import { readFileSync as readFileSync15, existsSync as existsSync11 } from "fs";
|
|
4356
4759
|
|
|
4357
4760
|
// src/server/lint/run.ts
|
|
4358
4761
|
init_glob();
|
|
@@ -4654,15 +5057,15 @@ async function runLint(state, options = {}) {
|
|
|
4654
5057
|
|
|
4655
5058
|
// src/server/lint/outputs.ts
|
|
4656
5059
|
init_adapters();
|
|
4657
|
-
import { readFileSync as
|
|
4658
|
-
import { resolve as
|
|
5060
|
+
import { readFileSync as readFileSync10, existsSync as existsSync8 } from "fs";
|
|
5061
|
+
import { resolve as resolve8 } from "path";
|
|
4659
5062
|
function checkOutputs(state, root) {
|
|
4660
5063
|
const out = [];
|
|
4661
5064
|
for (const output of state.config.outputs) {
|
|
4662
5065
|
const result = getAdapter(output.adapter).export(state, output);
|
|
4663
5066
|
for (const file of result.files) {
|
|
4664
|
-
const abs =
|
|
4665
|
-
const current =
|
|
5067
|
+
const abs = resolve8(root, file.path);
|
|
5068
|
+
const current = existsSync8(abs) ? readFileSync10(abs, "utf8") : null;
|
|
4666
5069
|
if (current === null) {
|
|
4667
5070
|
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
4668
5071
|
} else if (current !== file.contents) {
|
|
@@ -4740,7 +5143,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
4740
5143
|
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "build-context", "scan", "prune", "split"];
|
|
4741
5144
|
var isCommand = (s) => s != null && COMMANDS.includes(s);
|
|
4742
5145
|
function parseArgs(argv) {
|
|
4743
|
-
const statePath =
|
|
5146
|
+
const statePath = resolve11(process.cwd(), "glotfile.json");
|
|
4744
5147
|
const first = argv[0];
|
|
4745
5148
|
if (first === "help" || first === "--help" || first === "-h") {
|
|
4746
5149
|
return isCommand(argv[1]) ? { command: argv[1], statePath, help: true } : { command: "help", statePath };
|
|
@@ -4758,7 +5161,7 @@ function parseArgs(argv) {
|
|
|
4758
5161
|
if (flag === "--help" || flag === "-h") args.help = true;
|
|
4759
5162
|
else if (flag === "--dev") args.dev = true;
|
|
4760
5163
|
else if ((flag === "--file" || flag === "-f") && next) {
|
|
4761
|
-
args.statePath =
|
|
5164
|
+
args.statePath = resolve11(process.cwd(), next);
|
|
4762
5165
|
i++;
|
|
4763
5166
|
} else if (flag === "--adapter" && next) {
|
|
4764
5167
|
args.adapter = next;
|
|
@@ -4814,7 +5217,7 @@ function watchTargetFor(statePath) {
|
|
|
4814
5217
|
return detectFormat(statePath) === "split" ? { path: splitDirFor(statePath), recursive: true } : { path: statePath, recursive: false };
|
|
4815
5218
|
}
|
|
4816
5219
|
async function runExport(args) {
|
|
4817
|
-
const root =
|
|
5220
|
+
const root = dirname4(resolve11(args.statePath));
|
|
4818
5221
|
const runOnce = () => {
|
|
4819
5222
|
const state = loadState(args.statePath);
|
|
4820
5223
|
const result = exportToDisk(state, root, args.adapter ? { adapter: args.adapter } : void 0);
|
|
@@ -4850,7 +5253,7 @@ async function runExport(args) {
|
|
|
4850
5253
|
}
|
|
4851
5254
|
async function runTranslate(args) {
|
|
4852
5255
|
const state = loadState(args.statePath);
|
|
4853
|
-
const projectRoot =
|
|
5256
|
+
const projectRoot = dirname4(resolve11(args.statePath));
|
|
4854
5257
|
const reqs = selectRequests(state, {
|
|
4855
5258
|
// Default to translating only empty values; --all forces a full re-translate
|
|
4856
5259
|
// (overwriting existing translations). --only missing stays as a no-op alias.
|
|
@@ -4862,33 +5265,38 @@ async function runTranslate(args) {
|
|
|
4862
5265
|
let written = 0;
|
|
4863
5266
|
let errors = [];
|
|
4864
5267
|
if (toTranslate.length) {
|
|
5268
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
4865
5269
|
let provider;
|
|
4866
5270
|
try {
|
|
4867
|
-
provider = makeProvider(
|
|
5271
|
+
provider = makeProvider(ai);
|
|
4868
5272
|
} catch (e) {
|
|
4869
5273
|
console.error(e.message);
|
|
4870
5274
|
process.exitCode = 1;
|
|
4871
5275
|
return;
|
|
4872
5276
|
}
|
|
4873
5277
|
const { skipped } = attachScreenshotsForProvider(toTranslate, state, projectRoot, provider.supportsVision());
|
|
4874
|
-
if (skipped) console.warn(`Model "${
|
|
5278
|
+
if (skipped) console.warn(`Model "${ai.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4875
5279
|
console.log(`Translating ${toTranslate.length} string(s)\u2026`);
|
|
4876
5280
|
let batchCallbackFired = false;
|
|
4877
|
-
const results = await runLocaleParallel(toTranslate, provider,
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
5281
|
+
const results = await runLocaleParallel(toTranslate, provider, {
|
|
5282
|
+
onBatchComplete: (done, total, batchResults) => {
|
|
5283
|
+
batchCallbackFired = true;
|
|
5284
|
+
const batchApplied = applyResults(state, toTranslate, batchResults);
|
|
5285
|
+
written += batchApplied.written;
|
|
5286
|
+
errors.push(...batchApplied.errors);
|
|
5287
|
+
saveState(args.statePath, state);
|
|
5288
|
+
process.stdout.write(`\r ${done}/${total} translated`);
|
|
5289
|
+
}
|
|
4884
5290
|
});
|
|
4885
5291
|
process.stdout.write("\n");
|
|
4886
5292
|
if (!batchCallbackFired) {
|
|
4887
5293
|
({ written, errors } = applyResults(state, toTranslate, results));
|
|
4888
5294
|
}
|
|
4889
|
-
|
|
5295
|
+
appendLog(projectRoot, {
|
|
4890
5296
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4891
|
-
|
|
5297
|
+
kind: "translate",
|
|
5298
|
+
summary: `Translated ${toTranslate.length} item(s)`,
|
|
5299
|
+
model: ai.model,
|
|
4892
5300
|
system: buildSystemPrompt(),
|
|
4893
5301
|
items: toTranslate.map((r) => ({
|
|
4894
5302
|
id: r.id,
|
|
@@ -4915,7 +5323,7 @@ function printReport(report, format, rawText) {
|
|
|
4915
5323
|
}
|
|
4916
5324
|
async function runLintCmd(args) {
|
|
4917
5325
|
const state = loadState(args.statePath);
|
|
4918
|
-
const rawText =
|
|
5326
|
+
const rawText = existsSync11(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
|
|
4919
5327
|
const report = await runLint(state, { locales: args.locales, ruleIds: args.ruleIds });
|
|
4920
5328
|
printReport(report, args.format, rawText);
|
|
4921
5329
|
const tooManyWarnings = args.maxWarnings != null && report.counts.warn > args.maxWarnings;
|
|
@@ -4935,8 +5343,8 @@ async function runCheck(args) {
|
|
|
4935
5343
|
process.exitCode = 1;
|
|
4936
5344
|
return;
|
|
4937
5345
|
}
|
|
4938
|
-
const rawText =
|
|
4939
|
-
const root =
|
|
5346
|
+
const rawText = existsSync11(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
|
|
5347
|
+
const root = dirname4(resolve11(args.statePath));
|
|
4940
5348
|
const lint = await runLint(state, {});
|
|
4941
5349
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
|
|
4942
5350
|
const counts = countSeverities(findings);
|
|
@@ -4946,9 +5354,9 @@ async function runCheck(args) {
|
|
|
4946
5354
|
}
|
|
4947
5355
|
async function runImportCmd(args) {
|
|
4948
5356
|
const { runImport: runImport2 } = await Promise.resolve().then(() => (init_run2(), run_exports));
|
|
4949
|
-
const projectRoot = args.importSource ?
|
|
4950
|
-
const out =
|
|
4951
|
-
if (
|
|
5357
|
+
const projectRoot = args.importSource ? resolve11(args.importSource) : dirname4(resolve11(args.statePath));
|
|
5358
|
+
const out = resolve11(projectRoot, "glotfile.json");
|
|
5359
|
+
if (existsSync11(out) && !args.importForce) {
|
|
4952
5360
|
console.error(`${out} already exists; pass --force to overwrite`);
|
|
4953
5361
|
process.exitCode = 1;
|
|
4954
5362
|
return;
|
|
@@ -4973,7 +5381,7 @@ async function runImportCmd(args) {
|
|
|
4973
5381
|
}
|
|
4974
5382
|
async function runBuildContext(args) {
|
|
4975
5383
|
const state = loadState(args.statePath);
|
|
4976
|
-
const projectRoot =
|
|
5384
|
+
const projectRoot = dirname4(resolve11(args.statePath));
|
|
4977
5385
|
const cache2 = loadUsageCache(projectRoot);
|
|
4978
5386
|
if (!cache2) {
|
|
4979
5387
|
console.error("No usage index found. Run 'glotfile scan' first.");
|
|
@@ -4992,7 +5400,7 @@ async function runBuildContext(args) {
|
|
|
4992
5400
|
}
|
|
4993
5401
|
let provider;
|
|
4994
5402
|
try {
|
|
4995
|
-
provider = makeProvider(
|
|
5403
|
+
provider = makeProvider(loadLocalSettings(projectRoot).ai);
|
|
4996
5404
|
} catch (e) {
|
|
4997
5405
|
console.error(e.message);
|
|
4998
5406
|
process.exitCode = 1;
|
|
@@ -5022,7 +5430,7 @@ async function runBuildContext(args) {
|
|
|
5022
5430
|
}
|
|
5023
5431
|
async function runScanCmd(args) {
|
|
5024
5432
|
const state = loadState(args.statePath);
|
|
5025
|
-
const projectRoot =
|
|
5433
|
+
const projectRoot = dirname4(resolve11(args.statePath));
|
|
5026
5434
|
const existing = loadUsageCache(projectRoot);
|
|
5027
5435
|
const result = runScan(projectRoot, state.config.scan ?? {}, existing);
|
|
5028
5436
|
const fileCount2 = Object.keys(result.files).length;
|
|
@@ -5041,7 +5449,7 @@ async function runPrune(args) {
|
|
|
5041
5449
|
for (const k of findEmptySourceKeys(state)) toRemove.add(k);
|
|
5042
5450
|
}
|
|
5043
5451
|
if (args.unused) {
|
|
5044
|
-
const projectRoot =
|
|
5452
|
+
const projectRoot = dirname4(resolve11(args.statePath));
|
|
5045
5453
|
const cache2 = runScan(projectRoot, state.config.scan ?? {}, loadUsageCache(projectRoot));
|
|
5046
5454
|
const used = new Set(computeUsedKeys(state, cache2));
|
|
5047
5455
|
for (const k of Object.keys(state.keys)) {
|
|
@@ -5208,9 +5616,10 @@ async function main(argv) {
|
|
|
5208
5616
|
if (args.command === "split") return runSplit(args);
|
|
5209
5617
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
5210
5618
|
const { url } = await startServer2({ statePath: args.statePath, dev: args.dev });
|
|
5211
|
-
console.log(`Glotfile
|
|
5619
|
+
if (args.dev) console.log(`Glotfile dev API on ${url} \u2014 open the UI at the Vite "Local:" URL above`);
|
|
5620
|
+
else console.log(`Glotfile running at ${url}`);
|
|
5212
5621
|
}
|
|
5213
|
-
if (
|
|
5622
|
+
if (resolve11(process.argv[1] ?? "") === resolve11(fileURLToPath2(import.meta.url))) {
|
|
5214
5623
|
main(process.argv.slice(2)).catch((err) => {
|
|
5215
5624
|
console.error(err instanceof Error ? err.message : String(err));
|
|
5216
5625
|
process.exit(1);
|