glotfile 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/server/cli.js +601 -215
- package/dist/server/server.js +501 -173
- package/dist/ui/assets/en-BZRN_IpI.svg +25 -0
- package/dist/ui/assets/index-BHjDAL9d.js +1847 -0
- package/dist/ui/assets/index-iW_TzurC.css +1 -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 false;
|
|
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,95 @@ 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
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
function loadLocalSettings(projectRoot) {
|
|
2232
|
+
const raw = readJson(settingsPath(projectRoot));
|
|
2233
|
+
return { ai: coerceAi(raw.ai), editor: isEditorId(raw.editor) ? raw.editor : DEFAULT_EDITOR };
|
|
2234
|
+
}
|
|
2235
|
+
function saveLocalSettings(projectRoot, patch) {
|
|
2236
|
+
const path = settingsPath(projectRoot);
|
|
2237
|
+
const merged = { ...readJson(path) };
|
|
2238
|
+
if (patch.ai !== void 0) merged.ai = patch.ai;
|
|
2239
|
+
if (patch.editor !== void 0) merged.editor = patch.editor;
|
|
2240
|
+
ensureGlotfileDir(projectRoot);
|
|
2241
|
+
writeFileAtomic(path, JSON.stringify(merged, null, 2) + "\n");
|
|
2242
|
+
}
|
|
2243
|
+
function aiConfigError(ai) {
|
|
2244
|
+
if (!ai || typeof ai !== "object") return "ai must be an object";
|
|
2245
|
+
const a = ai;
|
|
2246
|
+
if (typeof a.provider !== "string" || !PROVIDERS.includes(a.provider)) {
|
|
2247
|
+
return `ai.provider must be one of: ${PROVIDERS.join(", ")}`;
|
|
2248
|
+
}
|
|
2249
|
+
if (typeof a.model !== "string") return "ai.model must be a string";
|
|
2250
|
+
if (!(a.endpoint === null || a.endpoint === void 0 || typeof a.endpoint === "string")) return "ai.endpoint must be a string or null";
|
|
2251
|
+
if (!(a.region === void 0 || a.region === null || typeof a.region === "string")) return "ai.region must be a string or null";
|
|
2252
|
+
if (typeof a.batchSize !== "number") return "ai.batchSize must be a number";
|
|
2253
|
+
return null;
|
|
2254
|
+
}
|
|
2255
|
+
var EDITOR_IDS, isEditorId, DEFAULT_AI, DEFAULT_EDITOR, settingsPath;
|
|
2256
|
+
var init_local_settings = __esm({
|
|
2257
|
+
"src/server/local-settings.ts"() {
|
|
2258
|
+
"use strict";
|
|
2259
|
+
init_atomic_write();
|
|
2260
|
+
init_glotfile_dir();
|
|
2261
|
+
init_schema();
|
|
2262
|
+
EDITOR_IDS = ["vscode", "zed", "phpstorm"];
|
|
2263
|
+
isEditorId = (v) => EDITOR_IDS.includes(v);
|
|
2264
|
+
DEFAULT_AI = {
|
|
2265
|
+
provider: "anthropic",
|
|
2266
|
+
model: "claude-haiku-4-5-20251001",
|
|
2267
|
+
endpoint: null,
|
|
2268
|
+
region: null,
|
|
2269
|
+
batchSize: 25
|
|
2270
|
+
};
|
|
2271
|
+
DEFAULT_EDITOR = "vscode";
|
|
2272
|
+
settingsPath = (projectRoot) => resolve3(projectRoot, ".glotfile", "settings.json");
|
|
2042
2273
|
}
|
|
2043
2274
|
});
|
|
2044
2275
|
|
|
@@ -2054,8 +2285,8 @@ var init_glob = __esm({
|
|
|
2054
2285
|
});
|
|
2055
2286
|
|
|
2056
2287
|
// src/server/ai/run.ts
|
|
2057
|
-
import { readFileSync as
|
|
2058
|
-
import { resolve as
|
|
2288
|
+
import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
|
|
2289
|
+
import { resolve as resolve4, extname } from "path";
|
|
2059
2290
|
function selectRequests(state, opts) {
|
|
2060
2291
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
2061
2292
|
const keyRe = opts.keyGlob ? globToRegExp(opts.keyGlob) : null;
|
|
@@ -2134,11 +2365,11 @@ function attachScreenshots(reqs, state, projectRoot) {
|
|
|
2134
2365
|
const mediaType = MEDIA_TYPES[extname(screenshot).toLowerCase()];
|
|
2135
2366
|
if (!mediaType) continue;
|
|
2136
2367
|
if (!cache2.has(screenshot)) {
|
|
2137
|
-
const abs =
|
|
2138
|
-
if (!
|
|
2368
|
+
const abs = resolve4(projectRoot, screenshot);
|
|
2369
|
+
if (!existsSync4(abs)) {
|
|
2139
2370
|
cache2.set(screenshot, null);
|
|
2140
2371
|
} else {
|
|
2141
|
-
const buf =
|
|
2372
|
+
const buf = readFileSync5(abs);
|
|
2142
2373
|
cache2.set(screenshot, buf.length > MAX_IMAGE_BYTES ? null : { mediaType, base64: buf.toString("base64") });
|
|
2143
2374
|
}
|
|
2144
2375
|
}
|
|
@@ -2154,7 +2385,7 @@ function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision)
|
|
|
2154
2385
|
const keys = new Set(reqs.filter((r) => state.keys[r.key]?.screenshot).map((r) => r.key));
|
|
2155
2386
|
return { skipped: keys.size };
|
|
2156
2387
|
}
|
|
2157
|
-
async function runLocaleParallel(reqs, provider,
|
|
2388
|
+
async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal) {
|
|
2158
2389
|
if (!reqs.length) return [];
|
|
2159
2390
|
const byLocale = /* @__PURE__ */ new Map();
|
|
2160
2391
|
for (const req of reqs) {
|
|
@@ -2174,11 +2405,14 @@ async function runLocaleParallel(reqs, provider, onBatchComplete, concurrency =
|
|
|
2174
2405
|
while (next < groups.length) {
|
|
2175
2406
|
if (signal?.aborted) break;
|
|
2176
2407
|
const group = groups[next++];
|
|
2408
|
+
const locale = group[0].targetLocale;
|
|
2409
|
+
hooks.onLocaleStart?.(locale);
|
|
2177
2410
|
const localeResults = await provider.translate(group, (_localeDone, _localeTotal, batchResults) => {
|
|
2178
2411
|
done += batchResults.length;
|
|
2179
|
-
onBatchComplete?.(done, total, batchResults);
|
|
2412
|
+
hooks.onBatchComplete?.(done, total, batchResults, locale);
|
|
2180
2413
|
}, signal);
|
|
2181
2414
|
allResults.push(...localeResults);
|
|
2415
|
+
if (!signal?.aborted) hooks.onLocaleDone?.(locale);
|
|
2182
2416
|
}
|
|
2183
2417
|
}
|
|
2184
2418
|
const workers = Array.from({ length: Math.min(concurrency, groups.length) }, worker);
|
|
@@ -2200,10 +2434,11 @@ function applyResults(state, reqs, results, clock = systemClock, force = false)
|
|
|
2200
2434
|
if (applyMachineTranslationForms(state, req.key, req.targetLocale, res.forms, clock, force)) written++;
|
|
2201
2435
|
continue;
|
|
2202
2436
|
}
|
|
2203
|
-
if (res.
|
|
2437
|
+
if (res.translation === void 0) {
|
|
2204
2438
|
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
2205
2439
|
continue;
|
|
2206
2440
|
}
|
|
2441
|
+
if (res.error) errors.push({ key: req.key, locale: req.targetLocale, error: res.error });
|
|
2207
2442
|
if (applyMachineTranslation(state, req.key, req.targetLocale, res.translation, clock, force)) written++;
|
|
2208
2443
|
}
|
|
2209
2444
|
return { written, errors };
|
|
@@ -2228,49 +2463,46 @@ var init_run = __esm({
|
|
|
2228
2463
|
}
|
|
2229
2464
|
});
|
|
2230
2465
|
|
|
2231
|
-
// src/server/
|
|
2232
|
-
import { appendFileSync, readFileSync as
|
|
2233
|
-
import { resolve as
|
|
2466
|
+
// src/server/log.ts
|
|
2467
|
+
import { appendFileSync, readFileSync as readFileSync6, existsSync as existsSync5 } from "fs";
|
|
2468
|
+
import { resolve as resolve5 } from "path";
|
|
2234
2469
|
function logPath(projectRoot) {
|
|
2235
|
-
return
|
|
2470
|
+
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
2236
2471
|
}
|
|
2237
|
-
function
|
|
2238
|
-
|
|
2472
|
+
function appendLog(projectRoot, entry) {
|
|
2473
|
+
ensureGlotfileDir(projectRoot);
|
|
2239
2474
|
appendFileSync(logPath(projectRoot), JSON.stringify(entry) + "\n", "utf8");
|
|
2240
2475
|
}
|
|
2241
|
-
function
|
|
2476
|
+
function readLog(projectRoot, limit = 100) {
|
|
2242
2477
|
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
|
-
});
|
|
2478
|
+
if (!existsSync5(path)) return [];
|
|
2479
|
+
const lines = readFileSync6(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
2480
|
+
const entries = lines.map((l) => JSON.parse(l));
|
|
2250
2481
|
return entries.reverse().slice(0, limit);
|
|
2251
2482
|
}
|
|
2252
2483
|
var init_log = __esm({
|
|
2253
|
-
"src/server/
|
|
2484
|
+
"src/server/log.ts"() {
|
|
2254
2485
|
"use strict";
|
|
2486
|
+
init_glotfile_dir();
|
|
2255
2487
|
}
|
|
2256
2488
|
});
|
|
2257
2489
|
|
|
2258
2490
|
// src/server/scan.ts
|
|
2259
|
-
import { existsSync as
|
|
2260
|
-
import { resolve as
|
|
2491
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
2492
|
+
import { resolve as resolve6 } from "path";
|
|
2261
2493
|
function loadUsageCache(projectRoot) {
|
|
2262
|
-
const path =
|
|
2263
|
-
if (!
|
|
2494
|
+
const path = resolve6(projectRoot, ".glotfile", "usage.json");
|
|
2495
|
+
if (!existsSync6(path)) return null;
|
|
2264
2496
|
try {
|
|
2265
|
-
return JSON.parse(
|
|
2497
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
2266
2498
|
} catch {
|
|
2267
2499
|
return null;
|
|
2268
2500
|
}
|
|
2269
2501
|
}
|
|
2270
2502
|
function saveUsageCache(projectRoot, cache2) {
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2503
|
+
ensureGlotfileDir(projectRoot);
|
|
2504
|
+
const path = resolve6(projectRoot, ".glotfile", "usage.json");
|
|
2505
|
+
writeFileAtomic(path, JSON.stringify(cache2, null, 2) + "\n");
|
|
2274
2506
|
}
|
|
2275
2507
|
function findMissing(state) {
|
|
2276
2508
|
const targets = state.config.locales.filter((l) => l !== state.config.sourceLocale).sort();
|
|
@@ -2297,12 +2529,14 @@ function computeUsedKeys(state, cache2) {
|
|
|
2297
2529
|
var init_scan = __esm({
|
|
2298
2530
|
"src/server/scan.ts"() {
|
|
2299
2531
|
"use strict";
|
|
2532
|
+
init_atomic_write();
|
|
2533
|
+
init_glotfile_dir();
|
|
2300
2534
|
}
|
|
2301
2535
|
});
|
|
2302
2536
|
|
|
2303
2537
|
// src/server/scanner.ts
|
|
2304
|
-
import { readdirSync as readdirSync2, statSync, readFileSync as
|
|
2305
|
-
import { join as
|
|
2538
|
+
import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync8 } from "fs";
|
|
2539
|
+
import { join as join3, extname as extname2, relative } from "path";
|
|
2306
2540
|
function scannerForExt(ext) {
|
|
2307
2541
|
return EXT_SCANNER[ext] ?? null;
|
|
2308
2542
|
}
|
|
@@ -2428,7 +2662,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
2428
2662
|
}
|
|
2429
2663
|
for (const name of entries) {
|
|
2430
2664
|
if (ALWAYS_EXCLUDE.has(name)) continue;
|
|
2431
|
-
const abs =
|
|
2665
|
+
const abs = join3(dir, name);
|
|
2432
2666
|
const rel = relative(root, abs);
|
|
2433
2667
|
let st;
|
|
2434
2668
|
try {
|
|
@@ -2458,7 +2692,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
2458
2692
|
const ext = extname2(relPath);
|
|
2459
2693
|
const scanner = scannerForExt(ext);
|
|
2460
2694
|
if (!scanner) continue;
|
|
2461
|
-
const abs =
|
|
2695
|
+
const abs = join3(projectRoot, relPath);
|
|
2462
2696
|
let st;
|
|
2463
2697
|
try {
|
|
2464
2698
|
st = statSync(abs);
|
|
@@ -2474,7 +2708,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
2474
2708
|
}
|
|
2475
2709
|
let content;
|
|
2476
2710
|
try {
|
|
2477
|
-
content =
|
|
2711
|
+
content = readFileSync8(abs, "utf8");
|
|
2478
2712
|
} catch {
|
|
2479
2713
|
continue;
|
|
2480
2714
|
}
|
|
@@ -2579,8 +2813,8 @@ var init_scanner = __esm({
|
|
|
2579
2813
|
});
|
|
2580
2814
|
|
|
2581
2815
|
// src/server/ai/context.ts
|
|
2582
|
-
import { existsSync as
|
|
2583
|
-
import { resolve as
|
|
2816
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
|
|
2817
|
+
import { resolve as resolve7 } from "path";
|
|
2584
2818
|
function globToRegExp2(glob) {
|
|
2585
2819
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
2586
2820
|
return new RegExp(`^${escaped}$`);
|
|
@@ -2592,10 +2826,10 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
2592
2826
|
const extraRefs = filtered.length > MAX_SNIPPETS ? filtered.length - MAX_SNIPPETS : 0;
|
|
2593
2827
|
const snippets = [];
|
|
2594
2828
|
for (const ref of selected) {
|
|
2595
|
-
const absPath =
|
|
2829
|
+
const absPath = resolve7(projectRoot, ref.file);
|
|
2596
2830
|
if (!fileCache.has(ref.file)) {
|
|
2597
|
-
if (!
|
|
2598
|
-
const content =
|
|
2831
|
+
if (!existsSync7(absPath)) continue;
|
|
2832
|
+
const content = readFileSync9(absPath, "utf8");
|
|
2599
2833
|
fileCache.set(ref.file, content.split("\n"));
|
|
2600
2834
|
}
|
|
2601
2835
|
const lines = fileCache.get(ref.file);
|
|
@@ -2740,8 +2974,8 @@ var init_context = __esm({
|
|
|
2740
2974
|
});
|
|
2741
2975
|
|
|
2742
2976
|
// src/server/import/detect.ts
|
|
2743
|
-
import { existsSync as
|
|
2744
|
-
import { join as
|
|
2977
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
2978
|
+
import { join as join4 } from "path";
|
|
2745
2979
|
function safeIsDir(p) {
|
|
2746
2980
|
try {
|
|
2747
2981
|
return statSync2(p).isDirectory();
|
|
@@ -2750,7 +2984,7 @@ function safeIsDir(p) {
|
|
|
2750
2984
|
}
|
|
2751
2985
|
}
|
|
2752
2986
|
function listDirs(dir) {
|
|
2753
|
-
return readdirSync3(dir).filter((e) => safeIsDir(
|
|
2987
|
+
return readdirSync3(dir).filter((e) => safeIsDir(join4(dir, e)));
|
|
2754
2988
|
}
|
|
2755
2989
|
function fileCount(dir) {
|
|
2756
2990
|
try {
|
|
@@ -2764,22 +2998,22 @@ function pickSource(locales, sizeOf) {
|
|
|
2764
2998
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
2765
2999
|
}
|
|
2766
3000
|
function detectLaravel(root) {
|
|
2767
|
-
const localeRoot = [
|
|
3001
|
+
const localeRoot = [join4(root, "resources", "lang"), join4(root, "lang")].find(safeIsDir);
|
|
2768
3002
|
if (!localeRoot) return null;
|
|
2769
3003
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
2770
3004
|
if (locales.length === 0) return null;
|
|
2771
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
3005
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join4(localeRoot, loc)));
|
|
2772
3006
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
2773
3007
|
}
|
|
2774
3008
|
function detectVue(root) {
|
|
2775
3009
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
2776
|
-
const localeRoot =
|
|
3010
|
+
const localeRoot = join4(root, rel);
|
|
2777
3011
|
if (!safeIsDir(localeRoot)) continue;
|
|
2778
3012
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
2779
3013
|
if (locales.length >= 2) {
|
|
2780
3014
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
2781
3015
|
try {
|
|
2782
|
-
return statSync2(
|
|
3016
|
+
return statSync2(join4(localeRoot, `${loc}.json`)).size;
|
|
2783
3017
|
} catch {
|
|
2784
3018
|
return 0;
|
|
2785
3019
|
}
|
|
@@ -2791,7 +3025,7 @@ function detectVue(root) {
|
|
|
2791
3025
|
}
|
|
2792
3026
|
function detectArb(root) {
|
|
2793
3027
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
2794
|
-
const localeRoot =
|
|
3028
|
+
const localeRoot = join4(root, rel);
|
|
2795
3029
|
if (!safeIsDir(localeRoot)) continue;
|
|
2796
3030
|
const locales = readdirSync3(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
2797
3031
|
if (locales.length >= 1) {
|
|
@@ -2801,7 +3035,7 @@ function detectArb(root) {
|
|
|
2801
3035
|
return null;
|
|
2802
3036
|
}
|
|
2803
3037
|
function detect(root, formatOverride) {
|
|
2804
|
-
if (!
|
|
3038
|
+
if (!existsSync9(root)) return null;
|
|
2805
3039
|
if (formatOverride) {
|
|
2806
3040
|
const fn = BY_FORMAT[formatOverride];
|
|
2807
3041
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -2856,8 +3090,8 @@ var init_flatten = __esm({
|
|
|
2856
3090
|
});
|
|
2857
3091
|
|
|
2858
3092
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
2859
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
2860
|
-
import { join as
|
|
3093
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
|
|
3094
|
+
import { join as join5 } from "path";
|
|
2861
3095
|
var LOCALE_RE2, vueI18nJson2;
|
|
2862
3096
|
var init_vue_i18n_json2 = __esm({
|
|
2863
3097
|
"src/server/import/parsers/vue-i18n-json.ts"() {
|
|
@@ -2877,7 +3111,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
2877
3111
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
2878
3112
|
let data;
|
|
2879
3113
|
try {
|
|
2880
|
-
data = JSON.parse(
|
|
3114
|
+
data = JSON.parse(readFileSync11(join5(localeRoot, file), "utf8"));
|
|
2881
3115
|
} catch (e) {
|
|
2882
3116
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
2883
3117
|
continue;
|
|
@@ -2905,16 +3139,16 @@ var init_placeholders2 = __esm({
|
|
|
2905
3139
|
|
|
2906
3140
|
// src/server/import/parsers/laravel-php.ts
|
|
2907
3141
|
import { readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
2908
|
-
import { join as
|
|
3142
|
+
import { join as join6, relative as relative2 } from "path";
|
|
2909
3143
|
import { execFileSync } from "child_process";
|
|
2910
3144
|
function listDirs2(dir) {
|
|
2911
|
-
return readdirSync5(dir).filter((e) => statSync3(
|
|
3145
|
+
return readdirSync5(dir).filter((e) => statSync3(join6(dir, e)).isDirectory());
|
|
2912
3146
|
}
|
|
2913
3147
|
function listPhpFiles(dir) {
|
|
2914
3148
|
const out = [];
|
|
2915
3149
|
const walk = (d) => {
|
|
2916
3150
|
for (const e of readdirSync5(d)) {
|
|
2917
|
-
const full =
|
|
3151
|
+
const full = join6(d, e);
|
|
2918
3152
|
if (statSync3(full).isDirectory()) walk(full);
|
|
2919
3153
|
else if (e.endsWith(".php")) out.push(full);
|
|
2920
3154
|
}
|
|
@@ -2957,7 +3191,7 @@ var init_laravel_php2 = __esm({
|
|
|
2957
3191
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
2958
3192
|
if (locale === "vendor") continue;
|
|
2959
3193
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
2960
|
-
const localeDir =
|
|
3194
|
+
const localeDir = join6(localeRoot, locale);
|
|
2961
3195
|
locales.push(locale);
|
|
2962
3196
|
for (const file of listPhpFiles(localeDir)) {
|
|
2963
3197
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -2982,8 +3216,8 @@ var init_laravel_php2 = __esm({
|
|
|
2982
3216
|
});
|
|
2983
3217
|
|
|
2984
3218
|
// src/server/import/parsers/flutter-arb.ts
|
|
2985
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
2986
|
-
import { join as
|
|
3219
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync12 } from "fs";
|
|
3220
|
+
import { join as join7 } from "path";
|
|
2987
3221
|
function localeFromArbName(file) {
|
|
2988
3222
|
const m = file.match(/^(.+)\.arb$/);
|
|
2989
3223
|
if (!m) return null;
|
|
@@ -3023,7 +3257,7 @@ var init_flutter_arb2 = __esm({
|
|
|
3023
3257
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
3024
3258
|
let data;
|
|
3025
3259
|
try {
|
|
3026
|
-
data = JSON.parse(
|
|
3260
|
+
data = JSON.parse(readFileSync12(join7(localeRoot, file), "utf8"));
|
|
3027
3261
|
} catch (e) {
|
|
3028
3262
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
3029
3263
|
continue;
|
|
@@ -3127,7 +3361,6 @@ function assemble2(parsed, opts) {
|
|
|
3127
3361
|
sourceLocale,
|
|
3128
3362
|
locales,
|
|
3129
3363
|
outputs: [output],
|
|
3130
|
-
ai: { provider: "anthropic", model: "claude-opus-4-8", endpoint: null, batchSize: 25 },
|
|
3131
3364
|
format: { indent: 2, sortKeys: true, finalNewline: true },
|
|
3132
3365
|
spelling: { customWords: [] }
|
|
3133
3366
|
},
|
|
@@ -3554,16 +3787,48 @@ var init_checks = __esm({
|
|
|
3554
3787
|
}
|
|
3555
3788
|
});
|
|
3556
3789
|
|
|
3790
|
+
// src/server/ui-prefs.ts
|
|
3791
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
3792
|
+
import { homedir } from "os";
|
|
3793
|
+
import { join as join8 } from "path";
|
|
3794
|
+
function readJson2(path) {
|
|
3795
|
+
try {
|
|
3796
|
+
const parsed = JSON.parse(readFileSync13(path, "utf8"));
|
|
3797
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3798
|
+
} catch {
|
|
3799
|
+
return {};
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
function loadUiPrefs(path) {
|
|
3803
|
+
const raw = readJson2(path);
|
|
3804
|
+
return { theme: isThemeMode(raw.theme) ? raw.theme : DEFAULTS.theme };
|
|
3805
|
+
}
|
|
3806
|
+
function saveUiPrefs(path, prefs) {
|
|
3807
|
+
const merged = { ...readJson2(path), ...prefs };
|
|
3808
|
+
writeFileAtomic(path, JSON.stringify(merged, null, 2) + "\n");
|
|
3809
|
+
}
|
|
3810
|
+
var THEMES, isThemeMode, defaultUiPrefsPath, DEFAULTS;
|
|
3811
|
+
var init_ui_prefs = __esm({
|
|
3812
|
+
"src/server/ui-prefs.ts"() {
|
|
3813
|
+
"use strict";
|
|
3814
|
+
init_atomic_write();
|
|
3815
|
+
THEMES = ["system", "light", "dark"];
|
|
3816
|
+
isThemeMode = (v) => THEMES.includes(v);
|
|
3817
|
+
defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
|
|
3818
|
+
DEFAULTS = { theme: "system" };
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
|
|
3557
3822
|
// src/server/api.ts
|
|
3558
3823
|
import { Hono } from "hono";
|
|
3559
3824
|
import { streamSSE } from "hono/streaming";
|
|
3560
|
-
import {
|
|
3561
|
-
import { dirname as
|
|
3825
|
+
import { readFileSync as readFileSync14, existsSync as existsSync10, readdirSync as readdirSync7, rmSync as rmSync4 } from "fs";
|
|
3826
|
+
import { dirname as dirname2, resolve as resolve9, basename, relative as relative3, sep } from "path";
|
|
3562
3827
|
function projectName(root) {
|
|
3563
|
-
const nameFile =
|
|
3564
|
-
if (
|
|
3828
|
+
const nameFile = resolve9(root, ".idea", ".name");
|
|
3829
|
+
if (existsSync10(nameFile)) {
|
|
3565
3830
|
try {
|
|
3566
|
-
const name =
|
|
3831
|
+
const name = readFileSync14(nameFile, "utf8").trim();
|
|
3567
3832
|
if (name) return name;
|
|
3568
3833
|
} catch {
|
|
3569
3834
|
}
|
|
@@ -3573,7 +3838,7 @@ function projectName(root) {
|
|
|
3573
3838
|
function createApi(deps) {
|
|
3574
3839
|
const app = new Hono();
|
|
3575
3840
|
const load = () => loadState(deps.statePath);
|
|
3576
|
-
const projectRoot =
|
|
3841
|
+
const projectRoot = dirname2(resolve9(deps.statePath));
|
|
3577
3842
|
let translateQueue = Promise.resolve();
|
|
3578
3843
|
const withTranslateLock = (fn) => {
|
|
3579
3844
|
const next = translateQueue.then(fn, fn);
|
|
@@ -3597,7 +3862,36 @@ function createApi(deps) {
|
|
|
3597
3862
|
saveState(deps.statePath, s);
|
|
3598
3863
|
scheduleAutoExport(s);
|
|
3599
3864
|
};
|
|
3865
|
+
const logChange = (entry) => appendLog(projectRoot, { ...entry, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3866
|
+
const valueText = (s, key, locale) => s.keys[key]?.values[locale]?.value;
|
|
3867
|
+
const uiPrefsPath = deps.uiPrefsPath ?? defaultUiPrefsPath();
|
|
3600
3868
|
app.get("/state", (c) => c.json(load()));
|
|
3869
|
+
app.get("/ui-prefs", (c) => c.json(loadUiPrefs(uiPrefsPath)));
|
|
3870
|
+
app.put("/ui-prefs", async (c) => {
|
|
3871
|
+
const { theme } = await c.req.json();
|
|
3872
|
+
if (!isThemeMode(theme)) return c.json({ error: "theme must be system, light, or dark" }, 400);
|
|
3873
|
+
saveUiPrefs(uiPrefsPath, { theme });
|
|
3874
|
+
return c.json({ ok: true });
|
|
3875
|
+
});
|
|
3876
|
+
app.get("/local-settings", (c) => c.json(loadLocalSettings(projectRoot)));
|
|
3877
|
+
app.put("/local-settings", async (c) => {
|
|
3878
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3879
|
+
const patch = {};
|
|
3880
|
+
if (body.ai !== void 0) {
|
|
3881
|
+
const err = aiConfigError(body.ai);
|
|
3882
|
+
if (err) return c.json({ error: err }, 400);
|
|
3883
|
+
patch.ai = body.ai;
|
|
3884
|
+
}
|
|
3885
|
+
if (body.editor !== void 0) {
|
|
3886
|
+
if (!isEditorId(body.editor)) return c.json({ error: "editor must be one of: vscode, zed, phpstorm" }, 400);
|
|
3887
|
+
patch.editor = body.editor;
|
|
3888
|
+
}
|
|
3889
|
+
if (patch.ai === void 0 && patch.editor === void 0) {
|
|
3890
|
+
return c.json({ error: "provide ai and/or editor" }, 400);
|
|
3891
|
+
}
|
|
3892
|
+
saveLocalSettings(projectRoot, patch);
|
|
3893
|
+
return c.json({ ok: true });
|
|
3894
|
+
});
|
|
3601
3895
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
3602
3896
|
app.get("/files", (c) => {
|
|
3603
3897
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -3609,10 +3903,10 @@ function createApi(deps) {
|
|
|
3609
3903
|
}
|
|
3610
3904
|
for (const name of entries) {
|
|
3611
3905
|
let abs;
|
|
3612
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
3613
|
-
abs =
|
|
3906
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync10(resolve9(projectRoot, name, "config.json"))) {
|
|
3907
|
+
abs = resolve9(projectRoot, `${name}.json`);
|
|
3614
3908
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
3615
|
-
abs =
|
|
3909
|
+
abs = resolve9(projectRoot, name);
|
|
3616
3910
|
} else {
|
|
3617
3911
|
continue;
|
|
3618
3912
|
}
|
|
@@ -3629,10 +3923,10 @@ function createApi(deps) {
|
|
|
3629
3923
|
app.post("/file", async (c) => {
|
|
3630
3924
|
const { path } = await c.req.json();
|
|
3631
3925
|
if (typeof path !== "string") return c.json({ error: "path must be a string" }, 400);
|
|
3632
|
-
const resolved =
|
|
3926
|
+
const resolved = resolve9(projectRoot, path);
|
|
3633
3927
|
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep);
|
|
3634
3928
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
3635
|
-
if (!
|
|
3929
|
+
if (!existsSync10(resolved)) return c.json({ error: "file not found" }, 400);
|
|
3636
3930
|
loadState(resolved);
|
|
3637
3931
|
deps.statePath = resolved;
|
|
3638
3932
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -3647,6 +3941,7 @@ function createApi(deps) {
|
|
|
3647
3941
|
const s = load();
|
|
3648
3942
|
createKey(s, key, value, void 0, plural ? { plural: { arg: plural.arg } } : {});
|
|
3649
3943
|
persist(s);
|
|
3944
|
+
logChange({ kind: "key", summary: `Created key ${key}`, key, after: value });
|
|
3650
3945
|
console.log(`[key] created ${key}`);
|
|
3651
3946
|
return c.json({ ok: true });
|
|
3652
3947
|
});
|
|
@@ -3656,37 +3951,45 @@ function createApi(deps) {
|
|
|
3656
3951
|
const s = load();
|
|
3657
3952
|
addCustomWord(s, word);
|
|
3658
3953
|
persist(s);
|
|
3954
|
+
logChange({ kind: "dictionary", summary: `Added "${word}" to dictionary`, after: word });
|
|
3659
3955
|
return c.json({ ok: true });
|
|
3660
3956
|
});
|
|
3661
3957
|
app.delete("/dictionary/:word", (c) => {
|
|
3662
3958
|
const s = load();
|
|
3663
|
-
|
|
3959
|
+
const word = c.req.param("word");
|
|
3960
|
+
removeCustomWord(s, word);
|
|
3664
3961
|
persist(s);
|
|
3962
|
+
logChange({ kind: "dictionary", summary: `Removed "${word}" from dictionary`, before: word });
|
|
3665
3963
|
return c.json({ ok: true });
|
|
3666
3964
|
});
|
|
3667
3965
|
app.patch("/keys/:key", async (c) => {
|
|
3668
3966
|
const key = c.req.param("key");
|
|
3669
3967
|
const body = await c.req.json();
|
|
3670
3968
|
const s = load();
|
|
3969
|
+
const beforeSource = typeof body.source === "string" ? valueText(s, key, s.config.sourceLocale) : void 0;
|
|
3671
3970
|
if (typeof body.rename === "string") renameKey(s, key, body.rename);
|
|
3672
3971
|
const target = typeof body.rename === "string" ? body.rename : key;
|
|
3673
3972
|
if (body.metadata) setMetadata(s, target, body.metadata);
|
|
3674
3973
|
if (typeof body.source === "string") setSourceValue(s, target, body.source);
|
|
3675
3974
|
if (typeof body.pluralArg === "string" && body.pluralArg.trim()) setPluralArg(s, target, body.pluralArg.trim());
|
|
3676
3975
|
persist(s);
|
|
3976
|
+
if (typeof body.rename === "string") logChange({ kind: "key", summary: `Renamed ${key} \u2192 ${body.rename}`, key: target, before: key, after: body.rename });
|
|
3977
|
+
if (body.metadata) logChange({ kind: "metadata", summary: `Updated metadata of ${target}`, key: target, after: body.metadata });
|
|
3978
|
+
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 });
|
|
3979
|
+
if (typeof body.pluralArg === "string" && body.pluralArg.trim()) logChange({ kind: "key", summary: `Changed plural arg of ${target}`, key: target, after: body.pluralArg.trim() });
|
|
3677
3980
|
if (typeof body.rename === "string") console.log(`[key] renamed ${key} \u2192 ${body.rename}`);
|
|
3678
3981
|
return c.json({ ok: true });
|
|
3679
3982
|
});
|
|
3680
3983
|
function removeOrphanScreenshot(s, screenshot) {
|
|
3681
3984
|
if (!screenshot) return;
|
|
3682
3985
|
for (const e of Object.values(s.keys)) if (e.screenshot === screenshot) return;
|
|
3683
|
-
const root =
|
|
3684
|
-
const abs =
|
|
3986
|
+
const root = dirname2(resolve9(deps.statePath));
|
|
3987
|
+
const abs = resolve9(root, screenshot);
|
|
3685
3988
|
const rel = relative3(root, abs);
|
|
3686
3989
|
const seg0 = rel.split(sep)[0] ?? "";
|
|
3687
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
3990
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync10(abs)) {
|
|
3688
3991
|
try {
|
|
3689
|
-
|
|
3992
|
+
rmSync4(abs);
|
|
3690
3993
|
} catch {
|
|
3691
3994
|
}
|
|
3692
3995
|
}
|
|
@@ -3695,9 +3998,11 @@ function createApi(deps) {
|
|
|
3695
3998
|
const s = load();
|
|
3696
3999
|
const key = c.req.param("key");
|
|
3697
4000
|
const shot = s.keys[key]?.screenshot;
|
|
4001
|
+
const before = valueText(s, key, s.config.sourceLocale);
|
|
3698
4002
|
deleteKey(s, key);
|
|
3699
4003
|
removeOrphanScreenshot(s, shot);
|
|
3700
4004
|
persist(s);
|
|
4005
|
+
logChange({ kind: "key", summary: `Deleted key ${key}`, key, before });
|
|
3701
4006
|
console.log(`[key] deleted ${key}`);
|
|
3702
4007
|
return c.json({ ok: true });
|
|
3703
4008
|
});
|
|
@@ -3721,6 +4026,7 @@ function createApi(deps) {
|
|
|
3721
4026
|
}
|
|
3722
4027
|
}
|
|
3723
4028
|
persist(s);
|
|
4029
|
+
if (cleared) logChange({ kind: "translation", summary: `Cleared ${cleared} value(s) across ${keys.length} key(s)`, after: { locales } });
|
|
3724
4030
|
console.log(`[bulk] cleared ${cleared} value(s)`);
|
|
3725
4031
|
return c.json({ cleared });
|
|
3726
4032
|
});
|
|
@@ -3738,6 +4044,7 @@ function createApi(deps) {
|
|
|
3738
4044
|
}
|
|
3739
4045
|
for (const shot of shots) removeOrphanScreenshot(s, shot);
|
|
3740
4046
|
persist(s);
|
|
4047
|
+
if (removed.length) logChange({ kind: "key", summary: `Deleted ${removed.length} key(s)`, before: removed });
|
|
3741
4048
|
console.log(`[bulk] deleted ${removed.length} key(s)`);
|
|
3742
4049
|
return c.json({ removed });
|
|
3743
4050
|
});
|
|
@@ -3763,6 +4070,7 @@ function createApi(deps) {
|
|
|
3763
4070
|
updated++;
|
|
3764
4071
|
}
|
|
3765
4072
|
persist(s);
|
|
4073
|
+
if (updated) logChange({ kind: "metadata", summary: `Updated metadata on ${updated} key(s)` });
|
|
3766
4074
|
console.log(`[bulk] updated metadata on ${updated} key(s)`);
|
|
3767
4075
|
return c.json({ updated });
|
|
3768
4076
|
});
|
|
@@ -3786,6 +4094,7 @@ function createApi(deps) {
|
|
|
3786
4094
|
}
|
|
3787
4095
|
}
|
|
3788
4096
|
persist(s);
|
|
4097
|
+
if (updated) logChange({ kind: "translation", summary: `Marked ${updated} value(s) as ${next}`, after: next });
|
|
3789
4098
|
console.log(`[bulk] set state ${next} on ${updated} value(s)`);
|
|
3790
4099
|
return c.json({ updated });
|
|
3791
4100
|
});
|
|
@@ -3795,15 +4104,21 @@ function createApi(deps) {
|
|
|
3795
4104
|
const s = load();
|
|
3796
4105
|
const key = c.req.param("key");
|
|
3797
4106
|
const locale = c.req.param("locale");
|
|
4107
|
+
const before = valueText(s, key, locale);
|
|
3798
4108
|
if (locale === s.config.sourceLocale) setSourceValue(s, key, value);
|
|
3799
4109
|
else setTargetValue(s, key, locale, value);
|
|
3800
4110
|
persist(s);
|
|
4111
|
+
logChange({ kind: "translation", summary: `Set ${locale} value of ${key}`, key, locale, before, after: value });
|
|
3801
4112
|
return c.json({ ok: true });
|
|
3802
4113
|
});
|
|
3803
4114
|
app.delete("/keys/:key/values/:locale", (c) => {
|
|
3804
4115
|
const s = load();
|
|
3805
|
-
|
|
4116
|
+
const key = c.req.param("key");
|
|
4117
|
+
const locale = c.req.param("locale");
|
|
4118
|
+
const before = valueText(s, key, locale);
|
|
4119
|
+
clearValue(s, key, locale);
|
|
3806
4120
|
persist(s);
|
|
4121
|
+
logChange({ kind: "translation", summary: `Cleared ${locale} value of ${key}`, key, locale, before });
|
|
3807
4122
|
return c.json({ ok: true });
|
|
3808
4123
|
});
|
|
3809
4124
|
app.put("/keys/:key/plural/:locale", async (c) => {
|
|
@@ -3812,52 +4127,68 @@ function createApi(deps) {
|
|
|
3812
4127
|
const s = load();
|
|
3813
4128
|
const key = c.req.param("key");
|
|
3814
4129
|
const locale = c.req.param("locale");
|
|
4130
|
+
const before = s.keys[key]?.values[locale]?.forms;
|
|
3815
4131
|
if (locale === s.config.sourceLocale) setSourcePluralForms(s, key, forms);
|
|
3816
4132
|
else setPluralForms(s, key, locale, forms);
|
|
3817
4133
|
persist(s);
|
|
4134
|
+
logChange({ kind: "translation", summary: `Set ${locale} plural forms of ${key}`, key, locale, before, after: forms });
|
|
3818
4135
|
return c.json({ ok: true });
|
|
3819
4136
|
});
|
|
3820
4137
|
app.post("/keys/:key/plural", async (c) => {
|
|
3821
4138
|
const { arg } = await c.req.json();
|
|
3822
4139
|
if (typeof arg !== "string" || !arg.trim()) return c.json({ error: "arg is required" }, 400);
|
|
3823
4140
|
const s = load();
|
|
3824
|
-
|
|
4141
|
+
const key = c.req.param("key");
|
|
4142
|
+
convertToPlural(s, key, arg);
|
|
3825
4143
|
persist(s);
|
|
4144
|
+
logChange({ kind: "key", summary: `Converted ${key} to plural`, key, after: arg });
|
|
3826
4145
|
return c.json({ ok: true });
|
|
3827
4146
|
});
|
|
3828
4147
|
app.delete("/keys/:key/plural", (c) => {
|
|
3829
4148
|
const s = load();
|
|
3830
|
-
|
|
4149
|
+
const key = c.req.param("key");
|
|
4150
|
+
convertToScalar(s, key);
|
|
3831
4151
|
persist(s);
|
|
4152
|
+
logChange({ kind: "key", summary: `Converted ${key} to scalar`, key });
|
|
3832
4153
|
return c.json({ ok: true });
|
|
3833
4154
|
});
|
|
3834
4155
|
app.put("/keys/:key/values/:locale/state", async (c) => {
|
|
3835
4156
|
const { state } = await c.req.json();
|
|
3836
4157
|
const s = load();
|
|
3837
|
-
|
|
4158
|
+
const key = c.req.param("key");
|
|
4159
|
+
const locale = c.req.param("locale");
|
|
4160
|
+
const before = s.keys[key]?.values[locale]?.state;
|
|
4161
|
+
setKeyState(s, key, locale, state);
|
|
3838
4162
|
persist(s);
|
|
4163
|
+
logChange({ kind: "translation", summary: `Marked ${key} ${locale} as ${state}`, key, locale, before, after: state });
|
|
3839
4164
|
return c.json({ ok: true });
|
|
3840
4165
|
});
|
|
3841
4166
|
app.post("/keys/:key/notes", async (c) => {
|
|
3842
4167
|
const { text } = await c.req.json();
|
|
3843
4168
|
if (typeof text !== "string" || !text.trim()) return c.json({ error: "note text is required" }, 400);
|
|
3844
4169
|
const s = load();
|
|
3845
|
-
const
|
|
4170
|
+
const key = c.req.param("key");
|
|
4171
|
+
const note = addNote(s, key, text);
|
|
3846
4172
|
persist(s);
|
|
4173
|
+
logChange({ kind: "note", summary: `Added note to ${key}`, key, after: text });
|
|
3847
4174
|
return c.json(note);
|
|
3848
4175
|
});
|
|
3849
4176
|
app.put("/keys/:key/notes/:id", async (c) => {
|
|
3850
4177
|
const { text } = await c.req.json();
|
|
3851
4178
|
if (typeof text !== "string" || !text.trim()) return c.json({ error: "note text is required" }, 400);
|
|
3852
4179
|
const s = load();
|
|
3853
|
-
|
|
4180
|
+
const key = c.req.param("key");
|
|
4181
|
+
editNote(s, key, c.req.param("id"), text);
|
|
3854
4182
|
persist(s);
|
|
4183
|
+
logChange({ kind: "note", summary: `Edited note on ${key}`, key, after: text });
|
|
3855
4184
|
return c.json({ ok: true });
|
|
3856
4185
|
});
|
|
3857
4186
|
app.delete("/keys/:key/notes/:id", (c) => {
|
|
3858
4187
|
const s = load();
|
|
3859
|
-
|
|
4188
|
+
const key = c.req.param("key");
|
|
4189
|
+
deleteNote(s, key, c.req.param("id"));
|
|
3860
4190
|
persist(s);
|
|
4191
|
+
logChange({ kind: "note", summary: `Deleted note on ${key}`, key });
|
|
3861
4192
|
return c.json({ ok: true });
|
|
3862
4193
|
});
|
|
3863
4194
|
app.put("/config", async (c) => {
|
|
@@ -3866,6 +4197,7 @@ function createApi(deps) {
|
|
|
3866
4197
|
return c.json({ error: "config.locales must be an array" }, 400);
|
|
3867
4198
|
}
|
|
3868
4199
|
const s = load();
|
|
4200
|
+
const beforeCfg = { locales: s.config.locales };
|
|
3869
4201
|
const removed = s.config.locales.filter((l) => !newConfig.locales.includes(l));
|
|
3870
4202
|
for (const l of removed) {
|
|
3871
4203
|
for (const e of Object.values(s.keys)) delete e.values[l];
|
|
@@ -3873,7 +4205,8 @@ function createApi(deps) {
|
|
|
3873
4205
|
s.config = newConfig;
|
|
3874
4206
|
validate(s);
|
|
3875
4207
|
persist(s);
|
|
3876
|
-
|
|
4208
|
+
logChange({ kind: "config", summary: `Saved config (${newConfig.locales.length} locale(s))`, before: beforeCfg, after: { locales: newConfig.locales } });
|
|
4209
|
+
console.log(`[config] saved \u2014 ${newConfig.locales.length} locale(s)`);
|
|
3877
4210
|
return c.json({ ok: true });
|
|
3878
4211
|
});
|
|
3879
4212
|
app.get("/glossary", (c) => c.json(load().glossary));
|
|
@@ -3881,14 +4214,19 @@ function createApi(deps) {
|
|
|
3881
4214
|
const entry = await c.req.json();
|
|
3882
4215
|
if (typeof entry?.term !== "string") return c.json({ error: "term must be a string" }, 400);
|
|
3883
4216
|
const s = load();
|
|
4217
|
+
const before = s.glossary.find((g) => g.term === entry.term);
|
|
3884
4218
|
upsertGlossaryEntry(s, entry);
|
|
3885
4219
|
persist(s);
|
|
4220
|
+
logChange({ kind: "glossary", summary: `${before ? "Updated" : "Added"} glossary term "${entry.term}"`, before, after: entry });
|
|
3886
4221
|
return c.json({ ok: true });
|
|
3887
4222
|
});
|
|
3888
4223
|
app.delete("/glossary/:term", (c) => {
|
|
3889
4224
|
const s = load();
|
|
3890
|
-
|
|
4225
|
+
const term = decodeURIComponent(c.req.param("term"));
|
|
4226
|
+
const before = s.glossary.find((g) => g.term === term);
|
|
4227
|
+
deleteGlossaryEntry(s, term);
|
|
3891
4228
|
persist(s);
|
|
4229
|
+
logChange({ kind: "glossary", summary: `Deleted glossary term "${term}"`, before });
|
|
3892
4230
|
return c.json({ ok: true });
|
|
3893
4231
|
});
|
|
3894
4232
|
app.post("/keys/:key/screenshot", async (c) => {
|
|
@@ -3896,18 +4234,18 @@ function createApi(deps) {
|
|
|
3896
4234
|
const body = await c.req.parseBody();
|
|
3897
4235
|
const file = body["file"];
|
|
3898
4236
|
if (!file || typeof file === "string") return c.json({ error: "no file uploaded" }, 400);
|
|
3899
|
-
const root =
|
|
4237
|
+
const root = dirname2(resolve9(deps.statePath));
|
|
3900
4238
|
const dirName = screenshotDirName(deps.statePath);
|
|
3901
|
-
const dir =
|
|
3902
|
-
mkdirSync6(dir, { recursive: true });
|
|
4239
|
+
const dir = resolve9(root, dirName);
|
|
3903
4240
|
const filename = `${sanitize(key)}__${sanitize(file.name)}`;
|
|
3904
|
-
|
|
4241
|
+
writeFileAtomic(resolve9(dir, filename), Buffer.from(await file.arrayBuffer()));
|
|
3905
4242
|
const path = `${dirName}/${filename}`;
|
|
3906
4243
|
const s = load();
|
|
3907
4244
|
const prev = s.keys[key]?.screenshot;
|
|
3908
4245
|
setMetadata(s, key, { screenshot: path });
|
|
3909
4246
|
if (prev && prev !== path) removeOrphanScreenshot(s, prev);
|
|
3910
4247
|
persist(s);
|
|
4248
|
+
logChange({ kind: "metadata", summary: `${prev ? "Replaced" : "Added"} screenshot on ${key}`, key, before: prev, after: path });
|
|
3911
4249
|
return c.json({ path });
|
|
3912
4250
|
});
|
|
3913
4251
|
app.delete("/keys/:key/screenshot", (c) => {
|
|
@@ -3917,6 +4255,7 @@ function createApi(deps) {
|
|
|
3917
4255
|
setMetadata(s, key, { screenshot: void 0 });
|
|
3918
4256
|
removeOrphanScreenshot(s, shot);
|
|
3919
4257
|
persist(s);
|
|
4258
|
+
logChange({ kind: "metadata", summary: `Removed screenshot from ${key}`, key, before: shot });
|
|
3920
4259
|
return c.json({ ok: true });
|
|
3921
4260
|
});
|
|
3922
4261
|
app.get("/export/preview", (c) => {
|
|
@@ -3961,12 +4300,13 @@ function createApi(deps) {
|
|
|
3961
4300
|
return c.json({ error: e.message }, 400);
|
|
3962
4301
|
}
|
|
3963
4302
|
persist(result.state);
|
|
4303
|
+
logChange({ kind: "import", summary: `Imported ${result.keyCount} key(s) across ${result.localeCount} locale(s)` });
|
|
3964
4304
|
console.log(`[import] ${result.keyCount} key(s) across ${result.localeCount} locale(s)${result.warnings.length ? `, ${result.warnings.length} warning(s)` : ""}`);
|
|
3965
4305
|
return c.json({ keyCount: result.keyCount, localeCount: result.localeCount, warnings: result.warnings });
|
|
3966
4306
|
});
|
|
3967
4307
|
app.post("/export", (c) => {
|
|
3968
4308
|
const s = narrowForExport(load());
|
|
3969
|
-
const root =
|
|
4309
|
+
const root = dirname2(resolve9(deps.statePath));
|
|
3970
4310
|
const warnings = [];
|
|
3971
4311
|
let count = 0;
|
|
3972
4312
|
for (const output of s.config.outputs) {
|
|
@@ -3974,9 +4314,8 @@ function createApi(deps) {
|
|
|
3974
4314
|
const result = adapter.export(s, output);
|
|
3975
4315
|
warnings.push(...result.warnings);
|
|
3976
4316
|
for (const f of result.files) {
|
|
3977
|
-
const abs =
|
|
3978
|
-
|
|
3979
|
-
writeFileSync5(abs, f.contents, "utf8");
|
|
4317
|
+
const abs = resolve9(root, f.path);
|
|
4318
|
+
writeFileAtomic(abs, f.contents);
|
|
3980
4319
|
count++;
|
|
3981
4320
|
}
|
|
3982
4321
|
}
|
|
@@ -3995,41 +4334,62 @@ function createApi(deps) {
|
|
|
3995
4334
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: 0, errors: [] }) });
|
|
3996
4335
|
return;
|
|
3997
4336
|
}
|
|
4337
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
3998
4338
|
let provider;
|
|
3999
4339
|
try {
|
|
4000
|
-
provider = deps.makeProvider ? deps.makeProvider(
|
|
4340
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
4001
4341
|
} catch (e) {
|
|
4002
4342
|
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
4003
4343
|
return;
|
|
4004
4344
|
}
|
|
4005
4345
|
const { skipped } = attachScreenshotsForProvider(reqs, s, projectRoot, provider.supportsVision());
|
|
4006
|
-
if (skipped) console.warn(`Model "${
|
|
4007
|
-
console.log(`[translate] ${reqs.length} string(s) \u2192 ${
|
|
4346
|
+
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4347
|
+
console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
|
|
4008
4348
|
let totalWritten = 0;
|
|
4009
4349
|
const allErrors = [];
|
|
4010
4350
|
const system = buildSystemPrompt();
|
|
4011
4351
|
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
|
-
|
|
4352
|
+
const localeTotals = /* @__PURE__ */ new Map();
|
|
4353
|
+
for (const r of reqs) localeTotals.set(r.targetLocale, (localeTotals.get(r.targetLocale) ?? 0) + 1);
|
|
4354
|
+
const localeDone = /* @__PURE__ */ new Map();
|
|
4355
|
+
await stream.writeSSE({
|
|
4356
|
+
event: "start",
|
|
4357
|
+
data: JSON.stringify({ total: reqs.length, locales: [...localeTotals].map(([locale, total]) => ({ locale, total })) })
|
|
4358
|
+
});
|
|
4359
|
+
await runLocaleParallel(reqs, provider, {
|
|
4360
|
+
// Announce a language the moment a worker picks it up — this is the
|
|
4361
|
+
// signal that "something is happening" during the long first LLM call.
|
|
4362
|
+
onLocaleStart: (locale) => {
|
|
4363
|
+
void stream.writeSSE({ event: "locale-start", data: JSON.stringify({ locale }) });
|
|
4364
|
+
},
|
|
4365
|
+
onBatchComplete: (done, total, batchResults, locale) => {
|
|
4366
|
+
const { written, errors } = applyResults(s, reqs, batchResults);
|
|
4367
|
+
persist(s);
|
|
4368
|
+
totalWritten += written;
|
|
4369
|
+
allErrors.push(...errors);
|
|
4370
|
+
appendLog(projectRoot, {
|
|
4371
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4372
|
+
kind: "translate",
|
|
4373
|
+
summary: `Translated ${batchResults.length} item(s)`,
|
|
4374
|
+
model: aiCfg.model,
|
|
4375
|
+
system,
|
|
4376
|
+
items: batchResults.map((r) => {
|
|
4377
|
+
const req = reqById.get(r.id);
|
|
4378
|
+
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 };
|
|
4379
|
+
}),
|
|
4380
|
+
results: batchResults
|
|
4381
|
+
});
|
|
4382
|
+
const ld = (localeDone.get(locale) ?? 0) + batchResults.length;
|
|
4383
|
+
localeDone.set(locale, ld);
|
|
4384
|
+
console.log(`[translate] ${done}/${total}`);
|
|
4385
|
+
void stream.writeSSE({
|
|
4386
|
+
event: "progress",
|
|
4387
|
+
data: JSON.stringify({ done, total, written: totalWritten, errors, locale, localeDone: ld, localeTotal: localeTotals.get(locale) ?? 0 })
|
|
4388
|
+
});
|
|
4389
|
+
},
|
|
4390
|
+
onLocaleDone: (locale) => {
|
|
4391
|
+
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
4392
|
+
}
|
|
4033
4393
|
}, void 0, signal);
|
|
4034
4394
|
if (!signal?.aborted) {
|
|
4035
4395
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
@@ -4052,19 +4412,22 @@ function createApi(deps) {
|
|
|
4052
4412
|
let written = 0;
|
|
4053
4413
|
let errors = [];
|
|
4054
4414
|
if (toTranslate.length) {
|
|
4415
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
4055
4416
|
let provider;
|
|
4056
4417
|
try {
|
|
4057
|
-
provider = deps.makeProvider ? deps.makeProvider(
|
|
4418
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
4058
4419
|
} catch (e) {
|
|
4059
4420
|
return c.json({ error: e.message }, 400);
|
|
4060
4421
|
}
|
|
4061
4422
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, projectRoot, provider.supportsVision());
|
|
4062
|
-
if (skipped) console.warn(`Model "${
|
|
4423
|
+
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4063
4424
|
const results = await runLocaleParallel(toTranslate, provider);
|
|
4064
4425
|
({ written, errors } = applyResults(s, toTranslate, results, void 0, force));
|
|
4065
4426
|
const entry = {
|
|
4066
4427
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4067
|
-
|
|
4428
|
+
kind: "translate",
|
|
4429
|
+
summary: `Translated ${toTranslate.length} item(s)`,
|
|
4430
|
+
model: aiCfg.model,
|
|
4068
4431
|
system: buildSystemPrompt(),
|
|
4069
4432
|
// Log the screenshot PATH only — never the image bytes.
|
|
4070
4433
|
items: toTranslate.map((r) => ({
|
|
@@ -4078,12 +4441,12 @@ function createApi(deps) {
|
|
|
4078
4441
|
})),
|
|
4079
4442
|
results
|
|
4080
4443
|
};
|
|
4081
|
-
|
|
4444
|
+
appendLog(projectRoot, entry);
|
|
4082
4445
|
}
|
|
4083
4446
|
persist(s);
|
|
4084
4447
|
return c.json({ requested: reqs.length, written, errors });
|
|
4085
4448
|
}));
|
|
4086
|
-
app.get("/
|
|
4449
|
+
app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
|
|
4087
4450
|
app.post("/scan", async (c) => {
|
|
4088
4451
|
const s = load();
|
|
4089
4452
|
const existing = loadUsageCache(projectRoot);
|
|
@@ -4107,7 +4470,7 @@ function createApi(deps) {
|
|
|
4107
4470
|
const refs = [];
|
|
4108
4471
|
const prefixRefs = [];
|
|
4109
4472
|
for (const [file, entry] of Object.entries(cache2.files)) {
|
|
4110
|
-
const abs =
|
|
4473
|
+
const abs = resolve9(projectRoot, file);
|
|
4111
4474
|
for (const r of entry.refs) {
|
|
4112
4475
|
if (r.key === key) refs.push({ file, abs, line: r.line, col: r.col, scanner: r.scanner });
|
|
4113
4476
|
}
|
|
@@ -4148,9 +4511,10 @@ function createApi(deps) {
|
|
|
4148
4511
|
keys: body.keys
|
|
4149
4512
|
}, cache2, body.lastRunAt);
|
|
4150
4513
|
if (!targets.length) return c.json({ requested: 0, written: 0, errors: [] });
|
|
4514
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
4151
4515
|
let provider;
|
|
4152
4516
|
try {
|
|
4153
|
-
provider = deps.makeProvider ? deps.makeProvider(
|
|
4517
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
4154
4518
|
} catch (e) {
|
|
4155
4519
|
return c.json({ error: e.message }, 400);
|
|
4156
4520
|
}
|
|
@@ -4172,10 +4536,11 @@ function createApi(deps) {
|
|
|
4172
4536
|
const raw = await provider.complete({ system, content: [{ type: "text", text: prompt }], schema: CONTEXT_BATCH_SCHEMA });
|
|
4173
4537
|
const batch = raw;
|
|
4174
4538
|
const { written, errors } = applyContext(s, targets, batch.items ?? []);
|
|
4175
|
-
|
|
4539
|
+
appendLog(projectRoot, {
|
|
4176
4540
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4177
4541
|
kind: "context",
|
|
4178
|
-
|
|
4542
|
+
summary: `Generated context for ${targets.length} key(s)`,
|
|
4543
|
+
model: aiCfg.model,
|
|
4179
4544
|
system,
|
|
4180
4545
|
items: targets.map((t) => ({ id: t.id, key: t.key, source: t.source })),
|
|
4181
4546
|
results: (batch.items ?? []).map((r) => ({ id: r.id, value: r.context, error: r.error }))
|
|
@@ -4207,6 +4572,9 @@ var init_api = __esm({
|
|
|
4207
4572
|
init_schema();
|
|
4208
4573
|
init_run2();
|
|
4209
4574
|
init_export_run();
|
|
4575
|
+
init_ui_prefs();
|
|
4576
|
+
init_local_settings();
|
|
4577
|
+
init_atomic_write();
|
|
4210
4578
|
sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
4211
4579
|
screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
4212
4580
|
}
|
|
@@ -4221,7 +4589,7 @@ __export(server_exports, {
|
|
|
4221
4589
|
import { Hono as Hono2 } from "hono";
|
|
4222
4590
|
import { serve } from "@hono/node-server";
|
|
4223
4591
|
import { fileURLToPath } from "url";
|
|
4224
|
-
import { dirname as
|
|
4592
|
+
import { dirname as dirname3, join as join9, resolve as resolve10, extname as extname3, sep as sep2 } from "path";
|
|
4225
4593
|
import { readFile, stat } from "fs/promises";
|
|
4226
4594
|
import { createServer } from "net";
|
|
4227
4595
|
import open from "open";
|
|
@@ -4239,14 +4607,14 @@ async function readFileResponse(absPath) {
|
|
|
4239
4607
|
function buildApp(opts) {
|
|
4240
4608
|
const app = new Hono2();
|
|
4241
4609
|
app.route("/api", createApi({ statePath: opts.statePath, autoExport: true }));
|
|
4242
|
-
const projectRoot =
|
|
4610
|
+
const projectRoot = dirname3(resolve10(opts.statePath));
|
|
4243
4611
|
app.get("/:dir/*", async (c, next) => {
|
|
4244
4612
|
const dirSeg = c.req.param("dir");
|
|
4245
4613
|
if (!dirSeg.endsWith("-screenshots")) return next();
|
|
4246
|
-
const shotsRoot =
|
|
4614
|
+
const shotsRoot = resolve10(projectRoot, dirSeg);
|
|
4247
4615
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4248
4616
|
const rest = pathname.slice(`/${dirSeg}`.length);
|
|
4249
|
-
const target =
|
|
4617
|
+
const target = resolve10(shotsRoot, "." + rest);
|
|
4250
4618
|
const inside = target === shotsRoot || target.startsWith(shotsRoot + sep2);
|
|
4251
4619
|
if (inside) {
|
|
4252
4620
|
const file = await readFileResponse(target);
|
|
@@ -4255,19 +4623,21 @@ function buildApp(opts) {
|
|
|
4255
4623
|
return c.notFound();
|
|
4256
4624
|
});
|
|
4257
4625
|
if (!opts.dev) {
|
|
4258
|
-
const root =
|
|
4626
|
+
const root = resolve10(opts.uiDir ?? DEFAULT_UI_DIR);
|
|
4259
4627
|
app.get("/*", async (c) => {
|
|
4260
4628
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4261
|
-
const target =
|
|
4629
|
+
const target = resolve10(root, "." + pathname);
|
|
4262
4630
|
const inside = target === root || target.startsWith(root + sep2);
|
|
4263
4631
|
if (inside && pathname !== "/") {
|
|
4264
4632
|
const file = await readFileResponse(target);
|
|
4265
4633
|
if (file) return file;
|
|
4266
4634
|
}
|
|
4267
|
-
const index = await readFileResponse(
|
|
4635
|
+
const index = await readFileResponse(join9(root, "index.html"));
|
|
4268
4636
|
if (index) return index;
|
|
4269
4637
|
return c.notFound();
|
|
4270
4638
|
});
|
|
4639
|
+
} else {
|
|
4640
|
+
app.get("/", (c) => c.html(DEV_LANDING_PAGE));
|
|
4271
4641
|
}
|
|
4272
4642
|
return app;
|
|
4273
4643
|
}
|
|
@@ -4299,7 +4669,7 @@ async function startServer(opts) {
|
|
|
4299
4669
|
});
|
|
4300
4670
|
}
|
|
4301
4671
|
function backgroundScan(statePath) {
|
|
4302
|
-
const projectRoot =
|
|
4672
|
+
const projectRoot = dirname3(resolve10(statePath));
|
|
4303
4673
|
Promise.resolve().then(() => {
|
|
4304
4674
|
const state = loadState(statePath);
|
|
4305
4675
|
const existing = loadUsageCache(projectRoot);
|
|
@@ -4311,7 +4681,7 @@ function backgroundScan(statePath) {
|
|
|
4311
4681
|
console.warn("[scan] failed:", err instanceof Error ? err.message : String(err));
|
|
4312
4682
|
});
|
|
4313
4683
|
}
|
|
4314
|
-
var here, DEFAULT_UI_DIR, MIME, DEFAULT_PORT, DEV_PORT;
|
|
4684
|
+
var here, DEFAULT_UI_DIR, MIME, DEFAULT_PORT, DEV_PORT, DEV_UI_URL, DEV_LANDING_PAGE;
|
|
4315
4685
|
var init_server = __esm({
|
|
4316
4686
|
"src/server/server.ts"() {
|
|
4317
4687
|
"use strict";
|
|
@@ -4319,8 +4689,8 @@ var init_server = __esm({
|
|
|
4319
4689
|
init_state();
|
|
4320
4690
|
init_scan();
|
|
4321
4691
|
init_scanner();
|
|
4322
|
-
here =
|
|
4323
|
-
DEFAULT_UI_DIR =
|
|
4692
|
+
here = dirname3(fileURLToPath(import.meta.url));
|
|
4693
|
+
DEFAULT_UI_DIR = join9(here, "..", "ui");
|
|
4324
4694
|
MIME = {
|
|
4325
4695
|
".html": "text/html; charset=utf-8",
|
|
4326
4696
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -4337,6 +4707,15 @@ var init_server = __esm({
|
|
|
4337
4707
|
};
|
|
4338
4708
|
DEFAULT_PORT = 3e3;
|
|
4339
4709
|
DEV_PORT = 8787;
|
|
4710
|
+
DEV_UI_URL = "http://localhost:5173";
|
|
4711
|
+
DEV_LANDING_PAGE = `<!doctype html>
|
|
4712
|
+
<html lang="en"><head><meta charset="utf-8"><title>Glotfile \u2014 dev API</title>
|
|
4713
|
+
<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>
|
|
4714
|
+
</head><body>
|
|
4715
|
+
<h1>Glotfile \u2014 dev API server</h1>
|
|
4716
|
+
<p>This port serves the <strong>API only</strong>. In dev, the app is served by Vite.</p>
|
|
4717
|
+
<p>Open the app \u2192 <a href="${DEV_UI_URL}">${DEV_UI_URL}</a> (the <code>[ui] Local:</code> URL in your terminal).</p>
|
|
4718
|
+
</body></html>`;
|
|
4340
4719
|
}
|
|
4341
4720
|
});
|
|
4342
4721
|
|
|
@@ -4345,14 +4724,15 @@ init_state();
|
|
|
4345
4724
|
init_export_run();
|
|
4346
4725
|
init_storage();
|
|
4347
4726
|
init_ai();
|
|
4727
|
+
init_local_settings();
|
|
4348
4728
|
init_run();
|
|
4349
4729
|
init_provider();
|
|
4350
4730
|
init_log();
|
|
4351
4731
|
init_scan();
|
|
4352
4732
|
init_scanner();
|
|
4353
4733
|
init_context();
|
|
4354
|
-
import { resolve as
|
|
4355
|
-
import { readFileSync as
|
|
4734
|
+
import { resolve as resolve11, dirname as dirname4 } from "path";
|
|
4735
|
+
import { readFileSync as readFileSync15, existsSync as existsSync11 } from "fs";
|
|
4356
4736
|
|
|
4357
4737
|
// src/server/lint/run.ts
|
|
4358
4738
|
init_glob();
|
|
@@ -4654,15 +5034,15 @@ async function runLint(state, options = {}) {
|
|
|
4654
5034
|
|
|
4655
5035
|
// src/server/lint/outputs.ts
|
|
4656
5036
|
init_adapters();
|
|
4657
|
-
import { readFileSync as
|
|
4658
|
-
import { resolve as
|
|
5037
|
+
import { readFileSync as readFileSync10, existsSync as existsSync8 } from "fs";
|
|
5038
|
+
import { resolve as resolve8 } from "path";
|
|
4659
5039
|
function checkOutputs(state, root) {
|
|
4660
5040
|
const out = [];
|
|
4661
5041
|
for (const output of state.config.outputs) {
|
|
4662
5042
|
const result = getAdapter(output.adapter).export(state, output);
|
|
4663
5043
|
for (const file of result.files) {
|
|
4664
|
-
const abs =
|
|
4665
|
-
const current =
|
|
5044
|
+
const abs = resolve8(root, file.path);
|
|
5045
|
+
const current = existsSync8(abs) ? readFileSync10(abs, "utf8") : null;
|
|
4666
5046
|
if (current === null) {
|
|
4667
5047
|
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
4668
5048
|
} else if (current !== file.contents) {
|
|
@@ -4740,7 +5120,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
4740
5120
|
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "build-context", "scan", "prune", "split"];
|
|
4741
5121
|
var isCommand = (s) => s != null && COMMANDS.includes(s);
|
|
4742
5122
|
function parseArgs(argv) {
|
|
4743
|
-
const statePath =
|
|
5123
|
+
const statePath = resolve11(process.cwd(), "glotfile.json");
|
|
4744
5124
|
const first = argv[0];
|
|
4745
5125
|
if (first === "help" || first === "--help" || first === "-h") {
|
|
4746
5126
|
return isCommand(argv[1]) ? { command: argv[1], statePath, help: true } : { command: "help", statePath };
|
|
@@ -4758,7 +5138,7 @@ function parseArgs(argv) {
|
|
|
4758
5138
|
if (flag === "--help" || flag === "-h") args.help = true;
|
|
4759
5139
|
else if (flag === "--dev") args.dev = true;
|
|
4760
5140
|
else if ((flag === "--file" || flag === "-f") && next) {
|
|
4761
|
-
args.statePath =
|
|
5141
|
+
args.statePath = resolve11(process.cwd(), next);
|
|
4762
5142
|
i++;
|
|
4763
5143
|
} else if (flag === "--adapter" && next) {
|
|
4764
5144
|
args.adapter = next;
|
|
@@ -4814,7 +5194,7 @@ function watchTargetFor(statePath) {
|
|
|
4814
5194
|
return detectFormat(statePath) === "split" ? { path: splitDirFor(statePath), recursive: true } : { path: statePath, recursive: false };
|
|
4815
5195
|
}
|
|
4816
5196
|
async function runExport(args) {
|
|
4817
|
-
const root =
|
|
5197
|
+
const root = dirname4(resolve11(args.statePath));
|
|
4818
5198
|
const runOnce = () => {
|
|
4819
5199
|
const state = loadState(args.statePath);
|
|
4820
5200
|
const result = exportToDisk(state, root, args.adapter ? { adapter: args.adapter } : void 0);
|
|
@@ -4850,7 +5230,7 @@ async function runExport(args) {
|
|
|
4850
5230
|
}
|
|
4851
5231
|
async function runTranslate(args) {
|
|
4852
5232
|
const state = loadState(args.statePath);
|
|
4853
|
-
const projectRoot =
|
|
5233
|
+
const projectRoot = dirname4(resolve11(args.statePath));
|
|
4854
5234
|
const reqs = selectRequests(state, {
|
|
4855
5235
|
// Default to translating only empty values; --all forces a full re-translate
|
|
4856
5236
|
// (overwriting existing translations). --only missing stays as a no-op alias.
|
|
@@ -4862,33 +5242,38 @@ async function runTranslate(args) {
|
|
|
4862
5242
|
let written = 0;
|
|
4863
5243
|
let errors = [];
|
|
4864
5244
|
if (toTranslate.length) {
|
|
5245
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
4865
5246
|
let provider;
|
|
4866
5247
|
try {
|
|
4867
|
-
provider = makeProvider(
|
|
5248
|
+
provider = makeProvider(ai);
|
|
4868
5249
|
} catch (e) {
|
|
4869
5250
|
console.error(e.message);
|
|
4870
5251
|
process.exitCode = 1;
|
|
4871
5252
|
return;
|
|
4872
5253
|
}
|
|
4873
5254
|
const { skipped } = attachScreenshotsForProvider(toTranslate, state, projectRoot, provider.supportsVision());
|
|
4874
|
-
if (skipped) console.warn(`Model "${
|
|
5255
|
+
if (skipped) console.warn(`Model "${ai.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4875
5256
|
console.log(`Translating ${toTranslate.length} string(s)\u2026`);
|
|
4876
5257
|
let batchCallbackFired = false;
|
|
4877
|
-
const results = await runLocaleParallel(toTranslate, provider,
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
5258
|
+
const results = await runLocaleParallel(toTranslate, provider, {
|
|
5259
|
+
onBatchComplete: (done, total, batchResults) => {
|
|
5260
|
+
batchCallbackFired = true;
|
|
5261
|
+
const batchApplied = applyResults(state, toTranslate, batchResults);
|
|
5262
|
+
written += batchApplied.written;
|
|
5263
|
+
errors.push(...batchApplied.errors);
|
|
5264
|
+
saveState(args.statePath, state);
|
|
5265
|
+
process.stdout.write(`\r ${done}/${total} translated`);
|
|
5266
|
+
}
|
|
4884
5267
|
});
|
|
4885
5268
|
process.stdout.write("\n");
|
|
4886
5269
|
if (!batchCallbackFired) {
|
|
4887
5270
|
({ written, errors } = applyResults(state, toTranslate, results));
|
|
4888
5271
|
}
|
|
4889
|
-
|
|
5272
|
+
appendLog(projectRoot, {
|
|
4890
5273
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4891
|
-
|
|
5274
|
+
kind: "translate",
|
|
5275
|
+
summary: `Translated ${toTranslate.length} item(s)`,
|
|
5276
|
+
model: ai.model,
|
|
4892
5277
|
system: buildSystemPrompt(),
|
|
4893
5278
|
items: toTranslate.map((r) => ({
|
|
4894
5279
|
id: r.id,
|
|
@@ -4915,7 +5300,7 @@ function printReport(report, format, rawText) {
|
|
|
4915
5300
|
}
|
|
4916
5301
|
async function runLintCmd(args) {
|
|
4917
5302
|
const state = loadState(args.statePath);
|
|
4918
|
-
const rawText =
|
|
5303
|
+
const rawText = existsSync11(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
|
|
4919
5304
|
const report = await runLint(state, { locales: args.locales, ruleIds: args.ruleIds });
|
|
4920
5305
|
printReport(report, args.format, rawText);
|
|
4921
5306
|
const tooManyWarnings = args.maxWarnings != null && report.counts.warn > args.maxWarnings;
|
|
@@ -4935,8 +5320,8 @@ async function runCheck(args) {
|
|
|
4935
5320
|
process.exitCode = 1;
|
|
4936
5321
|
return;
|
|
4937
5322
|
}
|
|
4938
|
-
const rawText =
|
|
4939
|
-
const root =
|
|
5323
|
+
const rawText = existsSync11(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
|
|
5324
|
+
const root = dirname4(resolve11(args.statePath));
|
|
4940
5325
|
const lint = await runLint(state, {});
|
|
4941
5326
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
|
|
4942
5327
|
const counts = countSeverities(findings);
|
|
@@ -4946,9 +5331,9 @@ async function runCheck(args) {
|
|
|
4946
5331
|
}
|
|
4947
5332
|
async function runImportCmd(args) {
|
|
4948
5333
|
const { runImport: runImport2 } = await Promise.resolve().then(() => (init_run2(), run_exports));
|
|
4949
|
-
const projectRoot = args.importSource ?
|
|
4950
|
-
const out =
|
|
4951
|
-
if (
|
|
5334
|
+
const projectRoot = args.importSource ? resolve11(args.importSource) : dirname4(resolve11(args.statePath));
|
|
5335
|
+
const out = resolve11(projectRoot, "glotfile.json");
|
|
5336
|
+
if (existsSync11(out) && !args.importForce) {
|
|
4952
5337
|
console.error(`${out} already exists; pass --force to overwrite`);
|
|
4953
5338
|
process.exitCode = 1;
|
|
4954
5339
|
return;
|
|
@@ -4973,7 +5358,7 @@ async function runImportCmd(args) {
|
|
|
4973
5358
|
}
|
|
4974
5359
|
async function runBuildContext(args) {
|
|
4975
5360
|
const state = loadState(args.statePath);
|
|
4976
|
-
const projectRoot =
|
|
5361
|
+
const projectRoot = dirname4(resolve11(args.statePath));
|
|
4977
5362
|
const cache2 = loadUsageCache(projectRoot);
|
|
4978
5363
|
if (!cache2) {
|
|
4979
5364
|
console.error("No usage index found. Run 'glotfile scan' first.");
|
|
@@ -4992,7 +5377,7 @@ async function runBuildContext(args) {
|
|
|
4992
5377
|
}
|
|
4993
5378
|
let provider;
|
|
4994
5379
|
try {
|
|
4995
|
-
provider = makeProvider(
|
|
5380
|
+
provider = makeProvider(loadLocalSettings(projectRoot).ai);
|
|
4996
5381
|
} catch (e) {
|
|
4997
5382
|
console.error(e.message);
|
|
4998
5383
|
process.exitCode = 1;
|
|
@@ -5022,7 +5407,7 @@ async function runBuildContext(args) {
|
|
|
5022
5407
|
}
|
|
5023
5408
|
async function runScanCmd(args) {
|
|
5024
5409
|
const state = loadState(args.statePath);
|
|
5025
|
-
const projectRoot =
|
|
5410
|
+
const projectRoot = dirname4(resolve11(args.statePath));
|
|
5026
5411
|
const existing = loadUsageCache(projectRoot);
|
|
5027
5412
|
const result = runScan(projectRoot, state.config.scan ?? {}, existing);
|
|
5028
5413
|
const fileCount2 = Object.keys(result.files).length;
|
|
@@ -5041,7 +5426,7 @@ async function runPrune(args) {
|
|
|
5041
5426
|
for (const k of findEmptySourceKeys(state)) toRemove.add(k);
|
|
5042
5427
|
}
|
|
5043
5428
|
if (args.unused) {
|
|
5044
|
-
const projectRoot =
|
|
5429
|
+
const projectRoot = dirname4(resolve11(args.statePath));
|
|
5045
5430
|
const cache2 = runScan(projectRoot, state.config.scan ?? {}, loadUsageCache(projectRoot));
|
|
5046
5431
|
const used = new Set(computeUsedKeys(state, cache2));
|
|
5047
5432
|
for (const k of Object.keys(state.keys)) {
|
|
@@ -5208,9 +5593,10 @@ async function main(argv) {
|
|
|
5208
5593
|
if (args.command === "split") return runSplit(args);
|
|
5209
5594
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
5210
5595
|
const { url } = await startServer2({ statePath: args.statePath, dev: args.dev });
|
|
5211
|
-
console.log(`Glotfile
|
|
5596
|
+
if (args.dev) console.log(`Glotfile dev API on ${url} \u2014 open the UI at the Vite "Local:" URL above`);
|
|
5597
|
+
else console.log(`Glotfile running at ${url}`);
|
|
5212
5598
|
}
|
|
5213
|
-
if (
|
|
5599
|
+
if (resolve11(process.argv[1] ?? "") === resolve11(fileURLToPath2(import.meta.url))) {
|
|
5214
5600
|
main(process.argv.slice(2)).catch((err) => {
|
|
5215
5601
|
console.error(err instanceof Error ? err.message : String(err));
|
|
5216
5602
|
process.exit(1);
|