glotfile 1.0.1 → 1.1.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/dist/server/cli.js +1250 -254
- package/dist/server/server.js +1032 -309
- package/dist/ui/assets/index-Bjwiz6KQ.css +1 -0
- package/dist/ui/assets/{index-5Imdw0oX.js → index-CrceYqHe.js} +79 -12
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BaHu118N.css +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -58,6 +58,14 @@ var init_atomic_write = __esm({
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
// src/server/lint/registry.ts
|
|
61
|
+
function unknownRuleIds(ids) {
|
|
62
|
+
const valid = new Set(RULE_IDS);
|
|
63
|
+
return ids.filter((id) => !valid.has(id));
|
|
64
|
+
}
|
|
65
|
+
function suggestRuleId(unknown) {
|
|
66
|
+
const lower = unknown.toLowerCase();
|
|
67
|
+
return RULE_IDS.find((id) => id.includes(lower) || lower.includes(id));
|
|
68
|
+
}
|
|
61
69
|
var RULE_IDS, DEFAULT_SEVERITY;
|
|
62
70
|
var init_registry = __esm({
|
|
63
71
|
"src/server/lint/registry.ts"() {
|
|
@@ -271,7 +279,8 @@ function validate(raw) {
|
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
if (raw.glossary !== void 0 && !Array.isArray(raw.glossary)) fail("glossary must be an array");
|
|
274
|
-
|
|
282
|
+
if (raw.glossarySuggestions !== void 0 && !Array.isArray(raw.glossarySuggestions)) fail("glossarySuggestions must be an array");
|
|
283
|
+
const state = { glossary: [], glossarySuggestions: [], ...raw };
|
|
275
284
|
return state;
|
|
276
285
|
}
|
|
277
286
|
function defaultState() {
|
|
@@ -290,6 +299,7 @@ function defaultState() {
|
|
|
290
299
|
autoExport: true
|
|
291
300
|
},
|
|
292
301
|
glossary: [],
|
|
302
|
+
glossarySuggestions: [],
|
|
293
303
|
keys: {}
|
|
294
304
|
};
|
|
295
305
|
}
|
|
@@ -807,6 +817,39 @@ function upsertGlossaryEntry(state, entry) {
|
|
|
807
817
|
function deleteGlossaryEntry(state, term) {
|
|
808
818
|
state.glossary = state.glossary.filter((e) => e.term !== term);
|
|
809
819
|
}
|
|
820
|
+
function normGlossaryTerm(term) {
|
|
821
|
+
return term.trim().toLowerCase();
|
|
822
|
+
}
|
|
823
|
+
function mergeGlossarySuggestions(state, found) {
|
|
824
|
+
const known = /* @__PURE__ */ new Set();
|
|
825
|
+
for (const g of state.glossary) known.add(normGlossaryTerm(g.term));
|
|
826
|
+
for (const s of state.glossarySuggestions) known.add(normGlossaryTerm(s.term));
|
|
827
|
+
const added = [];
|
|
828
|
+
for (const f of found) {
|
|
829
|
+
const term = f.term.trim();
|
|
830
|
+
if (!term) continue;
|
|
831
|
+
const key = normGlossaryTerm(term);
|
|
832
|
+
if (known.has(key)) continue;
|
|
833
|
+
known.add(key);
|
|
834
|
+
const sug = { term, status: "pending" };
|
|
835
|
+
if (f.note?.trim()) sug.note = f.note.trim();
|
|
836
|
+
if (f.doNotTranslate) sug.doNotTranslate = true;
|
|
837
|
+
if (f.caseSensitive) sug.caseSensitive = true;
|
|
838
|
+
if (f.wholeWord === false) sug.wholeWord = false;
|
|
839
|
+
state.glossarySuggestions.push(sug);
|
|
840
|
+
added.push(sug);
|
|
841
|
+
}
|
|
842
|
+
return added;
|
|
843
|
+
}
|
|
844
|
+
function dismissGlossarySuggestion(state, term) {
|
|
845
|
+
const key = normGlossaryTerm(term);
|
|
846
|
+
const s = state.glossarySuggestions.find((x) => normGlossaryTerm(x.term) === key);
|
|
847
|
+
if (s) s.status = "dismissed";
|
|
848
|
+
}
|
|
849
|
+
function removeGlossarySuggestion(state, term) {
|
|
850
|
+
const key = normGlossaryTerm(term);
|
|
851
|
+
state.glossarySuggestions = state.glossarySuggestions.filter((x) => normGlossaryTerm(x.term) !== key);
|
|
852
|
+
}
|
|
810
853
|
function addCustomWord(state, word) {
|
|
811
854
|
const w = word.trim();
|
|
812
855
|
if (!w) return;
|
|
@@ -2528,6 +2571,54 @@ var init_batch = __esm({
|
|
|
2528
2571
|
}
|
|
2529
2572
|
});
|
|
2530
2573
|
|
|
2574
|
+
// src/server/ai/price-cache.ts
|
|
2575
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2576
|
+
import { homedir } from "os";
|
|
2577
|
+
import { join as join3 } from "path";
|
|
2578
|
+
function isModelPrice(v) {
|
|
2579
|
+
if (!v || typeof v !== "object") return false;
|
|
2580
|
+
const p = v;
|
|
2581
|
+
return typeof p.inputPerMTok === "number" && typeof p.outputPerMTok === "number";
|
|
2582
|
+
}
|
|
2583
|
+
function loadPriceCache(path = defaultPriceCachePath()) {
|
|
2584
|
+
let parsed;
|
|
2585
|
+
try {
|
|
2586
|
+
parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
2587
|
+
} catch {
|
|
2588
|
+
return null;
|
|
2589
|
+
}
|
|
2590
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
2591
|
+
const raw = parsed;
|
|
2592
|
+
if (!raw.models || typeof raw.models !== "object") return null;
|
|
2593
|
+
const models = {};
|
|
2594
|
+
for (const [id, price] of Object.entries(raw.models)) {
|
|
2595
|
+
if (isModelPrice(price)) models[id] = price;
|
|
2596
|
+
}
|
|
2597
|
+
return {
|
|
2598
|
+
source: typeof raw.source === "string" ? raw.source : "unknown",
|
|
2599
|
+
fetchedAt: typeof raw.fetchedAt === "string" ? raw.fetchedAt : "",
|
|
2600
|
+
models
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
function savePriceCache(cache2, path = defaultPriceCachePath()) {
|
|
2604
|
+
writeFileAtomic(path, JSON.stringify(cache2, null, 2) + "\n");
|
|
2605
|
+
}
|
|
2606
|
+
function getPriceCache() {
|
|
2607
|
+
if (memo === void 0) memo = loadPriceCache();
|
|
2608
|
+
return memo;
|
|
2609
|
+
}
|
|
2610
|
+
function invalidatePriceCache() {
|
|
2611
|
+
memo = void 0;
|
|
2612
|
+
}
|
|
2613
|
+
var defaultPriceCachePath, memo;
|
|
2614
|
+
var init_price_cache = __esm({
|
|
2615
|
+
"src/server/ai/price-cache.ts"() {
|
|
2616
|
+
"use strict";
|
|
2617
|
+
init_atomic_write();
|
|
2618
|
+
defaultPriceCachePath = () => process.env.GLOTFILE_PRICES_PATH || join3(homedir(), ".glotfile", "model-prices.json");
|
|
2619
|
+
}
|
|
2620
|
+
});
|
|
2621
|
+
|
|
2531
2622
|
// src/server/ai/pricing.ts
|
|
2532
2623
|
function addUsage(into, add) {
|
|
2533
2624
|
into.inputTokens += add.inputTokens;
|
|
@@ -2541,7 +2632,9 @@ function usageCostUsd(usage, ai, multiplier = 1) {
|
|
|
2541
2632
|
return pricing ? estimateUsageCostUsd(usage, pricing, multiplier) : void 0;
|
|
2542
2633
|
}
|
|
2543
2634
|
function estimateUsageCostUsd(usage, pricing, multiplier = 1) {
|
|
2544
|
-
const
|
|
2635
|
+
const writeRate = pricing.cacheWritePerMTok ?? pricing.inputPerMTok * CACHE_WRITE_MULTIPLIER;
|
|
2636
|
+
const readRate = pricing.cacheReadPerMTok ?? pricing.inputPerMTok * CACHE_READ_MULTIPLIER;
|
|
2637
|
+
const inputCost = usage.inputTokens * pricing.inputPerMTok + usage.cacheCreationInputTokens * writeRate + usage.cacheReadInputTokens * readRate;
|
|
2545
2638
|
return (inputCost + usage.outputTokens * pricing.outputPerMTok) / 1e6 * multiplier;
|
|
2546
2639
|
}
|
|
2547
2640
|
function bareModelId(model) {
|
|
@@ -2552,12 +2645,27 @@ function bareModelId(model) {
|
|
|
2552
2645
|
if (anth !== -1) id = id.slice(anth + "anthropic.".length);
|
|
2553
2646
|
return id;
|
|
2554
2647
|
}
|
|
2555
|
-
function
|
|
2648
|
+
function lookupCachePrice(cache2, id) {
|
|
2649
|
+
const exact = cache2.models[id];
|
|
2650
|
+
if (exact) return { source: "cache", ...exact };
|
|
2651
|
+
let best;
|
|
2652
|
+
for (const [cid, price] of Object.entries(cache2.models)) {
|
|
2653
|
+
if (id.startsWith(cid) && (!best || cid.length > best.id.length)) {
|
|
2654
|
+
best = { id: cid, price: { source: "cache", ...price } };
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
return best ? best.price : null;
|
|
2658
|
+
}
|
|
2659
|
+
function resolvePricing(ai, cache2 = getPriceCache()) {
|
|
2556
2660
|
if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
|
|
2557
2661
|
return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
|
|
2558
2662
|
}
|
|
2559
2663
|
if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
|
|
2560
2664
|
const id = bareModelId(ai.model);
|
|
2665
|
+
if (cache2) {
|
|
2666
|
+
const cached = lookupCachePrice(cache2, id);
|
|
2667
|
+
if (cached) return cached;
|
|
2668
|
+
}
|
|
2561
2669
|
let best;
|
|
2562
2670
|
for (const row of PRICE_TABLE) {
|
|
2563
2671
|
if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
|
|
@@ -2568,6 +2676,7 @@ var BATCH_PRICE_MULTIPLIER, CACHE_WRITE_MULTIPLIER, CACHE_READ_MULTIPLIER, PRICE
|
|
|
2568
2676
|
var init_pricing = __esm({
|
|
2569
2677
|
"src/server/ai/pricing.ts"() {
|
|
2570
2678
|
"use strict";
|
|
2679
|
+
init_price_cache();
|
|
2571
2680
|
BATCH_PRICE_MULTIPLIER = 0.5;
|
|
2572
2681
|
CACHE_WRITE_MULTIPLIER = 1.25;
|
|
2573
2682
|
CACHE_READ_MULTIPLIER = 0.1;
|
|
@@ -3216,11 +3325,11 @@ var init_glotfile_dir = __esm({
|
|
|
3216
3325
|
});
|
|
3217
3326
|
|
|
3218
3327
|
// src/server/local-settings.ts
|
|
3219
|
-
import { readFileSync as
|
|
3328
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3220
3329
|
import { resolve as resolve3 } from "path";
|
|
3221
3330
|
function readJson(path) {
|
|
3222
3331
|
try {
|
|
3223
|
-
const parsed = JSON.parse(
|
|
3332
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
3224
3333
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3225
3334
|
} catch {
|
|
3226
3335
|
return {};
|
|
@@ -3307,13 +3416,23 @@ var init_local_settings = __esm({
|
|
|
3307
3416
|
});
|
|
3308
3417
|
|
|
3309
3418
|
// src/server/glossary.ts
|
|
3310
|
-
function
|
|
3311
|
-
return
|
|
3419
|
+
function escapeRegExp2(s) {
|
|
3420
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3421
|
+
}
|
|
3422
|
+
function contains(haystack, needle, caseSensitive, wholeWord) {
|
|
3423
|
+
if (!wholeWord) {
|
|
3424
|
+
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
3425
|
+
}
|
|
3426
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp2(needle)}(?![\\p{L}\\p{N}])`, caseSensitive ? "u" : "iu");
|
|
3427
|
+
return re.test(haystack);
|
|
3428
|
+
}
|
|
3429
|
+
function termInSource(source, entry) {
|
|
3430
|
+
return contains(source, entry.term, entry.caseSensitive, entry.wholeWord ?? true);
|
|
3312
3431
|
}
|
|
3313
3432
|
function relevantGlossary(source, targetLocale, glossary) {
|
|
3314
3433
|
const hints = [];
|
|
3315
3434
|
for (const entry of glossary) {
|
|
3316
|
-
if (!
|
|
3435
|
+
if (!termInSource(source, entry)) continue;
|
|
3317
3436
|
hints.push({
|
|
3318
3437
|
term: entry.term,
|
|
3319
3438
|
doNotTranslate: entry.doNotTranslate,
|
|
@@ -3323,10 +3442,20 @@ function relevantGlossary(source, targetLocale, glossary) {
|
|
|
3323
3442
|
}
|
|
3324
3443
|
return hints;
|
|
3325
3444
|
}
|
|
3445
|
+
function sourceKeysForTerm(state, term, opts = {}) {
|
|
3446
|
+
const pseudo = { term, caseSensitive: opts.caseSensitive, wholeWord: opts.wholeWord };
|
|
3447
|
+
const out = [];
|
|
3448
|
+
for (const [key, entry] of Object.entries(state.keys)) {
|
|
3449
|
+
const lv = entry.values[state.config.sourceLocale];
|
|
3450
|
+
const text = lv?.value ?? lv?.forms?.other ?? "";
|
|
3451
|
+
if (text && termInSource(text, pseudo)) out.push(key);
|
|
3452
|
+
}
|
|
3453
|
+
return out;
|
|
3454
|
+
}
|
|
3326
3455
|
function glossaryViolations(source, value, targetLocale, glossary) {
|
|
3327
3456
|
const out = [];
|
|
3328
3457
|
for (const entry of glossary) {
|
|
3329
|
-
if (!
|
|
3458
|
+
if (!termInSource(source, entry)) continue;
|
|
3330
3459
|
if (entry.doNotTranslate) {
|
|
3331
3460
|
if (!contains(value, entry.term, entry.caseSensitive)) {
|
|
3332
3461
|
out.push({ term: entry.term, expected: entry.term, kind: "do-not-translate" });
|
|
@@ -3347,7 +3476,7 @@ var init_glossary = __esm({
|
|
|
3347
3476
|
});
|
|
3348
3477
|
|
|
3349
3478
|
// src/server/ai/run.ts
|
|
3350
|
-
import { readFileSync as
|
|
3479
|
+
import { readFileSync as readFileSync6, existsSync as existsSync5 } from "fs";
|
|
3351
3480
|
import { resolve as resolve4, extname } from "path";
|
|
3352
3481
|
function selectRequests(state, opts) {
|
|
3353
3482
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
@@ -3421,7 +3550,7 @@ function attachScreenshots(reqs, state, projectRoot) {
|
|
|
3421
3550
|
if (!existsSync5(abs)) {
|
|
3422
3551
|
cache2.set(screenshot, null);
|
|
3423
3552
|
} else {
|
|
3424
|
-
const buf =
|
|
3553
|
+
const buf = readFileSync6(abs);
|
|
3425
3554
|
cache2.set(screenshot, buf.length > MAX_IMAGE_BYTES ? null : { mediaType, base64: buf.toString("base64") });
|
|
3426
3555
|
}
|
|
3427
3556
|
}
|
|
@@ -3567,16 +3696,16 @@ var init_run = __esm({
|
|
|
3567
3696
|
});
|
|
3568
3697
|
|
|
3569
3698
|
// src/server/ai/pending-batch.ts
|
|
3570
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as
|
|
3571
|
-
import { join as
|
|
3699
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, rmSync as rmSync4 } from "fs";
|
|
3700
|
+
import { join as join4 } from "path";
|
|
3572
3701
|
function pendingBatchPath(projectRoot) {
|
|
3573
|
-
return
|
|
3702
|
+
return join4(projectRoot, ".glotfile", "batch.json");
|
|
3574
3703
|
}
|
|
3575
3704
|
function loadPendingBatch(projectRoot) {
|
|
3576
3705
|
const path = pendingBatchPath(projectRoot);
|
|
3577
3706
|
if (!existsSync6(path)) return void 0;
|
|
3578
3707
|
try {
|
|
3579
|
-
const parsed = JSON.parse(
|
|
3708
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
3580
3709
|
if (parsed?.version !== 1) return void 0;
|
|
3581
3710
|
return parsed;
|
|
3582
3711
|
} catch {
|
|
@@ -3584,9 +3713,9 @@ function loadPendingBatch(projectRoot) {
|
|
|
3584
3713
|
}
|
|
3585
3714
|
}
|
|
3586
3715
|
function savePendingBatch(projectRoot, pending) {
|
|
3587
|
-
const dir =
|
|
3716
|
+
const dir = join4(projectRoot, ".glotfile");
|
|
3588
3717
|
mkdirSync4(dir, { recursive: true });
|
|
3589
|
-
const gitignore =
|
|
3718
|
+
const gitignore = join4(dir, ".gitignore");
|
|
3590
3719
|
if (!existsSync6(gitignore)) writeFileSync3(gitignore, "*\n");
|
|
3591
3720
|
writeFileSync3(pendingBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
3592
3721
|
}
|
|
@@ -3600,7 +3729,7 @@ var init_pending_batch = __esm({
|
|
|
3600
3729
|
});
|
|
3601
3730
|
|
|
3602
3731
|
// src/server/log.ts
|
|
3603
|
-
import { appendFileSync, existsSync as existsSync7, openSync, fstatSync, readSync, closeSync, statSync as statSync2, readFileSync as
|
|
3732
|
+
import { appendFileSync, existsSync as existsSync7, openSync, fstatSync, readSync, closeSync, statSync as statSync2, readFileSync as readFileSync8 } from "fs";
|
|
3604
3733
|
import { resolve as resolve5 } from "path";
|
|
3605
3734
|
function logPath(projectRoot) {
|
|
3606
3735
|
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
@@ -3613,7 +3742,7 @@ function appendLog(projectRoot, entry) {
|
|
|
3613
3742
|
}
|
|
3614
3743
|
function trimLog(path, maxBytes = MAX_LOG_BYTES, targetBytes = TRIM_LOG_TO_BYTES) {
|
|
3615
3744
|
if (!existsSync7(path) || statSync2(path).size <= maxBytes) return;
|
|
3616
|
-
const lines =
|
|
3745
|
+
const lines = readFileSync8(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
3617
3746
|
const kept = [];
|
|
3618
3747
|
let bytes = 0;
|
|
3619
3748
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -3812,7 +3941,7 @@ var init_batch_run = __esm({
|
|
|
3812
3941
|
});
|
|
3813
3942
|
|
|
3814
3943
|
// src/server/ai/context.ts
|
|
3815
|
-
import { existsSync as existsSync8, readFileSync as
|
|
3944
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
|
|
3816
3945
|
import { resolve as resolve6 } from "path";
|
|
3817
3946
|
function globToRegExp2(glob) {
|
|
3818
3947
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
@@ -3828,7 +3957,7 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
3828
3957
|
const absPath = resolve6(projectRoot, ref.file);
|
|
3829
3958
|
if (!fileCache.has(ref.file)) {
|
|
3830
3959
|
if (!existsSync8(absPath)) continue;
|
|
3831
|
-
const content =
|
|
3960
|
+
const content = readFileSync9(absPath, "utf8");
|
|
3832
3961
|
fileCache.set(ref.file, content.split("\n"));
|
|
3833
3962
|
}
|
|
3834
3963
|
const lines = fileCache.get(ref.file);
|
|
@@ -3844,6 +3973,21 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
3844
3973
|
}
|
|
3845
3974
|
return snippets;
|
|
3846
3975
|
}
|
|
3976
|
+
function attachUsageSnippets(targets, cache2, projectRoot) {
|
|
3977
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
3978
|
+
for (const target of targets) {
|
|
3979
|
+
const allRefs = Object.entries(cache2.files).flatMap(
|
|
3980
|
+
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
3981
|
+
key: r.key,
|
|
3982
|
+
file,
|
|
3983
|
+
line: r.line,
|
|
3984
|
+
col: r.col,
|
|
3985
|
+
scanner: r.scanner
|
|
3986
|
+
}))
|
|
3987
|
+
);
|
|
3988
|
+
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3847
3991
|
function buildUsageIndex(cache2) {
|
|
3848
3992
|
const index = /* @__PURE__ */ new Map();
|
|
3849
3993
|
for (const [file, entry] of Object.entries(cache2.files)) {
|
|
@@ -3982,16 +4126,16 @@ var init_context = __esm({
|
|
|
3982
4126
|
});
|
|
3983
4127
|
|
|
3984
4128
|
// src/server/ai/pending-context-batch.ts
|
|
3985
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as
|
|
3986
|
-
import { join as
|
|
4129
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync4, rmSync as rmSync5 } from "fs";
|
|
4130
|
+
import { join as join5 } from "path";
|
|
3987
4131
|
function pendingContextBatchPath(projectRoot) {
|
|
3988
|
-
return
|
|
4132
|
+
return join5(projectRoot, ".glotfile", "context-batch.json");
|
|
3989
4133
|
}
|
|
3990
4134
|
function loadPendingContextBatch(projectRoot) {
|
|
3991
4135
|
const path = pendingContextBatchPath(projectRoot);
|
|
3992
4136
|
if (!existsSync9(path)) return void 0;
|
|
3993
4137
|
try {
|
|
3994
|
-
const parsed = JSON.parse(
|
|
4138
|
+
const parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
3995
4139
|
if (parsed?.version !== 1) return void 0;
|
|
3996
4140
|
return parsed;
|
|
3997
4141
|
} catch {
|
|
@@ -3999,9 +4143,9 @@ function loadPendingContextBatch(projectRoot) {
|
|
|
3999
4143
|
}
|
|
4000
4144
|
}
|
|
4001
4145
|
function savePendingContextBatch(projectRoot, pending) {
|
|
4002
|
-
const dir =
|
|
4146
|
+
const dir = join5(projectRoot, ".glotfile");
|
|
4003
4147
|
mkdirSync5(dir, { recursive: true });
|
|
4004
|
-
const gitignore =
|
|
4148
|
+
const gitignore = join5(dir, ".gitignore");
|
|
4005
4149
|
if (!existsSync9(gitignore)) writeFileSync4(gitignore, "*\n");
|
|
4006
4150
|
writeFileSync4(pendingContextBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
4007
4151
|
}
|
|
@@ -4122,6 +4266,245 @@ var init_context_batch_run = __esm({
|
|
|
4122
4266
|
}
|
|
4123
4267
|
});
|
|
4124
4268
|
|
|
4269
|
+
// src/server/ai/glossary-suggest.ts
|
|
4270
|
+
function globToRegExp3(glob) {
|
|
4271
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
4272
|
+
return new RegExp(`^${escaped}$`);
|
|
4273
|
+
}
|
|
4274
|
+
function selectGlossarySources(state, opts) {
|
|
4275
|
+
const keyRe = opts.keyGlob ? globToRegExp3(opts.keyGlob) : null;
|
|
4276
|
+
let rows = [];
|
|
4277
|
+
for (const key of Object.keys(state.keys)) {
|
|
4278
|
+
if (keyRe && !keyRe.test(key)) continue;
|
|
4279
|
+
const entry = state.keys[key];
|
|
4280
|
+
if (opts.since) {
|
|
4281
|
+
if (!entry.createdAt || entry.createdAt < opts.since) continue;
|
|
4282
|
+
}
|
|
4283
|
+
const lv = entry.values[state.config.sourceLocale];
|
|
4284
|
+
const source = (lv?.value ?? lv?.forms?.other ?? "").trim();
|
|
4285
|
+
if (!source) continue;
|
|
4286
|
+
rows.push({ key, source });
|
|
4287
|
+
}
|
|
4288
|
+
rows.sort((a, b) => {
|
|
4289
|
+
const ta = state.keys[a.key].createdAt ?? "";
|
|
4290
|
+
const tb = state.keys[b.key].createdAt ?? "";
|
|
4291
|
+
return tb.localeCompare(ta) || a.key.localeCompare(b.key);
|
|
4292
|
+
});
|
|
4293
|
+
if (opts.limit !== void 0) rows = rows.slice(0, opts.limit);
|
|
4294
|
+
return rows;
|
|
4295
|
+
}
|
|
4296
|
+
function knownTermList(state) {
|
|
4297
|
+
const out = /* @__PURE__ */ new Set();
|
|
4298
|
+
for (const g of state.glossary) out.add(g.term);
|
|
4299
|
+
for (const s of state.glossarySuggestions) out.add(s.term);
|
|
4300
|
+
return [...out];
|
|
4301
|
+
}
|
|
4302
|
+
function buildGlossarySuggestSystemPrompt() {
|
|
4303
|
+
return [
|
|
4304
|
+
"You identify GLOSSARY-CANDIDATE terms in a UI string catalog so they translate consistently.",
|
|
4305
|
+
"A glossary term is a brand or product name, a feature or module name, an acronym, a piece of domain/industry jargon, or any noun phrase that should translate the SAME way everywhere (or stay verbatim).",
|
|
4306
|
+
"You are given source strings (the app's original language). Return the candidate terms you find.",
|
|
4307
|
+
"Rules:",
|
|
4308
|
+
"- Only surface terms a translator would benefit from pinning. IGNORE ordinary words, verbs, and generic UI labels (e.g. 'Save', 'Cancel', 'Welcome').",
|
|
4309
|
+
"- Prefer terms that recur or are clearly proper nouns / product names / acronyms.",
|
|
4310
|
+
"- Set doNotTranslate: true for brand/product names, code identifiers, and acronyms that must stay verbatim in every language.",
|
|
4311
|
+
"- Set caseSensitive: true only when casing is meaningful (e.g. an all-caps acronym that must not match a lowercase common word).",
|
|
4312
|
+
"- Set wholeWord: false ONLY if the term should also match inside larger words; otherwise omit it (whole-word is the default).",
|
|
4313
|
+
"- note: one short phrase on why it's a term (e.g. 'product name', 'industry acronym', 'recurring UI concept'). Keep it under 80 characters.",
|
|
4314
|
+
"- Do NOT return any term in the provided 'Already known' list.",
|
|
4315
|
+
"- Return the term exactly as it appears in the source (preserve casing)."
|
|
4316
|
+
].join("\n");
|
|
4317
|
+
}
|
|
4318
|
+
function buildGlossarySuggestBatchPrompt(sources, knownTerms) {
|
|
4319
|
+
const known = knownTerms.length ? knownTerms.join(", ") : "(none yet)";
|
|
4320
|
+
const lines = sources.map((s) => `- [${s.key}] ${s.source}`).join("\n");
|
|
4321
|
+
return [
|
|
4322
|
+
`Already known (do NOT return these): ${known}`,
|
|
4323
|
+
"",
|
|
4324
|
+
"Source strings:",
|
|
4325
|
+
lines,
|
|
4326
|
+
"",
|
|
4327
|
+
'Return JSON {"terms":[{"term","note?","doNotTranslate?","caseSensitive?","wholeWord?"}]}. Return an empty array if you find no good candidates.'
|
|
4328
|
+
].join("\n");
|
|
4329
|
+
}
|
|
4330
|
+
function dedupeTerms(terms) {
|
|
4331
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4332
|
+
const out = [];
|
|
4333
|
+
for (const t of terms) {
|
|
4334
|
+
const term = t.term?.trim();
|
|
4335
|
+
if (!term) continue;
|
|
4336
|
+
const key = term.toLowerCase();
|
|
4337
|
+
if (seen.has(key)) continue;
|
|
4338
|
+
seen.add(key);
|
|
4339
|
+
out.push({ ...t, term });
|
|
4340
|
+
}
|
|
4341
|
+
return out;
|
|
4342
|
+
}
|
|
4343
|
+
var GLOSSARY_SUGGEST_SCHEMA;
|
|
4344
|
+
var init_glossary_suggest = __esm({
|
|
4345
|
+
"src/server/ai/glossary-suggest.ts"() {
|
|
4346
|
+
"use strict";
|
|
4347
|
+
GLOSSARY_SUGGEST_SCHEMA = {
|
|
4348
|
+
type: "object",
|
|
4349
|
+
properties: {
|
|
4350
|
+
terms: {
|
|
4351
|
+
type: "array",
|
|
4352
|
+
items: {
|
|
4353
|
+
type: "object",
|
|
4354
|
+
properties: {
|
|
4355
|
+
term: { type: "string" },
|
|
4356
|
+
note: { type: "string" },
|
|
4357
|
+
doNotTranslate: { type: "boolean" },
|
|
4358
|
+
caseSensitive: { type: "boolean" },
|
|
4359
|
+
wholeWord: { type: "boolean" }
|
|
4360
|
+
},
|
|
4361
|
+
required: ["term"],
|
|
4362
|
+
additionalProperties: false
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
},
|
|
4366
|
+
required: ["terms"],
|
|
4367
|
+
additionalProperties: false
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4370
|
+
});
|
|
4371
|
+
|
|
4372
|
+
// src/server/ai/pending-glossary-batch.ts
|
|
4373
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync5, rmSync as rmSync6 } from "fs";
|
|
4374
|
+
import { join as join6 } from "path";
|
|
4375
|
+
function pendingGlossaryBatchPath(projectRoot) {
|
|
4376
|
+
return join6(projectRoot, ".glotfile", "glossary-suggest-batch.json");
|
|
4377
|
+
}
|
|
4378
|
+
function loadPendingGlossaryBatch(projectRoot) {
|
|
4379
|
+
const path = pendingGlossaryBatchPath(projectRoot);
|
|
4380
|
+
if (!existsSync10(path)) return void 0;
|
|
4381
|
+
try {
|
|
4382
|
+
const parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
4383
|
+
if (parsed?.version !== 1) return void 0;
|
|
4384
|
+
return parsed;
|
|
4385
|
+
} catch {
|
|
4386
|
+
return void 0;
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
function savePendingGlossaryBatch(projectRoot, pending) {
|
|
4390
|
+
const dir = join6(projectRoot, ".glotfile");
|
|
4391
|
+
mkdirSync6(dir, { recursive: true });
|
|
4392
|
+
const gitignore = join6(dir, ".gitignore");
|
|
4393
|
+
if (!existsSync10(gitignore)) writeFileSync5(gitignore, "*\n");
|
|
4394
|
+
writeFileSync5(pendingGlossaryBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
4395
|
+
}
|
|
4396
|
+
function clearPendingGlossaryBatch(projectRoot) {
|
|
4397
|
+
rmSync6(pendingGlossaryBatchPath(projectRoot), { force: true });
|
|
4398
|
+
}
|
|
4399
|
+
var init_pending_glossary_batch = __esm({
|
|
4400
|
+
"src/server/ai/pending-glossary-batch.ts"() {
|
|
4401
|
+
"use strict";
|
|
4402
|
+
}
|
|
4403
|
+
});
|
|
4404
|
+
|
|
4405
|
+
// src/server/ai/glossary-batch-run.ts
|
|
4406
|
+
function completionRequestFor2(chunk2, knownTerms) {
|
|
4407
|
+
return {
|
|
4408
|
+
system: buildGlossarySuggestSystemPrompt(),
|
|
4409
|
+
content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunk2, knownTerms) }],
|
|
4410
|
+
schema: GLOSSARY_SUGGEST_SCHEMA
|
|
4411
|
+
};
|
|
4412
|
+
}
|
|
4413
|
+
async function submitGlossarySuggestBatch(provider, sources, knownTerms, batchSize, model, projectRoot) {
|
|
4414
|
+
if (loadPendingGlossaryBatch(projectRoot)) {
|
|
4415
|
+
throw new Error("A glossary suggestion batch is already pending. Apply or cancel it first.");
|
|
4416
|
+
}
|
|
4417
|
+
const chunks = [];
|
|
4418
|
+
const size = Math.max(1, batchSize);
|
|
4419
|
+
for (let i = 0; i < sources.length; i += size) chunks.push(sources.slice(i, i + size));
|
|
4420
|
+
const jobs = chunks.map((chunk2, i) => ({ customId: `gloss_${i}`, chunk: chunk2 }));
|
|
4421
|
+
const batchId = await provider.submitCompletionBatch(
|
|
4422
|
+
jobs.map((j) => ({ customId: j.customId, request: completionRequestFor2(j.chunk, knownTerms) }))
|
|
4423
|
+
);
|
|
4424
|
+
const pending = {
|
|
4425
|
+
version: 1,
|
|
4426
|
+
// Only Anthropic implements completion batches today.
|
|
4427
|
+
provider: "anthropic",
|
|
4428
|
+
model,
|
|
4429
|
+
batchId,
|
|
4430
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4431
|
+
total: sources.length,
|
|
4432
|
+
knownTerms,
|
|
4433
|
+
jobs: jobs.map((j) => ({
|
|
4434
|
+
customId: j.customId,
|
|
4435
|
+
requests: j.chunk
|
|
4436
|
+
}))
|
|
4437
|
+
};
|
|
4438
|
+
savePendingGlossaryBatch(projectRoot, pending);
|
|
4439
|
+
return pending;
|
|
4440
|
+
}
|
|
4441
|
+
async function applyGlossarySuggestBatchResults(load, persist, provider, pending, projectRoot, ai) {
|
|
4442
|
+
provider.takeUsage?.();
|
|
4443
|
+
const outcomes = await provider.completionBatchResults(pending.batchId);
|
|
4444
|
+
const batchUsage = provider.takeUsage?.();
|
|
4445
|
+
const allTerms = [];
|
|
4446
|
+
const errors = [];
|
|
4447
|
+
const jobFailures = [];
|
|
4448
|
+
const retryChunks = [];
|
|
4449
|
+
for (const job of pending.jobs) {
|
|
4450
|
+
const outcome = outcomes.get(job.customId);
|
|
4451
|
+
if (outcome?.type === "json") {
|
|
4452
|
+
const batch = outcome.value;
|
|
4453
|
+
allTerms.push(...batch.terms ?? []);
|
|
4454
|
+
continue;
|
|
4455
|
+
}
|
|
4456
|
+
if (!outcome) jobFailures.push({ customId: job.customId, locale: "", type: "missing" });
|
|
4457
|
+
else if (outcome.type === "malformed") jobFailures.push({ customId: job.customId, locale: "", type: "malformed", raw: outcome.raw });
|
|
4458
|
+
else jobFailures.push({ customId: job.customId, locale: "", type: "failed", error: outcome.error });
|
|
4459
|
+
retryChunks.push(job.requests);
|
|
4460
|
+
}
|
|
4461
|
+
for (const chunk2 of retryChunks) {
|
|
4462
|
+
try {
|
|
4463
|
+
const raw = await provider.complete(completionRequestFor2(chunk2, pending.knownTerms));
|
|
4464
|
+
const batch = raw;
|
|
4465
|
+
allTerms.push(...batch.terms ?? []);
|
|
4466
|
+
} catch (e) {
|
|
4467
|
+
errors.push({ error: e.message });
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
const retryUsage = provider.takeUsage?.();
|
|
4471
|
+
const pricing = resolvePricing({ ...ai, model: pending.model });
|
|
4472
|
+
let estimatedCostUsd;
|
|
4473
|
+
if (pricing && (batchUsage || retryUsage)) {
|
|
4474
|
+
estimatedCostUsd = (batchUsage ? estimateUsageCostUsd(batchUsage, pricing, BATCH_PRICE_MULTIPLIER) : 0) + (retryUsage ? estimateUsageCostUsd(retryUsage, pricing) : 0);
|
|
4475
|
+
}
|
|
4476
|
+
let usage;
|
|
4477
|
+
if (batchUsage || retryUsage) {
|
|
4478
|
+
usage = batchUsage ?? { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
|
|
4479
|
+
if (retryUsage) addUsage(usage, retryUsage);
|
|
4480
|
+
}
|
|
4481
|
+
const fresh = load();
|
|
4482
|
+
const added = mergeGlossarySuggestions(fresh, dedupeTerms(allTerms));
|
|
4483
|
+
persist(fresh);
|
|
4484
|
+
clearPendingGlossaryBatch(projectRoot);
|
|
4485
|
+
const costSuffix = estimatedCostUsd !== void 0 ? ` (~$${estimatedCostUsd.toFixed(2)})` : "";
|
|
4486
|
+
appendLog(projectRoot, {
|
|
4487
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4488
|
+
kind: "glossary",
|
|
4489
|
+
summary: `Applied glossary suggestion batch ${pending.batchId}: ${added.length} new term(s), ${errors.length} error(s), ${retryChunks.length} job(s) retried${costSuffix}`,
|
|
4490
|
+
model: pending.model,
|
|
4491
|
+
jobFailures: jobFailures.length ? jobFailures : void 0,
|
|
4492
|
+
usage,
|
|
4493
|
+
estimatedCostUsd
|
|
4494
|
+
});
|
|
4495
|
+
return { added: added.length, errors, retried: retryChunks.length };
|
|
4496
|
+
}
|
|
4497
|
+
var init_glossary_batch_run = __esm({
|
|
4498
|
+
"src/server/ai/glossary-batch-run.ts"() {
|
|
4499
|
+
"use strict";
|
|
4500
|
+
init_glossary_suggest();
|
|
4501
|
+
init_pending_glossary_batch();
|
|
4502
|
+
init_state();
|
|
4503
|
+
init_log();
|
|
4504
|
+
init_pricing();
|
|
4505
|
+
}
|
|
4506
|
+
});
|
|
4507
|
+
|
|
4125
4508
|
// src/server/ai/estimate.ts
|
|
4126
4509
|
function estimateTokens(text) {
|
|
4127
4510
|
const cjk = text.match(CJK_RE)?.length ?? 0;
|
|
@@ -4170,29 +4553,131 @@ function estimateTranslation(state, ai, opts) {
|
|
|
4170
4553
|
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4171
4554
|
};
|
|
4172
4555
|
}
|
|
4173
|
-
|
|
4556
|
+
function estimateContext(targets, ai) {
|
|
4557
|
+
const batchSize = Math.max(1, ai.contextBatchSize ?? ai.batchSize ?? 10);
|
|
4558
|
+
const batches = chunk(targets, batchSize);
|
|
4559
|
+
const system = buildContextSystemPrompt();
|
|
4560
|
+
let inputTokens = 0;
|
|
4561
|
+
let outputTokens = 0;
|
|
4562
|
+
for (const batch of batches) {
|
|
4563
|
+
inputTokens += estimateTokens(system) + estimateTokens(buildContextBatchPrompt(batch));
|
|
4564
|
+
outputTokens += batch.length * (CONTEXT_REPLY_OVERHEAD + TYPICAL_CONTEXT_TOKENS);
|
|
4565
|
+
}
|
|
4566
|
+
const pricing = resolvePricing(ai);
|
|
4567
|
+
return {
|
|
4568
|
+
keys: targets.length,
|
|
4569
|
+
batches: batches.length,
|
|
4570
|
+
inputTokens,
|
|
4571
|
+
outputTokens,
|
|
4572
|
+
pricing,
|
|
4573
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4574
|
+
};
|
|
4575
|
+
}
|
|
4576
|
+
function estimateGlossarySuggest(sources, knownTerms, ai) {
|
|
4577
|
+
const batchSize = Math.max(1, ai.contextBatchSize ?? ai.batchSize ?? 10);
|
|
4578
|
+
const batches = chunk(sources, batchSize);
|
|
4579
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
4580
|
+
let inputTokens = 0;
|
|
4581
|
+
let outputTokens = 0;
|
|
4582
|
+
for (const batch of batches) {
|
|
4583
|
+
inputTokens += estimateTokens(system) + estimateTokens(buildGlossarySuggestBatchPrompt(batch, knownTerms));
|
|
4584
|
+
outputTokens += Math.ceil(batch.length * TERM_YIELD) * TERM_REPLY_TOKENS;
|
|
4585
|
+
}
|
|
4586
|
+
const pricing = resolvePricing(ai);
|
|
4587
|
+
return {
|
|
4588
|
+
sources: sources.length,
|
|
4589
|
+
batches: batches.length,
|
|
4590
|
+
inputTokens,
|
|
4591
|
+
outputTokens,
|
|
4592
|
+
pricing,
|
|
4593
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4594
|
+
};
|
|
4595
|
+
}
|
|
4596
|
+
var CJK_RE, EXPANSION, ITEM_REPLY_OVERHEAD, FORM_REPLY_OVERHEAD, CONTEXT_REPLY_OVERHEAD, TYPICAL_CONTEXT_TOKENS, TERM_REPLY_TOKENS, TERM_YIELD;
|
|
4174
4597
|
var init_estimate = __esm({
|
|
4175
4598
|
"src/server/ai/estimate.ts"() {
|
|
4176
4599
|
"use strict";
|
|
4177
4600
|
init_run();
|
|
4178
4601
|
init_provider();
|
|
4602
|
+
init_context();
|
|
4603
|
+
init_glossary_suggest();
|
|
4179
4604
|
init_batch();
|
|
4180
4605
|
init_pricing();
|
|
4181
4606
|
CJK_RE = /[ -鿿가-豈-]/g;
|
|
4182
4607
|
EXPANSION = 1.2;
|
|
4183
4608
|
ITEM_REPLY_OVERHEAD = 16;
|
|
4184
4609
|
FORM_REPLY_OVERHEAD = 8;
|
|
4610
|
+
CONTEXT_REPLY_OVERHEAD = 16;
|
|
4611
|
+
TYPICAL_CONTEXT_TOKENS = 35;
|
|
4612
|
+
TERM_REPLY_TOKENS = 24;
|
|
4613
|
+
TERM_YIELD = 0.15;
|
|
4614
|
+
}
|
|
4615
|
+
});
|
|
4616
|
+
|
|
4617
|
+
// src/server/ai/price-fetch.ts
|
|
4618
|
+
function normalizeModelsDevPrices(api) {
|
|
4619
|
+
const out = {};
|
|
4620
|
+
const ranks = {};
|
|
4621
|
+
if (!api || typeof api !== "object") return out;
|
|
4622
|
+
for (const [provId, prov] of Object.entries(api)) {
|
|
4623
|
+
const models = prov?.models;
|
|
4624
|
+
if (!models || typeof models !== "object") continue;
|
|
4625
|
+
const rank = providerRank(provId);
|
|
4626
|
+
for (const [modelKey, model] of Object.entries(models)) {
|
|
4627
|
+
const cost = model?.cost;
|
|
4628
|
+
if (!cost || typeof cost.input !== "number" || typeof cost.output !== "number") continue;
|
|
4629
|
+
const bareId = bareModelId(modelKey);
|
|
4630
|
+
if (!bareId) continue;
|
|
4631
|
+
const existingRank = ranks[bareId];
|
|
4632
|
+
if (existingRank !== void 0 && existingRank <= rank) continue;
|
|
4633
|
+
const price = { inputPerMTok: cost.input, outputPerMTok: cost.output };
|
|
4634
|
+
if (typeof cost.cache_read === "number") price.cacheReadPerMTok = cost.cache_read;
|
|
4635
|
+
if (typeof cost.cache_write === "number") price.cacheWritePerMTok = cost.cache_write;
|
|
4636
|
+
out[bareId] = price;
|
|
4637
|
+
ranks[bareId] = rank;
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
return out;
|
|
4641
|
+
}
|
|
4642
|
+
async function refreshPrices(opts = {}) {
|
|
4643
|
+
const url = opts.url ?? priceUrl();
|
|
4644
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
4645
|
+
const res = await doFetch(url);
|
|
4646
|
+
if (!res.ok) throw new Error(`Failed to fetch prices from ${url}: HTTP ${res.status}`);
|
|
4647
|
+
const api = await res.json();
|
|
4648
|
+
const models = normalizeModelsDevPrices(api);
|
|
4649
|
+
const modelCount = Object.keys(models).length;
|
|
4650
|
+
if (modelCount === 0) throw new Error(`No model prices found in response from ${url}`);
|
|
4651
|
+
const cache2 = { source: "models.dev", fetchedAt: (opts.now ?? defaultNow)(), models };
|
|
4652
|
+
const path = opts.path ?? defaultPriceCachePath();
|
|
4653
|
+
savePriceCache(cache2, path);
|
|
4654
|
+
return { source: cache2.source, fetchedAt: cache2.fetchedAt, modelCount, path };
|
|
4655
|
+
}
|
|
4656
|
+
var MODELS_DEV_URL, priceUrl, PROVIDER_PREFERENCE, providerRank, defaultNow;
|
|
4657
|
+
var init_price_fetch = __esm({
|
|
4658
|
+
"src/server/ai/price-fetch.ts"() {
|
|
4659
|
+
"use strict";
|
|
4660
|
+
init_pricing();
|
|
4661
|
+
init_price_cache();
|
|
4662
|
+
MODELS_DEV_URL = "https://models.dev/api.json";
|
|
4663
|
+
priceUrl = () => process.env.GLOTFILE_PRICES_URL || MODELS_DEV_URL;
|
|
4664
|
+
PROVIDER_PREFERENCE = ["anthropic", "openai", "google", "meta-llama", "mistral", "deepseek", "xai", "groq"];
|
|
4665
|
+
providerRank = (provId) => {
|
|
4666
|
+
const i = PROVIDER_PREFERENCE.indexOf(provId);
|
|
4667
|
+
return i === -1 ? PROVIDER_PREFERENCE.length : i;
|
|
4668
|
+
};
|
|
4669
|
+
defaultNow = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
4185
4670
|
}
|
|
4186
4671
|
});
|
|
4187
4672
|
|
|
4188
4673
|
// src/server/scan.ts
|
|
4189
|
-
import { existsSync as
|
|
4674
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
|
|
4190
4675
|
import { resolve as resolve7 } from "path";
|
|
4191
4676
|
function loadUsageCache(projectRoot) {
|
|
4192
4677
|
const path = resolve7(projectRoot, ".glotfile", "usage.json");
|
|
4193
|
-
if (!
|
|
4678
|
+
if (!existsSync11(path)) return null;
|
|
4194
4679
|
try {
|
|
4195
|
-
return JSON.parse(
|
|
4680
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
4196
4681
|
} catch {
|
|
4197
4682
|
return null;
|
|
4198
4683
|
}
|
|
@@ -4257,8 +4742,8 @@ var init_scan = __esm({
|
|
|
4257
4742
|
});
|
|
4258
4743
|
|
|
4259
4744
|
// src/server/scanner.ts
|
|
4260
|
-
import { readdirSync as readdirSync3, statSync as statSync3, readFileSync as
|
|
4261
|
-
import { join as
|
|
4745
|
+
import { readdirSync as readdirSync3, statSync as statSync3, readFileSync as readFileSync13 } from "fs";
|
|
4746
|
+
import { join as join7, extname as extname2, relative } from "path";
|
|
4262
4747
|
function scannerForExt(ext) {
|
|
4263
4748
|
return EXT_SCANNER[ext] ?? null;
|
|
4264
4749
|
}
|
|
@@ -4480,7 +4965,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
4480
4965
|
}
|
|
4481
4966
|
for (const name of entries) {
|
|
4482
4967
|
if (ALWAYS_EXCLUDE.has(name)) continue;
|
|
4483
|
-
const abs =
|
|
4968
|
+
const abs = join7(dir, name);
|
|
4484
4969
|
const rel = relative(root, abs);
|
|
4485
4970
|
let st;
|
|
4486
4971
|
try {
|
|
@@ -4510,7 +4995,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
4510
4995
|
const ext = extname2(relPath);
|
|
4511
4996
|
const scanner = scannerForExt(ext);
|
|
4512
4997
|
if (!scanner) continue;
|
|
4513
|
-
const abs =
|
|
4998
|
+
const abs = join7(projectRoot, relPath);
|
|
4514
4999
|
let st;
|
|
4515
5000
|
try {
|
|
4516
5001
|
st = statSync3(abs);
|
|
@@ -4526,7 +5011,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
4526
5011
|
}
|
|
4527
5012
|
let content;
|
|
4528
5013
|
try {
|
|
4529
|
-
content =
|
|
5014
|
+
content = readFileSync13(abs, "utf8");
|
|
4530
5015
|
} catch {
|
|
4531
5016
|
continue;
|
|
4532
5017
|
}
|
|
@@ -4660,8 +5145,8 @@ var init_scanner = __esm({
|
|
|
4660
5145
|
});
|
|
4661
5146
|
|
|
4662
5147
|
// src/server/import/detect.ts
|
|
4663
|
-
import { existsSync as
|
|
4664
|
-
import { join as
|
|
5148
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync14, statSync as statSync4 } from "fs";
|
|
5149
|
+
import { join as join8 } from "path";
|
|
4665
5150
|
function safeIsDir(p) {
|
|
4666
5151
|
try {
|
|
4667
5152
|
return statSync4(p).isDirectory();
|
|
@@ -4670,7 +5155,7 @@ function safeIsDir(p) {
|
|
|
4670
5155
|
}
|
|
4671
5156
|
}
|
|
4672
5157
|
function listDirs(dir) {
|
|
4673
|
-
return readdirSync4(dir).filter((e) => safeIsDir(
|
|
5158
|
+
return readdirSync4(dir).filter((e) => safeIsDir(join8(dir, e)));
|
|
4674
5159
|
}
|
|
4675
5160
|
function fileCount(dir) {
|
|
4676
5161
|
try {
|
|
@@ -4684,23 +5169,23 @@ function pickSource(locales, sizeOf) {
|
|
|
4684
5169
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4685
5170
|
}
|
|
4686
5171
|
function detectLaravel(root) {
|
|
4687
|
-
const localeRoot = [
|
|
5172
|
+
const localeRoot = [join8(root, "resources", "lang"), join8(root, "lang")].find(safeIsDir);
|
|
4688
5173
|
if (!localeRoot) return null;
|
|
4689
5174
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4690
5175
|
if (locales.length === 0) return null;
|
|
4691
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
5176
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join8(localeRoot, loc)));
|
|
4692
5177
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4693
5178
|
}
|
|
4694
5179
|
function detectVue(root, forced = false) {
|
|
4695
5180
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4696
|
-
const localeRoot =
|
|
5181
|
+
const localeRoot = join8(root, rel);
|
|
4697
5182
|
if (!safeIsDir(localeRoot)) continue;
|
|
4698
5183
|
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4699
5184
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4700
5185
|
if (enough) {
|
|
4701
5186
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4702
5187
|
try {
|
|
4703
|
-
return statSync4(
|
|
5188
|
+
return statSync4(join8(localeRoot, `${loc}.json`)).size;
|
|
4704
5189
|
} catch {
|
|
4705
5190
|
return 0;
|
|
4706
5191
|
}
|
|
@@ -4711,9 +5196,9 @@ function detectVue(root, forced = false) {
|
|
|
4711
5196
|
return null;
|
|
4712
5197
|
}
|
|
4713
5198
|
function hasNextIntlSignal(root) {
|
|
4714
|
-
if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) =>
|
|
5199
|
+
if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync12(join8(root, rel)))) return true;
|
|
4715
5200
|
try {
|
|
4716
|
-
const pkg = JSON.parse(
|
|
5201
|
+
const pkg = JSON.parse(readFileSync14(join8(root, "package.json"), "utf8"));
|
|
4717
5202
|
if (pkg.dependencies?.["next-intl"] || pkg.devDependencies?.["next-intl"]) return true;
|
|
4718
5203
|
} catch {
|
|
4719
5204
|
}
|
|
@@ -4722,7 +5207,7 @@ function hasNextIntlSignal(root) {
|
|
|
4722
5207
|
function nextIntlDefaultLocale(root) {
|
|
4723
5208
|
for (const rel of NEXT_INTL_ROUTING_CANDIDATES) {
|
|
4724
5209
|
try {
|
|
4725
|
-
const m =
|
|
5210
|
+
const m = readFileSync14(join8(root, rel), "utf8").match(/defaultLocale\s*:\s*['"]([^'"]+)['"]/);
|
|
4726
5211
|
if (m) return m[1];
|
|
4727
5212
|
} catch {
|
|
4728
5213
|
}
|
|
@@ -4732,14 +5217,14 @@ function nextIntlDefaultLocale(root) {
|
|
|
4732
5217
|
function detectNextIntl(root, forced = false) {
|
|
4733
5218
|
if (!forced && !hasNextIntlSignal(root)) return null;
|
|
4734
5219
|
for (const rel of NEXT_INTL_DIR_CANDIDATES) {
|
|
4735
|
-
const localeRoot =
|
|
5220
|
+
const localeRoot = join8(root, rel);
|
|
4736
5221
|
if (!safeIsDir(localeRoot)) continue;
|
|
4737
5222
|
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4738
5223
|
if (locales.length === 0) continue;
|
|
4739
5224
|
const def = nextIntlDefaultLocale(root);
|
|
4740
5225
|
const sourceLocale = def && locales.includes(def) ? def : pickSource(locales, (loc) => {
|
|
4741
5226
|
try {
|
|
4742
|
-
return statSync4(
|
|
5227
|
+
return statSync4(join8(localeRoot, `${loc}.json`)).size;
|
|
4743
5228
|
} catch {
|
|
4744
5229
|
return 0;
|
|
4745
5230
|
}
|
|
@@ -4750,7 +5235,7 @@ function detectNextIntl(root, forced = false) {
|
|
|
4750
5235
|
}
|
|
4751
5236
|
function detectArb(root) {
|
|
4752
5237
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4753
|
-
const localeRoot =
|
|
5238
|
+
const localeRoot = join8(root, rel);
|
|
4754
5239
|
if (!safeIsDir(localeRoot)) continue;
|
|
4755
5240
|
const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4756
5241
|
if (locales.length >= 1) {
|
|
@@ -4760,10 +5245,10 @@ function detectArb(root) {
|
|
|
4760
5245
|
return null;
|
|
4761
5246
|
}
|
|
4762
5247
|
function lprojLocales(dir) {
|
|
4763
|
-
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
5248
|
+
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync12(join8(dir, `${l}.lproj`, "Localizable.strings")));
|
|
4764
5249
|
}
|
|
4765
5250
|
function detectApple(root) {
|
|
4766
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5251
|
+
const candidates = [root, ...listDirs(root).map((d) => join8(root, d))];
|
|
4767
5252
|
let best = null;
|
|
4768
5253
|
for (const dir of candidates) {
|
|
4769
5254
|
const locales = lprojLocales(dir);
|
|
@@ -4775,7 +5260,7 @@ function detectApple(root) {
|
|
|
4775
5260
|
locales,
|
|
4776
5261
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4777
5262
|
try {
|
|
4778
|
-
return statSync4(
|
|
5263
|
+
return statSync4(join8(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4779
5264
|
} catch {
|
|
4780
5265
|
return 0;
|
|
4781
5266
|
}
|
|
@@ -4787,7 +5272,7 @@ function detectApple(root) {
|
|
|
4787
5272
|
}
|
|
4788
5273
|
function detectAngularXliff(root) {
|
|
4789
5274
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4790
|
-
const localeRoot = rel === "." ? root :
|
|
5275
|
+
const localeRoot = rel === "." ? root : join8(root, rel);
|
|
4791
5276
|
if (!safeIsDir(localeRoot)) continue;
|
|
4792
5277
|
const files = readdirSync4(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4793
5278
|
if (files.length === 0) continue;
|
|
@@ -4795,7 +5280,7 @@ function detectAngularXliff(root) {
|
|
|
4795
5280
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4796
5281
|
let sourceLocale;
|
|
4797
5282
|
try {
|
|
4798
|
-
sourceLocale =
|
|
5283
|
+
sourceLocale = readFileSync14(join8(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4799
5284
|
} catch {
|
|
4800
5285
|
}
|
|
4801
5286
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -4806,14 +5291,14 @@ function detectAngularXliff(root) {
|
|
|
4806
5291
|
return null;
|
|
4807
5292
|
}
|
|
4808
5293
|
function detectRails(root) {
|
|
4809
|
-
const localeRoot =
|
|
5294
|
+
const localeRoot = join8(root, "config", "locales");
|
|
4810
5295
|
if (!safeIsDir(localeRoot)) return null;
|
|
4811
5296
|
const locales = [];
|
|
4812
5297
|
for (const file of readdirSync4(localeRoot).sort()) {
|
|
4813
5298
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
4814
5299
|
let text;
|
|
4815
5300
|
try {
|
|
4816
|
-
text =
|
|
5301
|
+
text = readFileSync14(join8(localeRoot, file), "utf8");
|
|
4817
5302
|
} catch {
|
|
4818
5303
|
continue;
|
|
4819
5304
|
}
|
|
@@ -4827,15 +5312,15 @@ function detectRails(root) {
|
|
|
4827
5312
|
}
|
|
4828
5313
|
function detectI18next(root) {
|
|
4829
5314
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4830
|
-
const localeRoot =
|
|
5315
|
+
const localeRoot = join8(root, rel);
|
|
4831
5316
|
if (!safeIsDir(localeRoot)) continue;
|
|
4832
5317
|
const locales = listDirs(localeRoot).filter(
|
|
4833
|
-
(d) => LOCALE_RE.test(d) && readdirSync4(
|
|
5318
|
+
(d) => LOCALE_RE.test(d) && readdirSync4(join8(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4834
5319
|
);
|
|
4835
5320
|
if (locales.length === 0) continue;
|
|
4836
5321
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4837
5322
|
try {
|
|
4838
|
-
return readdirSync4(
|
|
5323
|
+
return readdirSync4(join8(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync4(join8(localeRoot, loc, f)).size, 0);
|
|
4839
5324
|
} catch {
|
|
4840
5325
|
return 0;
|
|
4841
5326
|
}
|
|
@@ -4852,8 +5337,8 @@ function gettextLocales(dir) {
|
|
|
4852
5337
|
if (!locales.includes(flat)) locales.push(flat);
|
|
4853
5338
|
continue;
|
|
4854
5339
|
}
|
|
4855
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
4856
|
-
const sub =
|
|
5340
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join8(dir, entry))) continue;
|
|
5341
|
+
const sub = join8(dir, entry);
|
|
4857
5342
|
const hasPo = (d) => {
|
|
4858
5343
|
try {
|
|
4859
5344
|
return readdirSync4(d).some((f) => f.endsWith(".po"));
|
|
@@ -4861,7 +5346,7 @@ function gettextLocales(dir) {
|
|
|
4861
5346
|
return false;
|
|
4862
5347
|
}
|
|
4863
5348
|
};
|
|
4864
|
-
if (hasPo(
|
|
5349
|
+
if (hasPo(join8(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4865
5350
|
if (!locales.includes(entry)) locales.push(entry);
|
|
4866
5351
|
}
|
|
4867
5352
|
}
|
|
@@ -4869,7 +5354,7 @@ function gettextLocales(dir) {
|
|
|
4869
5354
|
}
|
|
4870
5355
|
function detectGettext(root) {
|
|
4871
5356
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4872
|
-
const localeRoot =
|
|
5357
|
+
const localeRoot = join8(root, rel);
|
|
4873
5358
|
if (!safeIsDir(localeRoot)) continue;
|
|
4874
5359
|
const locales = gettextLocales(localeRoot);
|
|
4875
5360
|
if (locales.length === 0) continue;
|
|
@@ -4878,10 +5363,10 @@ function detectGettext(root) {
|
|
|
4878
5363
|
return null;
|
|
4879
5364
|
}
|
|
4880
5365
|
function detectAppleStringsdict(root) {
|
|
4881
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5366
|
+
const candidates = [root, ...listDirs(root).map((d) => join8(root, d))];
|
|
4882
5367
|
let best = null;
|
|
4883
5368
|
for (const dir of candidates) {
|
|
4884
|
-
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
5369
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync12(join8(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
4885
5370
|
if (locales.length === 0) continue;
|
|
4886
5371
|
if (!best || locales.length > best.locales.length) {
|
|
4887
5372
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4890,7 +5375,7 @@ function detectAppleStringsdict(root) {
|
|
|
4890
5375
|
return best;
|
|
4891
5376
|
}
|
|
4892
5377
|
function detect(root, formatOverride) {
|
|
4893
|
-
if (!
|
|
5378
|
+
if (!existsSync12(root)) return null;
|
|
4894
5379
|
if (formatOverride) {
|
|
4895
5380
|
const fn = BY_FORMAT[formatOverride];
|
|
4896
5381
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -4968,8 +5453,8 @@ var init_flatten = __esm({
|
|
|
4968
5453
|
});
|
|
4969
5454
|
|
|
4970
5455
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4971
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
4972
|
-
import { join as
|
|
5456
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync15 } from "fs";
|
|
5457
|
+
import { join as join9 } from "path";
|
|
4973
5458
|
function fromVueI18n(value) {
|
|
4974
5459
|
return value.replace(/\{'([^']*)'\}/g, "'$1'");
|
|
4975
5460
|
}
|
|
@@ -4992,7 +5477,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
4992
5477
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4993
5478
|
let data;
|
|
4994
5479
|
try {
|
|
4995
|
-
data = JSON.parse(
|
|
5480
|
+
data = JSON.parse(readFileSync15(join9(localeRoot, file), "utf8"));
|
|
4996
5481
|
} catch (e) {
|
|
4997
5482
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4998
5483
|
continue;
|
|
@@ -5009,8 +5494,8 @@ var init_vue_i18n_json2 = __esm({
|
|
|
5009
5494
|
});
|
|
5010
5495
|
|
|
5011
5496
|
// src/server/import/parsers/next-intl-json.ts
|
|
5012
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
5013
|
-
import { join as
|
|
5497
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync16 } from "fs";
|
|
5498
|
+
import { join as join10 } from "path";
|
|
5014
5499
|
var LOCALE_RE3, nextIntlJson2;
|
|
5015
5500
|
var init_next_intl_json2 = __esm({
|
|
5016
5501
|
"src/server/import/parsers/next-intl-json.ts"() {
|
|
@@ -5030,7 +5515,7 @@ var init_next_intl_json2 = __esm({
|
|
|
5030
5515
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5031
5516
|
let data;
|
|
5032
5517
|
try {
|
|
5033
|
-
data = JSON.parse(
|
|
5518
|
+
data = JSON.parse(readFileSync16(join10(localeRoot, file), "utf8"));
|
|
5034
5519
|
} catch (e) {
|
|
5035
5520
|
warnings.push(`next-intl-json: failed to parse ${file}: ${e.message}`);
|
|
5036
5521
|
continue;
|
|
@@ -5064,16 +5549,16 @@ var init_placeholders2 = __esm({
|
|
|
5064
5549
|
|
|
5065
5550
|
// src/server/import/parsers/laravel-php.ts
|
|
5066
5551
|
import { readdirSync as readdirSync7, statSync as statSync5 } from "fs";
|
|
5067
|
-
import { join as
|
|
5552
|
+
import { join as join11, relative as relative2 } from "path";
|
|
5068
5553
|
import { execFileSync } from "child_process";
|
|
5069
5554
|
function listDirs2(dir) {
|
|
5070
|
-
return readdirSync7(dir).filter((e) => statSync5(
|
|
5555
|
+
return readdirSync7(dir).filter((e) => statSync5(join11(dir, e)).isDirectory());
|
|
5071
5556
|
}
|
|
5072
5557
|
function listPhpFiles(dir) {
|
|
5073
5558
|
const out = [];
|
|
5074
5559
|
const walk = (d) => {
|
|
5075
5560
|
for (const e of readdirSync7(d)) {
|
|
5076
|
-
const full =
|
|
5561
|
+
const full = join11(d, e);
|
|
5077
5562
|
if (statSync5(full).isDirectory()) walk(full);
|
|
5078
5563
|
else if (e.endsWith(".php")) out.push(full);
|
|
5079
5564
|
}
|
|
@@ -5116,7 +5601,7 @@ var init_laravel_php2 = __esm({
|
|
|
5116
5601
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
5117
5602
|
if (locale === "vendor") continue;
|
|
5118
5603
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5119
|
-
const localeDir =
|
|
5604
|
+
const localeDir = join11(localeRoot, locale);
|
|
5120
5605
|
locales.push(locale);
|
|
5121
5606
|
for (const file of listPhpFiles(localeDir)) {
|
|
5122
5607
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -5141,8 +5626,8 @@ var init_laravel_php2 = __esm({
|
|
|
5141
5626
|
});
|
|
5142
5627
|
|
|
5143
5628
|
// src/server/import/parsers/flutter-arb.ts
|
|
5144
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
5145
|
-
import { join as
|
|
5629
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync17 } from "fs";
|
|
5630
|
+
import { join as join12 } from "path";
|
|
5146
5631
|
function localeFromArbName(file) {
|
|
5147
5632
|
const m = file.match(/^(.+)\.arb$/);
|
|
5148
5633
|
if (!m) return null;
|
|
@@ -5182,7 +5667,7 @@ var init_flutter_arb2 = __esm({
|
|
|
5182
5667
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5183
5668
|
let data;
|
|
5184
5669
|
try {
|
|
5185
|
-
data = JSON.parse(
|
|
5670
|
+
data = JSON.parse(readFileSync17(join12(localeRoot, file), "utf8"));
|
|
5186
5671
|
} catch (e) {
|
|
5187
5672
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
5188
5673
|
continue;
|
|
@@ -5209,8 +5694,8 @@ var init_flutter_arb2 = __esm({
|
|
|
5209
5694
|
});
|
|
5210
5695
|
|
|
5211
5696
|
// src/server/import/parsers/apple-strings.ts
|
|
5212
|
-
import { readdirSync as readdirSync9, readFileSync as
|
|
5213
|
-
import { join as
|
|
5697
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync18, statSync as statSync6 } from "fs";
|
|
5698
|
+
import { join as join13 } from "path";
|
|
5214
5699
|
function localeFromLproj(dir) {
|
|
5215
5700
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
5216
5701
|
if (!m) return null;
|
|
@@ -5330,16 +5815,16 @@ var init_apple_strings2 = __esm({
|
|
|
5330
5815
|
const locale = localeFromLproj(dir);
|
|
5331
5816
|
if (!locale) continue;
|
|
5332
5817
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5333
|
-
const file =
|
|
5818
|
+
const file = join13(localeRoot, dir, TABLE);
|
|
5334
5819
|
let text;
|
|
5335
5820
|
try {
|
|
5336
5821
|
if (!statSync6(file).isFile()) continue;
|
|
5337
|
-
text =
|
|
5822
|
+
text = readFileSync18(file, "utf8");
|
|
5338
5823
|
} catch {
|
|
5339
5824
|
continue;
|
|
5340
5825
|
}
|
|
5341
5826
|
locales.push(locale);
|
|
5342
|
-
const others = readdirSync9(
|
|
5827
|
+
const others = readdirSync9(join13(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
5343
5828
|
if (others.length) {
|
|
5344
5829
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
5345
5830
|
}
|
|
@@ -5354,8 +5839,8 @@ var init_apple_strings2 = __esm({
|
|
|
5354
5839
|
});
|
|
5355
5840
|
|
|
5356
5841
|
// src/server/import/parsers/angular-xliff.ts
|
|
5357
|
-
import { readdirSync as readdirSync10, readFileSync as
|
|
5358
|
-
import { join as
|
|
5842
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync19 } from "fs";
|
|
5843
|
+
import { join as join14 } from "path";
|
|
5359
5844
|
function decodeEntities(s) {
|
|
5360
5845
|
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
5361
5846
|
}
|
|
@@ -5426,7 +5911,7 @@ var init_angular_xliff2 = __esm({
|
|
|
5426
5911
|
if (fnameLocale !== void 0 && !LOCALE_RE6.test(fnameLocale)) continue;
|
|
5427
5912
|
let xml;
|
|
5428
5913
|
try {
|
|
5429
|
-
xml =
|
|
5914
|
+
xml = readFileSync19(join14(localeRoot, file), "utf8");
|
|
5430
5915
|
} catch (e) {
|
|
5431
5916
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
5432
5917
|
continue;
|
|
@@ -5473,8 +5958,8 @@ var init_angular_xliff2 = __esm({
|
|
|
5473
5958
|
});
|
|
5474
5959
|
|
|
5475
5960
|
// src/server/import/parsers/gettext-po.ts
|
|
5476
|
-
import { readdirSync as readdirSync11, readFileSync as
|
|
5477
|
-
import { join as
|
|
5961
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync20 } from "fs";
|
|
5962
|
+
import { join as join15 } from "path";
|
|
5478
5963
|
function unescapePo(s) {
|
|
5479
5964
|
return s.replace(
|
|
5480
5965
|
/\\([\\"ntr])/g,
|
|
@@ -5563,17 +6048,17 @@ function discoverPoFiles(root) {
|
|
|
5563
6048
|
for (const e of entries) {
|
|
5564
6049
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
5565
6050
|
const base = e.name.slice(0, -3);
|
|
5566
|
-
found.push({ path:
|
|
6051
|
+
found.push({ path: join15(root, e.name), rel: e.name, locale: LOCALE_RE7.test(base) ? base : null });
|
|
5567
6052
|
} else if (e.isDirectory() && LOCALE_RE7.test(e.name)) {
|
|
5568
|
-
for (const sub of [
|
|
6053
|
+
for (const sub of [join15(e.name, "LC_MESSAGES"), e.name]) {
|
|
5569
6054
|
let names;
|
|
5570
6055
|
try {
|
|
5571
|
-
names = readdirSync11(
|
|
6056
|
+
names = readdirSync11(join15(root, sub)).sort();
|
|
5572
6057
|
} catch {
|
|
5573
6058
|
continue;
|
|
5574
6059
|
}
|
|
5575
6060
|
for (const f of names) {
|
|
5576
|
-
if (f.endsWith(".po")) found.push({ path:
|
|
6061
|
+
if (f.endsWith(".po")) found.push({ path: join15(root, sub, f), rel: join15(sub, f), locale: e.name });
|
|
5577
6062
|
}
|
|
5578
6063
|
}
|
|
5579
6064
|
}
|
|
@@ -5597,7 +6082,7 @@ var init_gettext_po2 = __esm({
|
|
|
5597
6082
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
5598
6083
|
let entries;
|
|
5599
6084
|
try {
|
|
5600
|
-
entries = parseEntries(
|
|
6085
|
+
entries = parseEntries(readFileSync20(file.path, "utf8"));
|
|
5601
6086
|
} catch (e) {
|
|
5602
6087
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
5603
6088
|
continue;
|
|
@@ -5644,8 +6129,8 @@ var init_gettext_po2 = __esm({
|
|
|
5644
6129
|
});
|
|
5645
6130
|
|
|
5646
6131
|
// src/server/import/parsers/i18next-json.ts
|
|
5647
|
-
import { readdirSync as readdirSync12, readFileSync as
|
|
5648
|
-
import { join as
|
|
6132
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync21, statSync as statSync7 } from "fs";
|
|
6133
|
+
import { join as join16 } from "path";
|
|
5649
6134
|
function safeIsDir2(p) {
|
|
5650
6135
|
try {
|
|
5651
6136
|
return statSync7(p).isDirectory();
|
|
@@ -5660,7 +6145,7 @@ function fromI18next(value) {
|
|
|
5660
6145
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
5661
6146
|
let data;
|
|
5662
6147
|
try {
|
|
5663
|
-
data = JSON.parse(
|
|
6148
|
+
data = JSON.parse(readFileSync21(path, "utf8"));
|
|
5664
6149
|
} catch (e) {
|
|
5665
6150
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
5666
6151
|
return false;
|
|
@@ -5713,7 +6198,7 @@ var init_i18next_json2 = __esm({
|
|
|
5713
6198
|
const keys = {};
|
|
5714
6199
|
const locales = [];
|
|
5715
6200
|
for (const entry of readdirSync12(localeRoot).sort()) {
|
|
5716
|
-
const full =
|
|
6201
|
+
const full = join16(localeRoot, entry);
|
|
5717
6202
|
if (safeIsDir2(full)) {
|
|
5718
6203
|
if (!LOCALE_RE8.test(entry)) continue;
|
|
5719
6204
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
@@ -5722,7 +6207,7 @@ var init_i18next_json2 = __esm({
|
|
|
5722
6207
|
if (!file.endsWith(".json")) continue;
|
|
5723
6208
|
const ns = file.slice(0, -".json".length);
|
|
5724
6209
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
5725
|
-
if (ingestFile(
|
|
6210
|
+
if (ingestFile(join16(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
5726
6211
|
}
|
|
5727
6212
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
5728
6213
|
} else if (entry.endsWith(".json")) {
|
|
@@ -5741,8 +6226,8 @@ var init_i18next_json2 = __esm({
|
|
|
5741
6226
|
});
|
|
5742
6227
|
|
|
5743
6228
|
// src/server/import/parsers/rails-yaml.ts
|
|
5744
|
-
import { readdirSync as readdirSync13, readFileSync as
|
|
5745
|
-
import { join as
|
|
6229
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync22 } from "fs";
|
|
6230
|
+
import { join as join17 } from "path";
|
|
5746
6231
|
function makeNode() {
|
|
5747
6232
|
return /* @__PURE__ */ Object.create(null);
|
|
5748
6233
|
}
|
|
@@ -5966,7 +6451,7 @@ var init_rails_yaml2 = __esm({
|
|
|
5966
6451
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5967
6452
|
let text;
|
|
5968
6453
|
try {
|
|
5969
|
-
text =
|
|
6454
|
+
text = readFileSync22(join17(localeRoot, file), "utf8");
|
|
5970
6455
|
} catch (e) {
|
|
5971
6456
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5972
6457
|
continue;
|
|
@@ -5989,8 +6474,8 @@ var init_rails_yaml2 = __esm({
|
|
|
5989
6474
|
});
|
|
5990
6475
|
|
|
5991
6476
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5992
|
-
import { readdirSync as readdirSync14, readFileSync as
|
|
5993
|
-
import { join as
|
|
6477
|
+
import { readdirSync as readdirSync14, readFileSync as readFileSync23, statSync as statSync8 } from "fs";
|
|
6478
|
+
import { join as join18 } from "path";
|
|
5994
6479
|
function localeFromLproj2(dir) {
|
|
5995
6480
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
5996
6481
|
if (!m) return null;
|
|
@@ -6150,16 +6635,16 @@ var init_apple_stringsdict2 = __esm({
|
|
|
6150
6635
|
const locale = localeFromLproj2(dir);
|
|
6151
6636
|
if (!locale) continue;
|
|
6152
6637
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
6153
|
-
const file =
|
|
6638
|
+
const file = join18(localeRoot, dir, TABLE2);
|
|
6154
6639
|
let text;
|
|
6155
6640
|
try {
|
|
6156
6641
|
if (!statSync8(file).isFile()) continue;
|
|
6157
|
-
text =
|
|
6642
|
+
text = readFileSync23(file, "utf8");
|
|
6158
6643
|
} catch {
|
|
6159
6644
|
continue;
|
|
6160
6645
|
}
|
|
6161
6646
|
locales.push(locale);
|
|
6162
|
-
const others = readdirSync14(
|
|
6647
|
+
const others = readdirSync14(join18(localeRoot, dir)).filter(
|
|
6163
6648
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
6164
6649
|
);
|
|
6165
6650
|
if (others.length) {
|
|
@@ -6633,7 +7118,7 @@ var init_run2 = __esm({
|
|
|
6633
7118
|
});
|
|
6634
7119
|
|
|
6635
7120
|
// src/server/lint/outputs.ts
|
|
6636
|
-
import { readFileSync as
|
|
7121
|
+
import { readFileSync as readFileSync24, existsSync as existsSync13 } from "fs";
|
|
6637
7122
|
import { resolve as resolve8 } from "path";
|
|
6638
7123
|
function checkOutputs(state, root) {
|
|
6639
7124
|
const out = [];
|
|
@@ -6641,7 +7126,7 @@ function checkOutputs(state, root) {
|
|
|
6641
7126
|
const result = getAdapter(output.adapter).export(state, output);
|
|
6642
7127
|
for (const file of result.files) {
|
|
6643
7128
|
const abs = resolve8(root, file.path);
|
|
6644
|
-
const current =
|
|
7129
|
+
const current = existsSync13(abs) ? readFileSync24(abs, "utf8") : null;
|
|
6645
7130
|
if (current === null) {
|
|
6646
7131
|
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
6647
7132
|
} else if (current !== file.contents) {
|
|
@@ -6749,6 +7234,7 @@ function assemble2(parsed, opts) {
|
|
|
6749
7234
|
spelling: { customWords: [] }
|
|
6750
7235
|
},
|
|
6751
7236
|
glossary: [],
|
|
7237
|
+
glossarySuggestions: [],
|
|
6752
7238
|
keys,
|
|
6753
7239
|
warnings
|
|
6754
7240
|
};
|
|
@@ -7092,24 +7578,74 @@ var init_checks = __esm({
|
|
|
7092
7578
|
}
|
|
7093
7579
|
});
|
|
7094
7580
|
|
|
7095
|
-
// src/server/
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
const parsed = JSON.parse(readFileSync23(path, "utf8"));
|
|
7102
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
7103
|
-
} catch {
|
|
7104
|
-
return {};
|
|
7581
|
+
// src/server/ai/explain-error.ts
|
|
7582
|
+
function rawMessage(err) {
|
|
7583
|
+
if (err instanceof Error && err.message) return err.message;
|
|
7584
|
+
if (typeof err === "string") return err;
|
|
7585
|
+
if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
|
|
7586
|
+
return err.message;
|
|
7105
7587
|
}
|
|
7588
|
+
return String(err ?? "Unknown error");
|
|
7106
7589
|
}
|
|
7107
|
-
function
|
|
7108
|
-
const raw =
|
|
7109
|
-
const
|
|
7110
|
-
if (
|
|
7111
|
-
|
|
7112
|
-
|
|
7590
|
+
function explainProviderError(provider, err) {
|
|
7591
|
+
const raw = rawMessage(err);
|
|
7592
|
+
const m = raw.toLowerCase();
|
|
7593
|
+
if (provider === "bedrock") {
|
|
7594
|
+
if (/could not load credentials|unable to locate credentials|credentialsprovider|credentials from any providers/.test(m)) {
|
|
7595
|
+
return "No AWS credentials found. Set AWS_PROFILE (or AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) in your shell or a .env file in the directory you started glotfile from, or use an SSO / instance role. If you just edited .env, restart glotfile so it reloads. For SSO, run `aws sso login`.";
|
|
7596
|
+
}
|
|
7597
|
+
if (/on-demand throughput isn.?t supported/.test(m) || /inference profile/.test(m)) {
|
|
7598
|
+
return 'This Bedrock model needs an inference profile for on-demand use. Prefix the model id with your region group \u2014 e.g. "eu.anthropic.claude-3-5-sonnet-20241022-v2:0" (or "us." / "apac." for your region).';
|
|
7599
|
+
}
|
|
7600
|
+
if (/not authorized to perform/.test(m) || /no identity-based policy/.test(m) || /bedrock:invoke/.test(m)) {
|
|
7601
|
+
return "Your AWS credentials authenticated, but their IAM policy doesn't allow this action. Add bedrock:InvokeModel (and bedrock:InvokeModelWithResponseStream) for this model to the IAM policy on this user/role.";
|
|
7602
|
+
}
|
|
7603
|
+
if (/access to the model|don.?t have access to the model/.test(m)) {
|
|
7604
|
+
return "Your account doesn't have access to this model in this region. Enable it in the Bedrock console under Model access, for the region you configured.";
|
|
7605
|
+
}
|
|
7606
|
+
if (/access ?denied/.test(m)) {
|
|
7607
|
+
return "Bedrock denied access. Either the model isn't enabled for your account/region (enable it in the Bedrock console under Model access) or your IAM policy is missing bedrock:InvokeModel for this model.";
|
|
7608
|
+
}
|
|
7609
|
+
if (/region/.test(m)) {
|
|
7610
|
+
return "No AWS region set for Bedrock. Set the Region in AI settings, or AWS_REGION in your environment.";
|
|
7611
|
+
}
|
|
7612
|
+
}
|
|
7613
|
+
const keyEnv = KEY_ENV[provider];
|
|
7614
|
+
if (keyEnv && /api key|unauthorized|\b401\b|authentication|incorrect api key|invalid x-api-key/.test(m)) {
|
|
7615
|
+
return `${provider} rejected the request \u2014 check ${keyEnv}. Set it in your environment or a .env file in the directory you started glotfile from.`;
|
|
7616
|
+
}
|
|
7617
|
+
return raw;
|
|
7618
|
+
}
|
|
7619
|
+
var KEY_ENV;
|
|
7620
|
+
var init_explain_error = __esm({
|
|
7621
|
+
"src/server/ai/explain-error.ts"() {
|
|
7622
|
+
"use strict";
|
|
7623
|
+
KEY_ENV = {
|
|
7624
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
7625
|
+
openai: "OPENAI_API_KEY",
|
|
7626
|
+
openrouter: "OPENROUTER_API_KEY"
|
|
7627
|
+
};
|
|
7628
|
+
}
|
|
7629
|
+
});
|
|
7630
|
+
|
|
7631
|
+
// src/server/ui-prefs.ts
|
|
7632
|
+
import { readFileSync as readFileSync25 } from "fs";
|
|
7633
|
+
import { homedir as homedir2 } from "os";
|
|
7634
|
+
import { join as join19 } from "path";
|
|
7635
|
+
function readJson2(path) {
|
|
7636
|
+
try {
|
|
7637
|
+
const parsed = JSON.parse(readFileSync25(path, "utf8"));
|
|
7638
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
7639
|
+
} catch {
|
|
7640
|
+
return {};
|
|
7641
|
+
}
|
|
7642
|
+
}
|
|
7643
|
+
function loadUiPrefs(path) {
|
|
7644
|
+
const raw = readJson2(path);
|
|
7645
|
+
const prefs = { theme: isThemeMode(raw.theme) ? raw.theme : DEFAULTS.theme };
|
|
7646
|
+
if (isPanelWidth(raw.keyColumnWidth)) prefs.keyColumnWidth = Math.round(raw.keyColumnWidth);
|
|
7647
|
+
if (isPanelWidth(raw.detailPanelWidth)) prefs.detailPanelWidth = Math.round(raw.detailPanelWidth);
|
|
7648
|
+
return prefs;
|
|
7113
7649
|
}
|
|
7114
7650
|
function saveUiPrefs(path, prefs) {
|
|
7115
7651
|
const merged = { ...readJson2(path), ...prefs };
|
|
@@ -7123,7 +7659,7 @@ var init_ui_prefs = __esm({
|
|
|
7123
7659
|
THEMES = ["system", "light", "dark"];
|
|
7124
7660
|
isThemeMode = (v) => THEMES.includes(v);
|
|
7125
7661
|
isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
7126
|
-
defaultUiPrefsPath = () =>
|
|
7662
|
+
defaultUiPrefsPath = () => join19(homedir2(), ".glotfile", "ui.json");
|
|
7127
7663
|
DEFAULTS = { theme: "system" };
|
|
7128
7664
|
}
|
|
7129
7665
|
});
|
|
@@ -7157,7 +7693,7 @@ var init_events = __esm({
|
|
|
7157
7693
|
|
|
7158
7694
|
// src/server/watch.ts
|
|
7159
7695
|
import { statSync as statSync9, readdirSync as readdirSync15 } from "fs";
|
|
7160
|
-
import { join as
|
|
7696
|
+
import { join as join20 } from "path";
|
|
7161
7697
|
import { createHash as createHash2 } from "crypto";
|
|
7162
7698
|
function hashState(state) {
|
|
7163
7699
|
return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
|
|
@@ -7173,15 +7709,15 @@ function signature(statePath) {
|
|
|
7173
7709
|
const parts = [];
|
|
7174
7710
|
for (const rel of ["config.json", "keys.json"]) {
|
|
7175
7711
|
try {
|
|
7176
|
-
const s = statSync9(
|
|
7712
|
+
const s = statSync9(join20(dir, rel));
|
|
7177
7713
|
parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
|
|
7178
7714
|
} catch {
|
|
7179
7715
|
}
|
|
7180
7716
|
}
|
|
7181
7717
|
try {
|
|
7182
|
-
for (const name of readdirSync15(
|
|
7718
|
+
for (const name of readdirSync15(join20(dir, "locales")).sort()) {
|
|
7183
7719
|
if (!name.endsWith(".json")) continue;
|
|
7184
|
-
const s = statSync9(
|
|
7720
|
+
const s = statSync9(join20(dir, "locales", name));
|
|
7185
7721
|
parts.push(`${name}:${s.size}:${s.mtimeMs}`);
|
|
7186
7722
|
}
|
|
7187
7723
|
} catch {
|
|
@@ -7260,34 +7796,19 @@ var init_watch = __esm({
|
|
|
7260
7796
|
// src/server/api.ts
|
|
7261
7797
|
import { Hono } from "hono";
|
|
7262
7798
|
import { streamSSE } from "hono/streaming";
|
|
7263
|
-
import { readFileSync as
|
|
7799
|
+
import { readFileSync as readFileSync26, existsSync as existsSync14, readdirSync as readdirSync16, statSync as statSync10, rmSync as rmSync7 } from "fs";
|
|
7264
7800
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
7265
7801
|
function projectName(root) {
|
|
7266
7802
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
7267
|
-
if (
|
|
7803
|
+
if (existsSync14(nameFile)) {
|
|
7268
7804
|
try {
|
|
7269
|
-
const name =
|
|
7805
|
+
const name = readFileSync26(nameFile, "utf8").trim();
|
|
7270
7806
|
if (name) return name;
|
|
7271
7807
|
} catch {
|
|
7272
7808
|
}
|
|
7273
7809
|
}
|
|
7274
7810
|
return basename(root);
|
|
7275
7811
|
}
|
|
7276
|
-
function attachUsageSnippets(targets, cache2, projectRoot) {
|
|
7277
|
-
const fileCache = /* @__PURE__ */ new Map();
|
|
7278
|
-
for (const target of targets) {
|
|
7279
|
-
const allRefs = Object.entries(cache2.files).flatMap(
|
|
7280
|
-
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
7281
|
-
key: r.key,
|
|
7282
|
-
file,
|
|
7283
|
-
line: r.line,
|
|
7284
|
-
col: r.col,
|
|
7285
|
-
scanner: r.scanner
|
|
7286
|
-
}))
|
|
7287
|
-
);
|
|
7288
|
-
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
7289
|
-
}
|
|
7290
|
-
}
|
|
7291
7812
|
function createApi(deps) {
|
|
7292
7813
|
const app = new Hono();
|
|
7293
7814
|
const load = () => loadState(deps.statePath);
|
|
@@ -7419,6 +7940,61 @@ function createApi(deps) {
|
|
|
7419
7940
|
}
|
|
7420
7941
|
return c.json({ ok: true });
|
|
7421
7942
|
});
|
|
7943
|
+
app.post("/ai-test", async (c) => {
|
|
7944
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7945
|
+
const meta = { provider: aiCfg.provider, model: aiCfg.model };
|
|
7946
|
+
let provider;
|
|
7947
|
+
try {
|
|
7948
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7949
|
+
} catch (e) {
|
|
7950
|
+
return c.json({ ok: false, ...meta, error: explainProviderError(aiCfg.provider, e) });
|
|
7951
|
+
}
|
|
7952
|
+
const controller = new AbortController();
|
|
7953
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
7954
|
+
try {
|
|
7955
|
+
const probe = {
|
|
7956
|
+
id: "probe",
|
|
7957
|
+
key: "glotfile.connection-test",
|
|
7958
|
+
source: "Hello",
|
|
7959
|
+
sourceLocale: "en",
|
|
7960
|
+
targetLocale: "es",
|
|
7961
|
+
placeholders: []
|
|
7962
|
+
};
|
|
7963
|
+
await provider.translate([probe], void 0, controller.signal);
|
|
7964
|
+
return c.json({ ok: true, ...meta });
|
|
7965
|
+
} catch (e) {
|
|
7966
|
+
const error = controller.signal.aborted ? "Connection test timed out after 30s \u2014 the provider didn't respond." : explainProviderError(aiCfg.provider, e);
|
|
7967
|
+
return c.json({ ok: false, ...meta, error });
|
|
7968
|
+
} finally {
|
|
7969
|
+
clearTimeout(timer);
|
|
7970
|
+
}
|
|
7971
|
+
});
|
|
7972
|
+
app.get("/prices", (c) => {
|
|
7973
|
+
const cache2 = loadPriceCache();
|
|
7974
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
7975
|
+
const pricing = resolvePricing(ai, cache2);
|
|
7976
|
+
return c.json({
|
|
7977
|
+
source: cache2?.source ?? null,
|
|
7978
|
+
fetchedAt: cache2?.fetchedAt ?? null,
|
|
7979
|
+
modelCount: cache2 ? Object.keys(cache2.models).length : 0,
|
|
7980
|
+
path: defaultPriceCachePath(),
|
|
7981
|
+
resolved: pricing ? { provider: ai.provider, model: ai.model, ...pricing } : null
|
|
7982
|
+
});
|
|
7983
|
+
});
|
|
7984
|
+
app.get("/prices/list", (c) => {
|
|
7985
|
+
const cache2 = loadPriceCache();
|
|
7986
|
+
const models = cache2 ? Object.entries(cache2.models).map(([id, p]) => ({ id, ...p })).sort((a, b) => a.id.localeCompare(b.id)) : [];
|
|
7987
|
+
return c.json({ source: cache2?.source ?? null, fetchedAt: cache2?.fetchedAt ?? null, models });
|
|
7988
|
+
});
|
|
7989
|
+
app.post("/prices/refresh", async (c) => {
|
|
7990
|
+
try {
|
|
7991
|
+
const res = await refreshPrices();
|
|
7992
|
+
invalidatePriceCache();
|
|
7993
|
+
return c.json({ ok: true, ...res });
|
|
7994
|
+
} catch (e) {
|
|
7995
|
+
return c.json({ error: e.message }, 502);
|
|
7996
|
+
}
|
|
7997
|
+
});
|
|
7422
7998
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
7423
7999
|
app.get("/files", (c) => {
|
|
7424
8000
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -7440,7 +8016,7 @@ function createApi(deps) {
|
|
|
7440
8016
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
7441
8017
|
const abs = resolve9(dir, name);
|
|
7442
8018
|
let filePath = null;
|
|
7443
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
8019
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync14(resolve9(abs, "config.json"))) {
|
|
7444
8020
|
filePath = resolve9(dir, `${name}.json`);
|
|
7445
8021
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
7446
8022
|
filePath = abs;
|
|
@@ -7474,7 +8050,7 @@ function createApi(deps) {
|
|
|
7474
8050
|
const resolved = resolve9(projectRoot, path);
|
|
7475
8051
|
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
7476
8052
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
7477
|
-
if (!
|
|
8053
|
+
if (!existsSync14(resolved)) return c.json({ error: "file not found" }, 400);
|
|
7478
8054
|
loadState(resolved);
|
|
7479
8055
|
deps.statePath = resolved;
|
|
7480
8056
|
watcher.retarget(resolved);
|
|
@@ -7536,9 +8112,9 @@ function createApi(deps) {
|
|
|
7536
8112
|
const abs = resolve9(root, screenshot);
|
|
7537
8113
|
const rel = relative4(root, abs);
|
|
7538
8114
|
const seg0 = rel.split(sep2)[0] ?? "";
|
|
7539
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
8115
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync14(abs)) {
|
|
7540
8116
|
try {
|
|
7541
|
-
|
|
8117
|
+
rmSync7(abs);
|
|
7542
8118
|
} catch {
|
|
7543
8119
|
}
|
|
7544
8120
|
}
|
|
@@ -7783,6 +8359,177 @@ function createApi(deps) {
|
|
|
7783
8359
|
logChange({ kind: "glossary", summary: `Deleted glossary term "${term}"`, before });
|
|
7784
8360
|
return c.json({ ok: true });
|
|
7785
8361
|
});
|
|
8362
|
+
app.get("/glossary/suggestions", (c) => {
|
|
8363
|
+
const s = load();
|
|
8364
|
+
const pending = s.glossarySuggestions.filter((x) => x.status === "pending");
|
|
8365
|
+
return c.json(pending.map((x) => ({
|
|
8366
|
+
...x,
|
|
8367
|
+
occurrences: sourceKeysForTerm(s, x.term, { caseSensitive: x.caseSensitive, wholeWord: x.wholeWord }).length
|
|
8368
|
+
})));
|
|
8369
|
+
});
|
|
8370
|
+
app.post("/glossary/suggestions/dismiss", async (c) => {
|
|
8371
|
+
const { term } = await c.req.json();
|
|
8372
|
+
if (typeof term !== "string") return c.json({ error: "term must be a string" }, 400);
|
|
8373
|
+
const s = load();
|
|
8374
|
+
dismissGlossarySuggestion(s, term);
|
|
8375
|
+
persist(s);
|
|
8376
|
+
logChange({ kind: "glossary", summary: `Dismissed suggested term "${term}"` });
|
|
8377
|
+
return c.json({ ok: true });
|
|
8378
|
+
});
|
|
8379
|
+
app.delete("/glossary/suggestions/:term", (c) => {
|
|
8380
|
+
const s = load();
|
|
8381
|
+
const term = decodeURIComponent(c.req.param("term"));
|
|
8382
|
+
removeGlossarySuggestion(s, term);
|
|
8383
|
+
persist(s);
|
|
8384
|
+
return c.json({ ok: true });
|
|
8385
|
+
});
|
|
8386
|
+
app.post("/glossary/suggest", async (c) => {
|
|
8387
|
+
const signal = c.req.raw.signal;
|
|
8388
|
+
const body = await c.req.json().catch(() => ({}));
|
|
8389
|
+
return streamSSE(c, async (stream) => {
|
|
8390
|
+
const s0 = load();
|
|
8391
|
+
const sources = selectGlossarySources(s0, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
|
|
8392
|
+
if (!sources.length) {
|
|
8393
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ added: 0, terms: [] }) });
|
|
8394
|
+
return;
|
|
8395
|
+
}
|
|
8396
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8397
|
+
let provider;
|
|
8398
|
+
try {
|
|
8399
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
8400
|
+
} catch (e) {
|
|
8401
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
8402
|
+
return;
|
|
8403
|
+
}
|
|
8404
|
+
const known = knownTermList(s0);
|
|
8405
|
+
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: sources.length }) });
|
|
8406
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
8407
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
8408
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
8409
|
+
const chunks = [];
|
|
8410
|
+
for (let i = 0; i < sources.length; i += batchSize) chunks.push(sources.slice(i, i + batchSize));
|
|
8411
|
+
const all = [];
|
|
8412
|
+
let done = 0;
|
|
8413
|
+
let next = 0;
|
|
8414
|
+
async function worker() {
|
|
8415
|
+
while (next < chunks.length) {
|
|
8416
|
+
if (signal?.aborted) break;
|
|
8417
|
+
const chunkRows = chunks[next++];
|
|
8418
|
+
try {
|
|
8419
|
+
const raw = await provider.complete({ system, content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunkRows, known) }], schema: GLOSSARY_SUGGEST_SCHEMA });
|
|
8420
|
+
all.push(...raw.terms ?? []);
|
|
8421
|
+
} catch (e) {
|
|
8422
|
+
void stream.writeSSE({ event: "warn", data: JSON.stringify({ error: e.message }) });
|
|
8423
|
+
}
|
|
8424
|
+
done += chunkRows.length;
|
|
8425
|
+
void stream.writeSSE({ event: "progress", data: JSON.stringify({ done, total: sources.length }) });
|
|
8426
|
+
}
|
|
8427
|
+
}
|
|
8428
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
8429
|
+
if (signal?.aborted) return;
|
|
8430
|
+
const fresh = load();
|
|
8431
|
+
const added = mergeGlossarySuggestions(fresh, dedupeTerms(all));
|
|
8432
|
+
const usage = provider.takeUsage?.();
|
|
8433
|
+
persist(fresh);
|
|
8434
|
+
appendLog(projectRoot, {
|
|
8435
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8436
|
+
kind: "glossary",
|
|
8437
|
+
summary: `Suggested ${added.length} glossary term(s)`,
|
|
8438
|
+
model: aiCfg.model,
|
|
8439
|
+
system,
|
|
8440
|
+
usage,
|
|
8441
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
8442
|
+
});
|
|
8443
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ added: added.length, terms: added }) });
|
|
8444
|
+
});
|
|
8445
|
+
});
|
|
8446
|
+
app.post("/glossary/suggest/estimate", async (c) => {
|
|
8447
|
+
const body = await c.req.json().catch(() => ({}));
|
|
8448
|
+
const s = load();
|
|
8449
|
+
const sources = selectGlossarySources(s, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
|
|
8450
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8451
|
+
return c.json(estimateGlossarySuggest(sources, knownTermList(s), aiCfg));
|
|
8452
|
+
});
|
|
8453
|
+
app.get("/glossary/suggest/batch/status", async (c) => {
|
|
8454
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8455
|
+
let supported = false;
|
|
8456
|
+
let provider;
|
|
8457
|
+
try {
|
|
8458
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
8459
|
+
supported = supportsBatchComplete(provider);
|
|
8460
|
+
} catch {
|
|
8461
|
+
}
|
|
8462
|
+
const pending = loadPendingGlossaryBatch(projectRoot);
|
|
8463
|
+
if (!pending) return c.json({ supported, pending: null });
|
|
8464
|
+
const base = { batchId: pending.batchId, createdAt: pending.createdAt, model: pending.model, total: pending.total };
|
|
8465
|
+
if (!provider || !supportsBatchComplete(provider)) {
|
|
8466
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null } });
|
|
8467
|
+
}
|
|
8468
|
+
try {
|
|
8469
|
+
const status = await provider.translationBatchStatus(pending.batchId);
|
|
8470
|
+
return c.json({ supported, pending: { ...base, status: status.status, counts: status.counts } });
|
|
8471
|
+
} catch (e) {
|
|
8472
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null, error: e.message } });
|
|
8473
|
+
}
|
|
8474
|
+
});
|
|
8475
|
+
app.post("/glossary/suggest/batch", (c) => withTranslateLock(async () => {
|
|
8476
|
+
const body = await c.req.json().catch(() => ({}));
|
|
8477
|
+
const s = load();
|
|
8478
|
+
const sources = selectGlossarySources(s, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
|
|
8479
|
+
if (!sources.length) return c.json({ error: "No source strings to scan." }, 400);
|
|
8480
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8481
|
+
let provider;
|
|
8482
|
+
try {
|
|
8483
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
8484
|
+
} catch (e) {
|
|
8485
|
+
return c.json({ error: e.message }, 400);
|
|
8486
|
+
}
|
|
8487
|
+
if (!supportsBatchComplete(provider)) {
|
|
8488
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
8489
|
+
}
|
|
8490
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
8491
|
+
let pending;
|
|
8492
|
+
try {
|
|
8493
|
+
pending = await submitGlossarySuggestBatch(provider, sources, knownTermList(s), batchSize, aiCfg.model, projectRoot);
|
|
8494
|
+
} catch (e) {
|
|
8495
|
+
return c.json({ error: e.message }, 409);
|
|
8496
|
+
}
|
|
8497
|
+
appendLog(projectRoot, {
|
|
8498
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8499
|
+
kind: "glossary",
|
|
8500
|
+
summary: `Submitted glossary suggestion batch ${pending.batchId} (${pending.total} sources)`,
|
|
8501
|
+
model: aiCfg.model
|
|
8502
|
+
});
|
|
8503
|
+
return c.json({ batchId: pending.batchId, total: pending.total });
|
|
8504
|
+
}));
|
|
8505
|
+
app.post("/glossary/suggest/batch/apply", (c) => withTranslateLock(async () => {
|
|
8506
|
+
const pending = loadPendingGlossaryBatch(projectRoot);
|
|
8507
|
+
if (!pending) return c.json({ error: "No pending glossary suggestion batch." }, 404);
|
|
8508
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8509
|
+
let provider;
|
|
8510
|
+
try {
|
|
8511
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
8512
|
+
} catch (e) {
|
|
8513
|
+
return c.json({ error: e.message }, 400);
|
|
8514
|
+
}
|
|
8515
|
+
if (!supportsBatchComplete(provider)) {
|
|
8516
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
8517
|
+
}
|
|
8518
|
+
const outcome = await applyGlossarySuggestBatchResults(load, persist, provider, pending, projectRoot, aiCfg);
|
|
8519
|
+
return c.json(outcome);
|
|
8520
|
+
}));
|
|
8521
|
+
app.post("/glossary/suggest/batch/cancel", async (c) => {
|
|
8522
|
+
const pending = loadPendingGlossaryBatch(projectRoot);
|
|
8523
|
+
if (!pending) return c.json({ error: "No pending glossary suggestion batch." }, 404);
|
|
8524
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8525
|
+
try {
|
|
8526
|
+
const provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
8527
|
+
if (supportsBatchComplete(provider)) await provider.cancelTranslationBatch(pending.batchId);
|
|
8528
|
+
} catch {
|
|
8529
|
+
}
|
|
8530
|
+
clearPendingGlossaryBatch(projectRoot);
|
|
8531
|
+
return c.json({ canceled: pending.batchId });
|
|
8532
|
+
});
|
|
7786
8533
|
app.post("/keys/:key/screenshot", async (c) => {
|
|
7787
8534
|
const key = c.req.param("key");
|
|
7788
8535
|
const body = await c.req.parseBody();
|
|
@@ -7968,7 +8715,7 @@ function createApi(deps) {
|
|
|
7968
8715
|
try {
|
|
7969
8716
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7970
8717
|
} catch (e) {
|
|
7971
|
-
await stream.writeSSE({ event: "error", data: JSON.stringify({ error:
|
|
8718
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: explainProviderError(aiCfg.provider, e) }) });
|
|
7972
8719
|
return;
|
|
7973
8720
|
}
|
|
7974
8721
|
const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
@@ -7985,58 +8732,65 @@ function createApi(deps) {
|
|
|
7985
8732
|
event: "start",
|
|
7986
8733
|
data: JSON.stringify({ total: reqs.length, locales: [...localeTotals].map(([locale, total]) => ({ locale, total })) })
|
|
7987
8734
|
});
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8735
|
+
try {
|
|
8736
|
+
await runLocaleParallel(reqs, provider, {
|
|
8737
|
+
// Announce a language the moment a worker picks it up — this is the
|
|
8738
|
+
// signal that "something is happening" during the long first LLM call.
|
|
8739
|
+
onLocaleStart: (locale) => {
|
|
8740
|
+
void stream.writeSSE({ event: "locale-start", data: JSON.stringify({ locale }) });
|
|
8741
|
+
},
|
|
8742
|
+
onBatchComplete: (done, total, batchResults, locale) => {
|
|
8743
|
+
const fresh = load();
|
|
8744
|
+
const { written, errors } = applyResults(fresh, reqs, batchResults);
|
|
8745
|
+
persist(fresh);
|
|
8746
|
+
totalWritten += written;
|
|
8747
|
+
allErrors.push(...errors);
|
|
8748
|
+
const usage = provider.takeUsage?.();
|
|
8749
|
+
appendLog(projectRoot, {
|
|
8750
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8751
|
+
kind: "translate",
|
|
8752
|
+
summary: `Translated ${batchResults.length} item(s)`,
|
|
8753
|
+
model: aiCfg.model,
|
|
8754
|
+
system,
|
|
8755
|
+
items: batchResults.map((r) => {
|
|
8756
|
+
const req = reqById.get(r.id);
|
|
8757
|
+
return { id: r.id, key: req?.key ?? "", source: req?.source ?? "", targetLocale: req?.targetLocale, context: req?.context, glossary: req?.glossary, screenshot: req ? fresh.keys[req.key]?.screenshot : void 0 };
|
|
8758
|
+
}),
|
|
8759
|
+
results: batchResults,
|
|
8760
|
+
usage,
|
|
8761
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
8762
|
+
});
|
|
8763
|
+
const ld = (localeDone.get(locale) ?? 0) + batchResults.length;
|
|
8764
|
+
localeDone.set(locale, ld);
|
|
8765
|
+
console.log(`[translate] ${done}/${total}`);
|
|
8766
|
+
void stream.writeSSE({
|
|
8767
|
+
event: "progress",
|
|
8768
|
+
data: JSON.stringify({ done, total, written: totalWritten, errors, locale, localeDone: ld, localeTotal: localeTotals.get(locale) ?? 0 })
|
|
8769
|
+
});
|
|
8770
|
+
},
|
|
8771
|
+
onLocaleDone: (locale) => {
|
|
8772
|
+
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
8773
|
+
},
|
|
8774
|
+
// Record the raw reply so an unparseable model response is diagnosable
|
|
8775
|
+
// from the activity log instead of vanishing into per-item errors.
|
|
8776
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
8777
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
8778
|
+
appendLog(projectRoot, {
|
|
8779
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8780
|
+
kind: "translate",
|
|
8781
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
8782
|
+
model: aiCfg.model,
|
|
8783
|
+
locale,
|
|
8784
|
+
raw
|
|
8785
|
+
});
|
|
8786
|
+
}
|
|
8787
|
+
}, aiCfg.concurrency, signal, aiCfg.batchSize);
|
|
8788
|
+
} catch (e) {
|
|
8789
|
+
if (!signal?.aborted) {
|
|
8790
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: explainProviderError(aiCfg.provider, e) }) });
|
|
8038
8791
|
}
|
|
8039
|
-
|
|
8792
|
+
return;
|
|
8793
|
+
}
|
|
8040
8794
|
if (!signal?.aborted) {
|
|
8041
8795
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
8042
8796
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -8063,23 +8817,28 @@ function createApi(deps) {
|
|
|
8063
8817
|
try {
|
|
8064
8818
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
8065
8819
|
} catch (e) {
|
|
8066
|
-
return c.json({ error:
|
|
8820
|
+
return c.json({ error: explainProviderError(aiCfg.provider, e) }, 400);
|
|
8067
8821
|
}
|
|
8068
8822
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
8069
8823
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8824
|
+
let results;
|
|
8825
|
+
try {
|
|
8826
|
+
results = await runLocaleParallel(toTranslate, provider, {
|
|
8827
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
8828
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
8829
|
+
appendLog(projectRoot, {
|
|
8830
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8831
|
+
kind: "translate",
|
|
8832
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
8833
|
+
model: aiCfg.model,
|
|
8834
|
+
locale,
|
|
8835
|
+
raw
|
|
8836
|
+
});
|
|
8837
|
+
}
|
|
8838
|
+
}, aiCfg.concurrency, void 0, aiCfg.batchSize);
|
|
8839
|
+
} catch (e) {
|
|
8840
|
+
return c.json({ error: explainProviderError(aiCfg.provider, e) }, 502);
|
|
8841
|
+
}
|
|
8083
8842
|
const latest = load();
|
|
8084
8843
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
8085
8844
|
const usage = provider.takeUsage?.();
|
|
@@ -8350,6 +9109,22 @@ function createApi(deps) {
|
|
|
8350
9109
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
8351
9110
|
});
|
|
8352
9111
|
});
|
|
9112
|
+
app.post("/context/estimate", async (c) => {
|
|
9113
|
+
const body = await c.req.json().catch(() => ({}));
|
|
9114
|
+
const cache2 = loadUsageCache(projectRoot);
|
|
9115
|
+
if (!cache2) return c.json({ error: "No usage index found. Run 'glotfile scan' first." }, 400);
|
|
9116
|
+
const targets = selectContextTargets(load(), {
|
|
9117
|
+
all: body.all,
|
|
9118
|
+
keyGlob: body.keyGlob,
|
|
9119
|
+
limit: body.limit,
|
|
9120
|
+
since: body.since,
|
|
9121
|
+
keys: body.keys,
|
|
9122
|
+
force: body.force
|
|
9123
|
+
}, cache2, body.lastRunAt);
|
|
9124
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
9125
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
9126
|
+
return c.json(estimateContext(targets, aiCfg));
|
|
9127
|
+
});
|
|
8353
9128
|
app.get("/context/batch/status", async (c) => {
|
|
8354
9129
|
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8355
9130
|
let supported = false;
|
|
@@ -8454,6 +9229,8 @@ var init_api = __esm({
|
|
|
8454
9229
|
"src/server/api.ts"() {
|
|
8455
9230
|
"use strict";
|
|
8456
9231
|
init_state();
|
|
9232
|
+
init_glossary_suggest();
|
|
9233
|
+
init_glossary();
|
|
8457
9234
|
init_accept();
|
|
8458
9235
|
init_scan();
|
|
8459
9236
|
init_scanner();
|
|
@@ -8467,12 +9244,17 @@ var init_api = __esm({
|
|
|
8467
9244
|
init_ai();
|
|
8468
9245
|
init_run();
|
|
8469
9246
|
init_provider();
|
|
9247
|
+
init_explain_error();
|
|
8470
9248
|
init_batch_run();
|
|
8471
9249
|
init_pending_batch();
|
|
8472
9250
|
init_context_batch_run();
|
|
8473
9251
|
init_pending_context_batch();
|
|
9252
|
+
init_glossary_batch_run();
|
|
9253
|
+
init_pending_glossary_batch();
|
|
8474
9254
|
init_estimate();
|
|
8475
9255
|
init_pricing();
|
|
9256
|
+
init_price_fetch();
|
|
9257
|
+
init_price_cache();
|
|
8476
9258
|
init_log();
|
|
8477
9259
|
init_schema();
|
|
8478
9260
|
init_run3();
|
|
@@ -8497,7 +9279,7 @@ __export(server_exports, {
|
|
|
8497
9279
|
import { Hono as Hono2 } from "hono";
|
|
8498
9280
|
import { serve } from "@hono/node-server";
|
|
8499
9281
|
import { fileURLToPath } from "url";
|
|
8500
|
-
import { dirname as dirname4, join as
|
|
9282
|
+
import { dirname as dirname4, join as join21, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
8501
9283
|
import { readFile, stat } from "fs/promises";
|
|
8502
9284
|
import { createServer } from "net";
|
|
8503
9285
|
import open from "open";
|
|
@@ -8553,7 +9335,7 @@ function buildApp(opts) {
|
|
|
8553
9335
|
const file = await readFileResponse(target);
|
|
8554
9336
|
if (file) return file;
|
|
8555
9337
|
}
|
|
8556
|
-
const index = await readFileResponse(
|
|
9338
|
+
const index = await readFileResponse(join21(root, "index.html"));
|
|
8557
9339
|
if (index) return index;
|
|
8558
9340
|
return c.notFound();
|
|
8559
9341
|
});
|
|
@@ -8623,7 +9405,7 @@ var init_server = __esm({
|
|
|
8623
9405
|
init_scanner();
|
|
8624
9406
|
init_usage();
|
|
8625
9407
|
here = dirname4(fileURLToPath(import.meta.url));
|
|
8626
|
-
DEFAULT_UI_DIR =
|
|
9408
|
+
DEFAULT_UI_DIR = join21(here, "..", "ui");
|
|
8627
9409
|
MIME = {
|
|
8628
9410
|
".html": "text/html; charset=utf-8",
|
|
8629
9411
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -8655,8 +9437,8 @@ var init_server = __esm({
|
|
|
8655
9437
|
// src/server/cli.ts
|
|
8656
9438
|
init_state();
|
|
8657
9439
|
init_stats();
|
|
8658
|
-
import { resolve as resolve11, dirname as dirname5, join as
|
|
8659
|
-
import { readFileSync as
|
|
9440
|
+
import { resolve as resolve11, dirname as dirname5, join as join22, basename as basename2 } from "path";
|
|
9441
|
+
import { readFileSync as readFileSync27, existsSync as existsSync15, mkdirSync as mkdirSync7, cpSync } from "fs";
|
|
8660
9442
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8661
9443
|
|
|
8662
9444
|
// src/server/agent-cli.ts
|
|
@@ -8787,14 +9569,20 @@ init_batch_run();
|
|
|
8787
9569
|
init_pending_batch();
|
|
8788
9570
|
init_context_batch_run();
|
|
8789
9571
|
init_pending_context_batch();
|
|
9572
|
+
init_glossary_batch_run();
|
|
9573
|
+
init_pending_glossary_batch();
|
|
8790
9574
|
init_estimate();
|
|
9575
|
+
init_glossary_suggest();
|
|
8791
9576
|
init_pricing();
|
|
9577
|
+
init_price_fetch();
|
|
9578
|
+
init_price_cache();
|
|
8792
9579
|
init_log();
|
|
8793
9580
|
init_scan();
|
|
8794
9581
|
init_scanner();
|
|
8795
9582
|
init_usage();
|
|
8796
9583
|
init_context();
|
|
8797
9584
|
init_run2();
|
|
9585
|
+
init_registry();
|
|
8798
9586
|
init_outputs();
|
|
8799
9587
|
|
|
8800
9588
|
// src/server/lint/locate.ts
|
|
@@ -8871,7 +9659,7 @@ function formatSarif(report, ctx) {
|
|
|
8871
9659
|
}
|
|
8872
9660
|
|
|
8873
9661
|
// src/server/cli.ts
|
|
8874
|
-
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "sync", "build-context", "scan", "prune", "split", "skill", "batch", "get", "stats", "set", "set-state", "clear", "apply"];
|
|
9662
|
+
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "sync", "build-context", "suggest-glossary", "scan", "prune", "split", "skill", "batch", "prices", "get", "stats", "set", "set-state", "clear", "apply"];
|
|
8875
9663
|
var isCommand = (s) => s != null && COMMANDS.includes(s);
|
|
8876
9664
|
function parseArgs(argv) {
|
|
8877
9665
|
const statePath = resolve11(process.cwd(), "glotfile.json");
|
|
@@ -8947,6 +9735,7 @@ function parseArgs(argv) {
|
|
|
8947
9735
|
else if (flag === "--batch") args.batch = true;
|
|
8948
9736
|
else if (flag === "--wait") args.wait = true;
|
|
8949
9737
|
else if (flag === "--print") args.print = true;
|
|
9738
|
+
else if (flag === "--refresh") args.refresh = true;
|
|
8950
9739
|
else if (flag === "--state" && next) {
|
|
8951
9740
|
args.states = next.split(",");
|
|
8952
9741
|
i++;
|
|
@@ -9042,7 +9831,7 @@ function translateSelection(args) {
|
|
|
9042
9831
|
}
|
|
9043
9832
|
function readStdin() {
|
|
9044
9833
|
try {
|
|
9045
|
-
return
|
|
9834
|
+
return readFileSync27(0, "utf8");
|
|
9046
9835
|
} catch {
|
|
9047
9836
|
return "";
|
|
9048
9837
|
}
|
|
@@ -9215,13 +10004,15 @@ async function runBatch(args) {
|
|
|
9215
10004
|
const projectRoot = dirname5(resolve11(args.statePath));
|
|
9216
10005
|
const pending = loadPendingBatch(projectRoot);
|
|
9217
10006
|
const ctxPending = loadPendingContextBatch(projectRoot);
|
|
9218
|
-
|
|
9219
|
-
|
|
10007
|
+
const glossPending = loadPendingGlossaryBatch(projectRoot);
|
|
10008
|
+
if (!pending && !ctxPending && !glossPending) {
|
|
10009
|
+
console.log("No pending batch. Start one with `glotfile translate --batch`, `glotfile build-context --batch`, or `glotfile suggest-glossary --batch`.");
|
|
9220
10010
|
return;
|
|
9221
10011
|
}
|
|
9222
10012
|
const action = args.batchAction ?? "status";
|
|
9223
10013
|
if (pending) await runTranslationBatchAction(args, pending, action, projectRoot);
|
|
9224
10014
|
if (ctxPending) await runContextBatchAction(args, ctxPending, action, projectRoot);
|
|
10015
|
+
if (glossPending) await runGlossaryBatchAction(args, glossPending, action, projectRoot);
|
|
9225
10016
|
}
|
|
9226
10017
|
async function runTranslationBatchAction(args, pending, action, projectRoot) {
|
|
9227
10018
|
if (action === "cancel") {
|
|
@@ -9307,18 +10098,65 @@ async function runContextBatchAction(args, pending, action, projectRoot) {
|
|
|
9307
10098
|
if (outcome.retried) console.log(`${outcome.retried} job(s) re-run synchronously (batch entries failed or were malformed).`);
|
|
9308
10099
|
for (const e of outcome.errors) console.warn(`skip ${e.key}: ${e.error}`);
|
|
9309
10100
|
}
|
|
10101
|
+
async function runGlossaryBatchAction(args, pending, action, projectRoot) {
|
|
10102
|
+
if (action === "cancel") {
|
|
10103
|
+
let remoteFailed = false;
|
|
10104
|
+
try {
|
|
10105
|
+
const ai2 = loadLocalSettings(projectRoot).ai;
|
|
10106
|
+
const provider2 = makeProvider(ai2);
|
|
10107
|
+
if (supportsBatchComplete(provider2)) {
|
|
10108
|
+
await provider2.cancelTranslationBatch(pending.batchId);
|
|
10109
|
+
} else {
|
|
10110
|
+
remoteFailed = true;
|
|
10111
|
+
}
|
|
10112
|
+
} catch {
|
|
10113
|
+
remoteFailed = true;
|
|
10114
|
+
}
|
|
10115
|
+
clearPendingGlossaryBatch(projectRoot);
|
|
10116
|
+
const suffix = remoteFailed ? " (remote cancel failed \u2014 it will expire server-side)" : "";
|
|
10117
|
+
console.log(`Canceled glossary suggestion batch ${pending.batchId}.${suffix}`);
|
|
10118
|
+
return;
|
|
10119
|
+
}
|
|
10120
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
10121
|
+
const provider = makeProviderOrExit(ai);
|
|
10122
|
+
if (!provider) return;
|
|
10123
|
+
if (!supportsBatchComplete(provider)) {
|
|
10124
|
+
console.error(`Pending glossary batch was submitted via anthropic, but the configured provider "${ai.provider}" has no batch support.`);
|
|
10125
|
+
process.exitCode = 1;
|
|
10126
|
+
return;
|
|
10127
|
+
}
|
|
10128
|
+
const status = await provider.translationBatchStatus(pending.batchId);
|
|
10129
|
+
const c = status.counts;
|
|
10130
|
+
console.log(`Glossary suggestion batch ${pending.batchId} (${pending.total} source(s), submitted ${pending.createdAt})`);
|
|
10131
|
+
console.log(` ${status.status} \u2014 ${c.succeeded} succeeded, ${c.processing} processing, ${c.errored} errored, ${c.expired} expired, ${c.canceled} canceled`);
|
|
10132
|
+
if (status.status !== "ended") {
|
|
10133
|
+
if (action === "apply") console.log("Not finished yet \u2014 try again later.");
|
|
10134
|
+
return;
|
|
10135
|
+
}
|
|
10136
|
+
const outcome = await applyGlossarySuggestBatchResults(
|
|
10137
|
+
() => loadState(args.statePath),
|
|
10138
|
+
(s) => saveState(args.statePath, s),
|
|
10139
|
+
provider,
|
|
10140
|
+
pending,
|
|
10141
|
+
projectRoot,
|
|
10142
|
+
ai
|
|
10143
|
+
);
|
|
10144
|
+
console.log(`Found ${outcome.added} new candidate term(s).`);
|
|
10145
|
+
if (outcome.retried) console.log(`${outcome.retried} job(s) re-run synchronously (batch entries failed or were malformed).`);
|
|
10146
|
+
for (const e of outcome.errors) console.warn(`batch job failed: ${e.error}`);
|
|
10147
|
+
}
|
|
9310
10148
|
function sarifContextFor(statePath) {
|
|
9311
10149
|
if (detectFormat(statePath) === "split") {
|
|
9312
10150
|
const dir = splitDirFor(statePath);
|
|
9313
|
-
const keysPath =
|
|
10151
|
+
const keysPath = join22(dir, "keys.json");
|
|
9314
10152
|
return {
|
|
9315
10153
|
keysUri: `${basename2(dir)}/keys.json`,
|
|
9316
|
-
keysRawText:
|
|
10154
|
+
keysRawText: existsSync15(keysPath) ? readFileSync27(keysPath, "utf8") : ""
|
|
9317
10155
|
};
|
|
9318
10156
|
}
|
|
9319
10157
|
return {
|
|
9320
10158
|
keysUri: basename2(statePath),
|
|
9321
|
-
keysRawText:
|
|
10159
|
+
keysRawText: existsSync15(statePath) ? readFileSync27(statePath, "utf8") : ""
|
|
9322
10160
|
};
|
|
9323
10161
|
}
|
|
9324
10162
|
function printReport(report, format, statePath) {
|
|
@@ -9327,6 +10165,18 @@ function printReport(report, format, statePath) {
|
|
|
9327
10165
|
else console.log(formatText(report).trimEnd());
|
|
9328
10166
|
}
|
|
9329
10167
|
async function runLintCmd(args) {
|
|
10168
|
+
if (args.ruleIds) {
|
|
10169
|
+
const unknown = unknownRuleIds(args.ruleIds);
|
|
10170
|
+
if (unknown.length > 0) {
|
|
10171
|
+
for (const id of unknown) {
|
|
10172
|
+
const hint = suggestRuleId(id);
|
|
10173
|
+
console.error(`Unknown --rule '${id}'.${hint ? ` Did you mean '${hint}'?` : ""}`);
|
|
10174
|
+
}
|
|
10175
|
+
console.error(`Valid rules: ${RULE_IDS.join(", ")}.`);
|
|
10176
|
+
process.exitCode = 1;
|
|
10177
|
+
return;
|
|
10178
|
+
}
|
|
10179
|
+
}
|
|
9330
10180
|
const state = loadState(args.statePath);
|
|
9331
10181
|
if (args.accept) {
|
|
9332
10182
|
const { acceptFindings: acceptFindings2 } = await Promise.resolve().then(() => (init_accept(), accept_exports));
|
|
@@ -9375,7 +10225,7 @@ async function runImportCmd(args) {
|
|
|
9375
10225
|
const { runImport: runImport2 } = await Promise.resolve().then(() => (init_run3(), run_exports));
|
|
9376
10226
|
const projectRoot = args.importSource ? resolve11(args.importSource) : dirname5(resolve11(args.statePath));
|
|
9377
10227
|
const out = resolve11(projectRoot, "glotfile.json");
|
|
9378
|
-
if (
|
|
10228
|
+
if (existsSync15(out) && !args.importForce) {
|
|
9379
10229
|
console.error(`${out} already exists; pass --force to overwrite`);
|
|
9380
10230
|
process.exitCode = 1;
|
|
9381
10231
|
return;
|
|
@@ -9468,29 +10318,30 @@ async function runBuildContext(args) {
|
|
|
9468
10318
|
console.log("No keys need context.");
|
|
9469
10319
|
return;
|
|
9470
10320
|
}
|
|
10321
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
10322
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
10323
|
+
if (args.estimate) {
|
|
10324
|
+
const est = estimateContext(targets, aiCfg);
|
|
10325
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
10326
|
+
console.log(`Estimate for ${fmt(est.keys)} key(s) in ${fmt(est.batches)} batch(es) \u2014 ${aiCfg.provider} \xB7 ${aiCfg.model}`);
|
|
10327
|
+
console.log(`Totals: ~${fmt(est.inputTokens)} input / ~${fmt(est.outputTokens)} output tokens`);
|
|
10328
|
+
if (est.pricing) {
|
|
10329
|
+
const cost = est.estimatedCost;
|
|
10330
|
+
console.log(`Estimated cost: ~$${cost >= 0.1 ? cost.toFixed(2) : cost.toFixed(4)} (\xB120%, ${est.pricing.source} pricing $${est.pricing.inputPerMTok}/$${est.pricing.outputPerMTok} per MTok)`);
|
|
10331
|
+
} else {
|
|
10332
|
+
console.log("No pricing known for this model \u2014 set inputPricePerMTok/outputPricePerMTok in your AI settings for a dollar estimate.");
|
|
10333
|
+
}
|
|
10334
|
+
return;
|
|
10335
|
+
}
|
|
9471
10336
|
let provider;
|
|
9472
10337
|
try {
|
|
9473
|
-
provider = makeProvider(
|
|
10338
|
+
provider = makeProvider(aiCfg);
|
|
9474
10339
|
} catch (e) {
|
|
9475
10340
|
console.error(e.message);
|
|
9476
10341
|
process.exitCode = 1;
|
|
9477
10342
|
return;
|
|
9478
10343
|
}
|
|
9479
|
-
const fileCache = /* @__PURE__ */ new Map();
|
|
9480
|
-
for (const target of targets) {
|
|
9481
|
-
const refs = Object.values(cache2.files).flatMap(
|
|
9482
|
-
(f) => f.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
9483
|
-
key: r.key,
|
|
9484
|
-
file: Object.keys(cache2.files).find((path) => cache2.files[path]?.refs.includes(r)) ?? "",
|
|
9485
|
-
line: r.line,
|
|
9486
|
-
col: r.col,
|
|
9487
|
-
scanner: r.scanner
|
|
9488
|
-
}))
|
|
9489
|
-
);
|
|
9490
|
-
target.usageSnippets = extractSnippets(refs, projectRoot, fileCache);
|
|
9491
|
-
}
|
|
9492
10344
|
const system = buildContextSystemPrompt();
|
|
9493
|
-
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
9494
10345
|
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
9495
10346
|
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
9496
10347
|
if (args.batch) {
|
|
@@ -9546,6 +10397,96 @@ async function runBuildContext(args) {
|
|
|
9546
10397
|
console.log(`Wrote context for ${written} key(s).`);
|
|
9547
10398
|
for (const e of errors) console.warn(`skip ${e.key}: ${e.error}`);
|
|
9548
10399
|
}
|
|
10400
|
+
async function runSuggestGlossary(args) {
|
|
10401
|
+
const state = loadState(args.statePath);
|
|
10402
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
10403
|
+
const sources = selectGlossarySources(state, { keyGlob: args.keyGlob, limit: args.limit, since: args.since });
|
|
10404
|
+
if (!sources.length) {
|
|
10405
|
+
console.log("No source strings to scan.");
|
|
10406
|
+
return;
|
|
10407
|
+
}
|
|
10408
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
10409
|
+
const known = knownTermList(state);
|
|
10410
|
+
if (args.estimate) {
|
|
10411
|
+
const est = estimateGlossarySuggest(sources, known, aiCfg);
|
|
10412
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
10413
|
+
console.log(`Estimate for ${fmt(est.sources)} source string(s) in ${fmt(est.batches)} batch(es) \u2014 ${aiCfg.provider} \xB7 ${aiCfg.model}`);
|
|
10414
|
+
console.log(`Totals: ~${fmt(est.inputTokens)} input / ~${fmt(est.outputTokens)} output tokens`);
|
|
10415
|
+
if (est.pricing) {
|
|
10416
|
+
const cost = est.estimatedCost;
|
|
10417
|
+
console.log(`Estimated cost: ~$${cost >= 0.1 ? cost.toFixed(2) : cost.toFixed(4)} (\xB120%, ${est.pricing.source} pricing $${est.pricing.inputPerMTok}/$${est.pricing.outputPerMTok} per MTok)`);
|
|
10418
|
+
} else {
|
|
10419
|
+
console.log("No pricing known for this model \u2014 set inputPricePerMTok/outputPricePerMTok in your AI settings for a dollar estimate.");
|
|
10420
|
+
}
|
|
10421
|
+
return;
|
|
10422
|
+
}
|
|
10423
|
+
let provider;
|
|
10424
|
+
try {
|
|
10425
|
+
provider = makeProvider(aiCfg);
|
|
10426
|
+
} catch (e) {
|
|
10427
|
+
console.error(e.message);
|
|
10428
|
+
process.exitCode = 1;
|
|
10429
|
+
return;
|
|
10430
|
+
}
|
|
10431
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
10432
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
10433
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
10434
|
+
if (args.batch) {
|
|
10435
|
+
if (!supportsBatchComplete(provider)) {
|
|
10436
|
+
console.error(`Provider "${aiCfg.provider}" does not support batch mode. Currently anthropic only.`);
|
|
10437
|
+
process.exitCode = 1;
|
|
10438
|
+
return;
|
|
10439
|
+
}
|
|
10440
|
+
let pending;
|
|
10441
|
+
try {
|
|
10442
|
+
pending = await submitGlossarySuggestBatch(provider, sources, known, batchSize, aiCfg.model, projectRoot);
|
|
10443
|
+
} catch (e) {
|
|
10444
|
+
console.error(e.message);
|
|
10445
|
+
process.exitCode = 1;
|
|
10446
|
+
return;
|
|
10447
|
+
}
|
|
10448
|
+
appendLog(projectRoot, {
|
|
10449
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10450
|
+
kind: "glossary",
|
|
10451
|
+
summary: `Submitted glossary suggestion batch ${pending.batchId} (${pending.total} sources)`,
|
|
10452
|
+
model: aiCfg.model,
|
|
10453
|
+
system
|
|
10454
|
+
});
|
|
10455
|
+
console.log(`Submitted glossary suggestion batch ${pending.batchId} \u2014 ${pending.total} source string(s) at 50% batch pricing.`);
|
|
10456
|
+
console.log("Check progress with `glotfile batch`; it applies results automatically when finished.");
|
|
10457
|
+
return;
|
|
10458
|
+
}
|
|
10459
|
+
const chunks = [];
|
|
10460
|
+
for (let i = 0; i < sources.length; i += batchSize) chunks.push(sources.slice(i, i + batchSize));
|
|
10461
|
+
const all = [];
|
|
10462
|
+
let done = 0;
|
|
10463
|
+
let next = 0;
|
|
10464
|
+
async function worker() {
|
|
10465
|
+
while (next < chunks.length) {
|
|
10466
|
+
const chunkRows = chunks[next++];
|
|
10467
|
+
try {
|
|
10468
|
+
const raw = await provider.complete({ system, content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunkRows, known) }], schema: GLOSSARY_SUGGEST_SCHEMA });
|
|
10469
|
+
const batch = raw;
|
|
10470
|
+
all.push(...batch.terms ?? []);
|
|
10471
|
+
} catch (e) {
|
|
10472
|
+
console.warn(`batch failed: ${e.message}`);
|
|
10473
|
+
}
|
|
10474
|
+
done += chunkRows.length;
|
|
10475
|
+
console.log(`[${done}/${sources.length}] scanned`);
|
|
10476
|
+
}
|
|
10477
|
+
}
|
|
10478
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
10479
|
+
const added = mergeGlossarySuggestions(state, dedupeTerms(all));
|
|
10480
|
+
saveState(args.statePath, state);
|
|
10481
|
+
appendLog(projectRoot, {
|
|
10482
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10483
|
+
kind: "glossary",
|
|
10484
|
+
summary: `Suggested ${added.length} glossary term(s)`,
|
|
10485
|
+
model: aiCfg.model
|
|
10486
|
+
});
|
|
10487
|
+
console.log(`Found ${added.length} new candidate term(s). Review them in the glossary UI.`);
|
|
10488
|
+
for (const s of added) console.log(` \u2022 ${s.term}${s.note ? ` \u2014 ${s.note}` : ""}`);
|
|
10489
|
+
}
|
|
9549
10490
|
async function runScanCmd(args) {
|
|
9550
10491
|
const state = loadState(args.statePath);
|
|
9551
10492
|
const projectRoot = dirname5(resolve11(args.statePath));
|
|
@@ -9619,19 +10560,19 @@ function runSplit(args) {
|
|
|
9619
10560
|
`Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
|
|
9620
10561
|
);
|
|
9621
10562
|
}
|
|
9622
|
-
var SKILL_SRC =
|
|
10563
|
+
var SKILL_SRC = join22(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
|
|
9623
10564
|
function runSkill(args) {
|
|
9624
10565
|
if (args.print) {
|
|
9625
|
-
console.log(
|
|
10566
|
+
console.log(readFileSync27(join22(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
|
|
9626
10567
|
return;
|
|
9627
10568
|
}
|
|
9628
10569
|
const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
|
|
9629
|
-
if (
|
|
10570
|
+
if (existsSync15(dest) && !args.importForce) {
|
|
9630
10571
|
console.error(`${dest} already exists; pass --force to overwrite`);
|
|
9631
10572
|
process.exitCode = 1;
|
|
9632
10573
|
return;
|
|
9633
10574
|
}
|
|
9634
|
-
|
|
10575
|
+
mkdirSync7(dirname5(dest), { recursive: true });
|
|
9635
10576
|
cpSync(SKILL_SRC, dest, { recursive: true });
|
|
9636
10577
|
console.log(`Installed the glotfile skill to ${dest}. Restart Claude Code to pick it up.`);
|
|
9637
10578
|
}
|
|
@@ -9801,6 +10742,39 @@ function runApply(args) {
|
|
|
9801
10742
|
console.log(JSON.stringify({ applied: r.applied, keysTouched: r.keysTouched, saved, dryRun: !!args.dryRun, errors: r.errors }, null, 2));
|
|
9802
10743
|
if (r.errors.length) process.exitCode = 1;
|
|
9803
10744
|
}
|
|
10745
|
+
async function runPrices(args) {
|
|
10746
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
10747
|
+
if (args.refresh) {
|
|
10748
|
+
try {
|
|
10749
|
+
const res = await refreshPrices();
|
|
10750
|
+
invalidatePriceCache();
|
|
10751
|
+
console.log(`Updated ${res.modelCount} model price(s) from ${res.source}.`);
|
|
10752
|
+
console.log(`Fetched ${new Date(res.fetchedAt).toLocaleString()} \u2192 ${res.path}`);
|
|
10753
|
+
} catch (e) {
|
|
10754
|
+
console.error(`Could not refresh prices: ${e.message}`);
|
|
10755
|
+
console.error("Existing cached prices (if any) are unchanged.");
|
|
10756
|
+
process.exitCode = 1;
|
|
10757
|
+
}
|
|
10758
|
+
return;
|
|
10759
|
+
}
|
|
10760
|
+
const cache2 = loadPriceCache();
|
|
10761
|
+
if (cache2) {
|
|
10762
|
+
const when = cache2.fetchedAt ? new Date(cache2.fetchedAt).toLocaleString() : "unknown time";
|
|
10763
|
+
console.log(`Price cache: ${Object.keys(cache2.models).length} model(s) from ${cache2.source}, fetched ${when}.`);
|
|
10764
|
+
console.log(`Location: ${defaultPriceCachePath()}`);
|
|
10765
|
+
} else {
|
|
10766
|
+
console.log("No price cache yet. Run `glotfile prices --refresh` to fetch the latest from models.dev.");
|
|
10767
|
+
}
|
|
10768
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
10769
|
+
const pricing = resolvePricing(aiCfg, cache2);
|
|
10770
|
+
if (pricing) {
|
|
10771
|
+
console.log(`
|
|
10772
|
+
${aiCfg.provider} \xB7 ${aiCfg.model}: $${pricing.inputPerMTok}/$${pricing.outputPerMTok} per MTok (${pricing.source}).`);
|
|
10773
|
+
} else {
|
|
10774
|
+
console.log(`
|
|
10775
|
+
No price known for ${aiCfg.provider} \xB7 ${aiCfg.model}. Set inputPricePerMTok/outputPricePerMTok in AI settings, or refresh.`);
|
|
10776
|
+
}
|
|
10777
|
+
}
|
|
9804
10778
|
var GLOBAL_OPTS = [
|
|
9805
10779
|
["-f, --file <path>", "State file to use (default: ./glotfile.json)"],
|
|
9806
10780
|
["-h, --help", "Show this help"]
|
|
@@ -9879,12 +10853,25 @@ var COMMAND_HELP = {
|
|
|
9879
10853
|
},
|
|
9880
10854
|
"build-context": {
|
|
9881
10855
|
summary: "AI-generate per-key context to improve translation (requires a prior scan).",
|
|
9882
|
-
usage: "glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>] [--batch]",
|
|
10856
|
+
usage: "glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>] [--estimate] [--batch]",
|
|
9883
10857
|
options: [
|
|
9884
10858
|
["--all", "(Re)build context for every key, not just those missing it"],
|
|
9885
10859
|
["--key <glob>", "Only keys matching this glob"],
|
|
9886
10860
|
["--limit <n>", "Process at most n keys"],
|
|
9887
|
-
["--since <date>", "Only keys added or changed since this date"]
|
|
10861
|
+
["--since <date>", "Only keys added or changed since this date"],
|
|
10862
|
+
["--estimate", "Print batches, tokens and estimated cost without building"],
|
|
10863
|
+
["--batch", "Submit via the provider's batch API (50% cost, async; anthropic only)"]
|
|
10864
|
+
]
|
|
10865
|
+
},
|
|
10866
|
+
"suggest-glossary": {
|
|
10867
|
+
summary: "AI-scan source strings for candidate glossary terms (adds a review queue; existing terms are skipped).",
|
|
10868
|
+
usage: "glotfile suggest-glossary [--key <glob>] [--limit <n>] [--since <date>] [--estimate] [--batch]",
|
|
10869
|
+
options: [
|
|
10870
|
+
["--key <glob>", "Only scan keys matching this glob"],
|
|
10871
|
+
["--limit <n>", "Scan at most n source strings"],
|
|
10872
|
+
["--since <date>", "Only keys added since this date"],
|
|
10873
|
+
["--estimate", "Print batches, tokens and estimated cost without scanning"],
|
|
10874
|
+
["--batch", "Submit via the provider's batch API (50% cost, async; anthropic only)"]
|
|
9888
10875
|
]
|
|
9889
10876
|
},
|
|
9890
10877
|
scan: {
|
|
@@ -9923,6 +10910,13 @@ var COMMAND_HELP = {
|
|
|
9923
10910
|
["cancel", "Cancel the pending batch and discard the handle"]
|
|
9924
10911
|
]
|
|
9925
10912
|
},
|
|
10913
|
+
prices: {
|
|
10914
|
+
summary: "Show or refresh the model price cache used for cost estimates (models.dev).",
|
|
10915
|
+
usage: "glotfile prices [--refresh]",
|
|
10916
|
+
options: [
|
|
10917
|
+
["--refresh", "Fetch the latest prices from models.dev into the cache (the only command that hits the network)"]
|
|
10918
|
+
]
|
|
10919
|
+
},
|
|
9926
10920
|
get: {
|
|
9927
10921
|
summary: "Extract values from the catalog (filtered) without loading the whole file. Prints JSON.",
|
|
9928
10922
|
usage: "glotfile get [<key-glob>\u2026] [--key <glob>] [--locale <list>] [--state <list>] [--fields <list>] [--keys-only] [--format json|ndjson]",
|
|
@@ -10012,8 +11006,8 @@ ${formatOpts([...options, ...GLOBAL_OPTS])}`);
|
|
|
10012
11006
|
);
|
|
10013
11007
|
}
|
|
10014
11008
|
function printVersion() {
|
|
10015
|
-
const pkgPath =
|
|
10016
|
-
console.log(JSON.parse(
|
|
11009
|
+
const pkgPath = join22(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
11010
|
+
console.log(JSON.parse(readFileSync27(pkgPath, "utf8")).version);
|
|
10017
11011
|
}
|
|
10018
11012
|
async function main(argv) {
|
|
10019
11013
|
const args = parseArgs(argv);
|
|
@@ -10033,11 +11027,13 @@ async function main(argv) {
|
|
|
10033
11027
|
if (args.command === "import") return runImportCmd(args);
|
|
10034
11028
|
if (args.command === "sync") return runSyncCmd(args);
|
|
10035
11029
|
if (args.command === "build-context") return runBuildContext(args);
|
|
11030
|
+
if (args.command === "suggest-glossary") return runSuggestGlossary(args);
|
|
10036
11031
|
if (args.command === "scan") return runScanCmd(args);
|
|
10037
11032
|
if (args.command === "prune") return runPrune(args);
|
|
10038
11033
|
if (args.command === "split") return runSplit(args);
|
|
10039
11034
|
if (args.command === "skill") return runSkill(args);
|
|
10040
11035
|
if (args.command === "batch") return runBatch(args);
|
|
11036
|
+
if (args.command === "prices") return runPrices(args);
|
|
10041
11037
|
if (args.command === "get") return runGetCmd(args);
|
|
10042
11038
|
if (args.command === "stats") return runStatsCmd(args);
|
|
10043
11039
|
if (args.command === "set") return runSet(args);
|