glotfile 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/cli.js +924 -230
- package/dist/server/server.js +724 -231
- package/dist/ui/assets/index-Bjwiz6KQ.css +1 -0
- package/dist/ui/assets/{index-5Imdw0oX.js → index-Dwn9g3g-.js} +67 -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,109 @@ 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
|
+
|
|
4125
4372
|
// src/server/ai/estimate.ts
|
|
4126
4373
|
function estimateTokens(text) {
|
|
4127
4374
|
const cjk = text.match(CJK_RE)?.length ?? 0;
|
|
@@ -4170,29 +4417,131 @@ function estimateTranslation(state, ai, opts) {
|
|
|
4170
4417
|
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4171
4418
|
};
|
|
4172
4419
|
}
|
|
4173
|
-
|
|
4420
|
+
function estimateContext(targets, ai) {
|
|
4421
|
+
const batchSize = Math.max(1, ai.contextBatchSize ?? ai.batchSize ?? 10);
|
|
4422
|
+
const batches = chunk(targets, batchSize);
|
|
4423
|
+
const system = buildContextSystemPrompt();
|
|
4424
|
+
let inputTokens = 0;
|
|
4425
|
+
let outputTokens = 0;
|
|
4426
|
+
for (const batch of batches) {
|
|
4427
|
+
inputTokens += estimateTokens(system) + estimateTokens(buildContextBatchPrompt(batch));
|
|
4428
|
+
outputTokens += batch.length * (CONTEXT_REPLY_OVERHEAD + TYPICAL_CONTEXT_TOKENS);
|
|
4429
|
+
}
|
|
4430
|
+
const pricing = resolvePricing(ai);
|
|
4431
|
+
return {
|
|
4432
|
+
keys: targets.length,
|
|
4433
|
+
batches: batches.length,
|
|
4434
|
+
inputTokens,
|
|
4435
|
+
outputTokens,
|
|
4436
|
+
pricing,
|
|
4437
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4438
|
+
};
|
|
4439
|
+
}
|
|
4440
|
+
function estimateGlossarySuggest(sources, knownTerms, ai) {
|
|
4441
|
+
const batchSize = Math.max(1, ai.contextBatchSize ?? ai.batchSize ?? 10);
|
|
4442
|
+
const batches = chunk(sources, batchSize);
|
|
4443
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
4444
|
+
let inputTokens = 0;
|
|
4445
|
+
let outputTokens = 0;
|
|
4446
|
+
for (const batch of batches) {
|
|
4447
|
+
inputTokens += estimateTokens(system) + estimateTokens(buildGlossarySuggestBatchPrompt(batch, knownTerms));
|
|
4448
|
+
outputTokens += Math.ceil(batch.length * TERM_YIELD) * TERM_REPLY_TOKENS;
|
|
4449
|
+
}
|
|
4450
|
+
const pricing = resolvePricing(ai);
|
|
4451
|
+
return {
|
|
4452
|
+
sources: sources.length,
|
|
4453
|
+
batches: batches.length,
|
|
4454
|
+
inputTokens,
|
|
4455
|
+
outputTokens,
|
|
4456
|
+
pricing,
|
|
4457
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
var CJK_RE, EXPANSION, ITEM_REPLY_OVERHEAD, FORM_REPLY_OVERHEAD, CONTEXT_REPLY_OVERHEAD, TYPICAL_CONTEXT_TOKENS, TERM_REPLY_TOKENS, TERM_YIELD;
|
|
4174
4461
|
var init_estimate = __esm({
|
|
4175
4462
|
"src/server/ai/estimate.ts"() {
|
|
4176
4463
|
"use strict";
|
|
4177
4464
|
init_run();
|
|
4178
4465
|
init_provider();
|
|
4466
|
+
init_context();
|
|
4467
|
+
init_glossary_suggest();
|
|
4179
4468
|
init_batch();
|
|
4180
4469
|
init_pricing();
|
|
4181
4470
|
CJK_RE = /[ -鿿가-豈-]/g;
|
|
4182
4471
|
EXPANSION = 1.2;
|
|
4183
4472
|
ITEM_REPLY_OVERHEAD = 16;
|
|
4184
4473
|
FORM_REPLY_OVERHEAD = 8;
|
|
4474
|
+
CONTEXT_REPLY_OVERHEAD = 16;
|
|
4475
|
+
TYPICAL_CONTEXT_TOKENS = 35;
|
|
4476
|
+
TERM_REPLY_TOKENS = 24;
|
|
4477
|
+
TERM_YIELD = 0.15;
|
|
4478
|
+
}
|
|
4479
|
+
});
|
|
4480
|
+
|
|
4481
|
+
// src/server/ai/price-fetch.ts
|
|
4482
|
+
function normalizeModelsDevPrices(api) {
|
|
4483
|
+
const out = {};
|
|
4484
|
+
const ranks = {};
|
|
4485
|
+
if (!api || typeof api !== "object") return out;
|
|
4486
|
+
for (const [provId, prov] of Object.entries(api)) {
|
|
4487
|
+
const models = prov?.models;
|
|
4488
|
+
if (!models || typeof models !== "object") continue;
|
|
4489
|
+
const rank = providerRank(provId);
|
|
4490
|
+
for (const [modelKey, model] of Object.entries(models)) {
|
|
4491
|
+
const cost = model?.cost;
|
|
4492
|
+
if (!cost || typeof cost.input !== "number" || typeof cost.output !== "number") continue;
|
|
4493
|
+
const bareId = bareModelId(modelKey);
|
|
4494
|
+
if (!bareId) continue;
|
|
4495
|
+
const existingRank = ranks[bareId];
|
|
4496
|
+
if (existingRank !== void 0 && existingRank <= rank) continue;
|
|
4497
|
+
const price = { inputPerMTok: cost.input, outputPerMTok: cost.output };
|
|
4498
|
+
if (typeof cost.cache_read === "number") price.cacheReadPerMTok = cost.cache_read;
|
|
4499
|
+
if (typeof cost.cache_write === "number") price.cacheWritePerMTok = cost.cache_write;
|
|
4500
|
+
out[bareId] = price;
|
|
4501
|
+
ranks[bareId] = rank;
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
return out;
|
|
4505
|
+
}
|
|
4506
|
+
async function refreshPrices(opts = {}) {
|
|
4507
|
+
const url = opts.url ?? priceUrl();
|
|
4508
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
4509
|
+
const res = await doFetch(url);
|
|
4510
|
+
if (!res.ok) throw new Error(`Failed to fetch prices from ${url}: HTTP ${res.status}`);
|
|
4511
|
+
const api = await res.json();
|
|
4512
|
+
const models = normalizeModelsDevPrices(api);
|
|
4513
|
+
const modelCount = Object.keys(models).length;
|
|
4514
|
+
if (modelCount === 0) throw new Error(`No model prices found in response from ${url}`);
|
|
4515
|
+
const cache2 = { source: "models.dev", fetchedAt: (opts.now ?? defaultNow)(), models };
|
|
4516
|
+
const path = opts.path ?? defaultPriceCachePath();
|
|
4517
|
+
savePriceCache(cache2, path);
|
|
4518
|
+
return { source: cache2.source, fetchedAt: cache2.fetchedAt, modelCount, path };
|
|
4519
|
+
}
|
|
4520
|
+
var MODELS_DEV_URL, priceUrl, PROVIDER_PREFERENCE, providerRank, defaultNow;
|
|
4521
|
+
var init_price_fetch = __esm({
|
|
4522
|
+
"src/server/ai/price-fetch.ts"() {
|
|
4523
|
+
"use strict";
|
|
4524
|
+
init_pricing();
|
|
4525
|
+
init_price_cache();
|
|
4526
|
+
MODELS_DEV_URL = "https://models.dev/api.json";
|
|
4527
|
+
priceUrl = () => process.env.GLOTFILE_PRICES_URL || MODELS_DEV_URL;
|
|
4528
|
+
PROVIDER_PREFERENCE = ["anthropic", "openai", "google", "meta-llama", "mistral", "deepseek", "xai", "groq"];
|
|
4529
|
+
providerRank = (provId) => {
|
|
4530
|
+
const i = PROVIDER_PREFERENCE.indexOf(provId);
|
|
4531
|
+
return i === -1 ? PROVIDER_PREFERENCE.length : i;
|
|
4532
|
+
};
|
|
4533
|
+
defaultNow = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
4185
4534
|
}
|
|
4186
4535
|
});
|
|
4187
4536
|
|
|
4188
4537
|
// src/server/scan.ts
|
|
4189
|
-
import { existsSync as existsSync10, readFileSync as
|
|
4538
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
|
|
4190
4539
|
import { resolve as resolve7 } from "path";
|
|
4191
4540
|
function loadUsageCache(projectRoot) {
|
|
4192
4541
|
const path = resolve7(projectRoot, ".glotfile", "usage.json");
|
|
4193
4542
|
if (!existsSync10(path)) return null;
|
|
4194
4543
|
try {
|
|
4195
|
-
return JSON.parse(
|
|
4544
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
4196
4545
|
} catch {
|
|
4197
4546
|
return null;
|
|
4198
4547
|
}
|
|
@@ -4257,8 +4606,8 @@ var init_scan = __esm({
|
|
|
4257
4606
|
});
|
|
4258
4607
|
|
|
4259
4608
|
// src/server/scanner.ts
|
|
4260
|
-
import { readdirSync as readdirSync3, statSync as statSync3, readFileSync as
|
|
4261
|
-
import { join as
|
|
4609
|
+
import { readdirSync as readdirSync3, statSync as statSync3, readFileSync as readFileSync12 } from "fs";
|
|
4610
|
+
import { join as join6, extname as extname2, relative } from "path";
|
|
4262
4611
|
function scannerForExt(ext) {
|
|
4263
4612
|
return EXT_SCANNER[ext] ?? null;
|
|
4264
4613
|
}
|
|
@@ -4480,7 +4829,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
4480
4829
|
}
|
|
4481
4830
|
for (const name of entries) {
|
|
4482
4831
|
if (ALWAYS_EXCLUDE.has(name)) continue;
|
|
4483
|
-
const abs =
|
|
4832
|
+
const abs = join6(dir, name);
|
|
4484
4833
|
const rel = relative(root, abs);
|
|
4485
4834
|
let st;
|
|
4486
4835
|
try {
|
|
@@ -4510,7 +4859,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
4510
4859
|
const ext = extname2(relPath);
|
|
4511
4860
|
const scanner = scannerForExt(ext);
|
|
4512
4861
|
if (!scanner) continue;
|
|
4513
|
-
const abs =
|
|
4862
|
+
const abs = join6(projectRoot, relPath);
|
|
4514
4863
|
let st;
|
|
4515
4864
|
try {
|
|
4516
4865
|
st = statSync3(abs);
|
|
@@ -4526,7 +4875,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
4526
4875
|
}
|
|
4527
4876
|
let content;
|
|
4528
4877
|
try {
|
|
4529
|
-
content =
|
|
4878
|
+
content = readFileSync12(abs, "utf8");
|
|
4530
4879
|
} catch {
|
|
4531
4880
|
continue;
|
|
4532
4881
|
}
|
|
@@ -4660,8 +5009,8 @@ var init_scanner = __esm({
|
|
|
4660
5009
|
});
|
|
4661
5010
|
|
|
4662
5011
|
// src/server/import/detect.ts
|
|
4663
|
-
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as
|
|
4664
|
-
import { join as
|
|
5012
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync4 } from "fs";
|
|
5013
|
+
import { join as join7 } from "path";
|
|
4665
5014
|
function safeIsDir(p) {
|
|
4666
5015
|
try {
|
|
4667
5016
|
return statSync4(p).isDirectory();
|
|
@@ -4670,7 +5019,7 @@ function safeIsDir(p) {
|
|
|
4670
5019
|
}
|
|
4671
5020
|
}
|
|
4672
5021
|
function listDirs(dir) {
|
|
4673
|
-
return readdirSync4(dir).filter((e) => safeIsDir(
|
|
5022
|
+
return readdirSync4(dir).filter((e) => safeIsDir(join7(dir, e)));
|
|
4674
5023
|
}
|
|
4675
5024
|
function fileCount(dir) {
|
|
4676
5025
|
try {
|
|
@@ -4684,23 +5033,23 @@ function pickSource(locales, sizeOf) {
|
|
|
4684
5033
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4685
5034
|
}
|
|
4686
5035
|
function detectLaravel(root) {
|
|
4687
|
-
const localeRoot = [
|
|
5036
|
+
const localeRoot = [join7(root, "resources", "lang"), join7(root, "lang")].find(safeIsDir);
|
|
4688
5037
|
if (!localeRoot) return null;
|
|
4689
5038
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4690
5039
|
if (locales.length === 0) return null;
|
|
4691
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
5040
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join7(localeRoot, loc)));
|
|
4692
5041
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4693
5042
|
}
|
|
4694
5043
|
function detectVue(root, forced = false) {
|
|
4695
5044
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4696
|
-
const localeRoot =
|
|
5045
|
+
const localeRoot = join7(root, rel);
|
|
4697
5046
|
if (!safeIsDir(localeRoot)) continue;
|
|
4698
5047
|
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4699
5048
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4700
5049
|
if (enough) {
|
|
4701
5050
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4702
5051
|
try {
|
|
4703
|
-
return statSync4(
|
|
5052
|
+
return statSync4(join7(localeRoot, `${loc}.json`)).size;
|
|
4704
5053
|
} catch {
|
|
4705
5054
|
return 0;
|
|
4706
5055
|
}
|
|
@@ -4711,9 +5060,9 @@ function detectVue(root, forced = false) {
|
|
|
4711
5060
|
return null;
|
|
4712
5061
|
}
|
|
4713
5062
|
function hasNextIntlSignal(root) {
|
|
4714
|
-
if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync11(
|
|
5063
|
+
if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync11(join7(root, rel)))) return true;
|
|
4715
5064
|
try {
|
|
4716
|
-
const pkg = JSON.parse(
|
|
5065
|
+
const pkg = JSON.parse(readFileSync13(join7(root, "package.json"), "utf8"));
|
|
4717
5066
|
if (pkg.dependencies?.["next-intl"] || pkg.devDependencies?.["next-intl"]) return true;
|
|
4718
5067
|
} catch {
|
|
4719
5068
|
}
|
|
@@ -4722,7 +5071,7 @@ function hasNextIntlSignal(root) {
|
|
|
4722
5071
|
function nextIntlDefaultLocale(root) {
|
|
4723
5072
|
for (const rel of NEXT_INTL_ROUTING_CANDIDATES) {
|
|
4724
5073
|
try {
|
|
4725
|
-
const m =
|
|
5074
|
+
const m = readFileSync13(join7(root, rel), "utf8").match(/defaultLocale\s*:\s*['"]([^'"]+)['"]/);
|
|
4726
5075
|
if (m) return m[1];
|
|
4727
5076
|
} catch {
|
|
4728
5077
|
}
|
|
@@ -4732,14 +5081,14 @@ function nextIntlDefaultLocale(root) {
|
|
|
4732
5081
|
function detectNextIntl(root, forced = false) {
|
|
4733
5082
|
if (!forced && !hasNextIntlSignal(root)) return null;
|
|
4734
5083
|
for (const rel of NEXT_INTL_DIR_CANDIDATES) {
|
|
4735
|
-
const localeRoot =
|
|
5084
|
+
const localeRoot = join7(root, rel);
|
|
4736
5085
|
if (!safeIsDir(localeRoot)) continue;
|
|
4737
5086
|
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4738
5087
|
if (locales.length === 0) continue;
|
|
4739
5088
|
const def = nextIntlDefaultLocale(root);
|
|
4740
5089
|
const sourceLocale = def && locales.includes(def) ? def : pickSource(locales, (loc) => {
|
|
4741
5090
|
try {
|
|
4742
|
-
return statSync4(
|
|
5091
|
+
return statSync4(join7(localeRoot, `${loc}.json`)).size;
|
|
4743
5092
|
} catch {
|
|
4744
5093
|
return 0;
|
|
4745
5094
|
}
|
|
@@ -4750,7 +5099,7 @@ function detectNextIntl(root, forced = false) {
|
|
|
4750
5099
|
}
|
|
4751
5100
|
function detectArb(root) {
|
|
4752
5101
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4753
|
-
const localeRoot =
|
|
5102
|
+
const localeRoot = join7(root, rel);
|
|
4754
5103
|
if (!safeIsDir(localeRoot)) continue;
|
|
4755
5104
|
const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4756
5105
|
if (locales.length >= 1) {
|
|
@@ -4760,10 +5109,10 @@ function detectArb(root) {
|
|
|
4760
5109
|
return null;
|
|
4761
5110
|
}
|
|
4762
5111
|
function lprojLocales(dir) {
|
|
4763
|
-
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(
|
|
5112
|
+
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join7(dir, `${l}.lproj`, "Localizable.strings")));
|
|
4764
5113
|
}
|
|
4765
5114
|
function detectApple(root) {
|
|
4766
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5115
|
+
const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
|
|
4767
5116
|
let best = null;
|
|
4768
5117
|
for (const dir of candidates) {
|
|
4769
5118
|
const locales = lprojLocales(dir);
|
|
@@ -4775,7 +5124,7 @@ function detectApple(root) {
|
|
|
4775
5124
|
locales,
|
|
4776
5125
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4777
5126
|
try {
|
|
4778
|
-
return statSync4(
|
|
5127
|
+
return statSync4(join7(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4779
5128
|
} catch {
|
|
4780
5129
|
return 0;
|
|
4781
5130
|
}
|
|
@@ -4787,7 +5136,7 @@ function detectApple(root) {
|
|
|
4787
5136
|
}
|
|
4788
5137
|
function detectAngularXliff(root) {
|
|
4789
5138
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4790
|
-
const localeRoot = rel === "." ? root :
|
|
5139
|
+
const localeRoot = rel === "." ? root : join7(root, rel);
|
|
4791
5140
|
if (!safeIsDir(localeRoot)) continue;
|
|
4792
5141
|
const files = readdirSync4(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4793
5142
|
if (files.length === 0) continue;
|
|
@@ -4795,7 +5144,7 @@ function detectAngularXliff(root) {
|
|
|
4795
5144
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4796
5145
|
let sourceLocale;
|
|
4797
5146
|
try {
|
|
4798
|
-
sourceLocale =
|
|
5147
|
+
sourceLocale = readFileSync13(join7(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4799
5148
|
} catch {
|
|
4800
5149
|
}
|
|
4801
5150
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -4806,14 +5155,14 @@ function detectAngularXliff(root) {
|
|
|
4806
5155
|
return null;
|
|
4807
5156
|
}
|
|
4808
5157
|
function detectRails(root) {
|
|
4809
|
-
const localeRoot =
|
|
5158
|
+
const localeRoot = join7(root, "config", "locales");
|
|
4810
5159
|
if (!safeIsDir(localeRoot)) return null;
|
|
4811
5160
|
const locales = [];
|
|
4812
5161
|
for (const file of readdirSync4(localeRoot).sort()) {
|
|
4813
5162
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
4814
5163
|
let text;
|
|
4815
5164
|
try {
|
|
4816
|
-
text =
|
|
5165
|
+
text = readFileSync13(join7(localeRoot, file), "utf8");
|
|
4817
5166
|
} catch {
|
|
4818
5167
|
continue;
|
|
4819
5168
|
}
|
|
@@ -4827,15 +5176,15 @@ function detectRails(root) {
|
|
|
4827
5176
|
}
|
|
4828
5177
|
function detectI18next(root) {
|
|
4829
5178
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4830
|
-
const localeRoot =
|
|
5179
|
+
const localeRoot = join7(root, rel);
|
|
4831
5180
|
if (!safeIsDir(localeRoot)) continue;
|
|
4832
5181
|
const locales = listDirs(localeRoot).filter(
|
|
4833
|
-
(d) => LOCALE_RE.test(d) && readdirSync4(
|
|
5182
|
+
(d) => LOCALE_RE.test(d) && readdirSync4(join7(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4834
5183
|
);
|
|
4835
5184
|
if (locales.length === 0) continue;
|
|
4836
5185
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4837
5186
|
try {
|
|
4838
|
-
return readdirSync4(
|
|
5187
|
+
return readdirSync4(join7(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync4(join7(localeRoot, loc, f)).size, 0);
|
|
4839
5188
|
} catch {
|
|
4840
5189
|
return 0;
|
|
4841
5190
|
}
|
|
@@ -4852,8 +5201,8 @@ function gettextLocales(dir) {
|
|
|
4852
5201
|
if (!locales.includes(flat)) locales.push(flat);
|
|
4853
5202
|
continue;
|
|
4854
5203
|
}
|
|
4855
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
4856
|
-
const sub =
|
|
5204
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join7(dir, entry))) continue;
|
|
5205
|
+
const sub = join7(dir, entry);
|
|
4857
5206
|
const hasPo = (d) => {
|
|
4858
5207
|
try {
|
|
4859
5208
|
return readdirSync4(d).some((f) => f.endsWith(".po"));
|
|
@@ -4861,7 +5210,7 @@ function gettextLocales(dir) {
|
|
|
4861
5210
|
return false;
|
|
4862
5211
|
}
|
|
4863
5212
|
};
|
|
4864
|
-
if (hasPo(
|
|
5213
|
+
if (hasPo(join7(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4865
5214
|
if (!locales.includes(entry)) locales.push(entry);
|
|
4866
5215
|
}
|
|
4867
5216
|
}
|
|
@@ -4869,7 +5218,7 @@ function gettextLocales(dir) {
|
|
|
4869
5218
|
}
|
|
4870
5219
|
function detectGettext(root) {
|
|
4871
5220
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4872
|
-
const localeRoot =
|
|
5221
|
+
const localeRoot = join7(root, rel);
|
|
4873
5222
|
if (!safeIsDir(localeRoot)) continue;
|
|
4874
5223
|
const locales = gettextLocales(localeRoot);
|
|
4875
5224
|
if (locales.length === 0) continue;
|
|
@@ -4878,10 +5227,10 @@ function detectGettext(root) {
|
|
|
4878
5227
|
return null;
|
|
4879
5228
|
}
|
|
4880
5229
|
function detectAppleStringsdict(root) {
|
|
4881
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5230
|
+
const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
|
|
4882
5231
|
let best = null;
|
|
4883
5232
|
for (const dir of candidates) {
|
|
4884
|
-
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(
|
|
5233
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join7(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
4885
5234
|
if (locales.length === 0) continue;
|
|
4886
5235
|
if (!best || locales.length > best.locales.length) {
|
|
4887
5236
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4968,8 +5317,8 @@ var init_flatten = __esm({
|
|
|
4968
5317
|
});
|
|
4969
5318
|
|
|
4970
5319
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4971
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
4972
|
-
import { join as
|
|
5320
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
|
|
5321
|
+
import { join as join8 } from "path";
|
|
4973
5322
|
function fromVueI18n(value) {
|
|
4974
5323
|
return value.replace(/\{'([^']*)'\}/g, "'$1'");
|
|
4975
5324
|
}
|
|
@@ -4992,7 +5341,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
4992
5341
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4993
5342
|
let data;
|
|
4994
5343
|
try {
|
|
4995
|
-
data = JSON.parse(
|
|
5344
|
+
data = JSON.parse(readFileSync14(join8(localeRoot, file), "utf8"));
|
|
4996
5345
|
} catch (e) {
|
|
4997
5346
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4998
5347
|
continue;
|
|
@@ -5009,8 +5358,8 @@ var init_vue_i18n_json2 = __esm({
|
|
|
5009
5358
|
});
|
|
5010
5359
|
|
|
5011
5360
|
// src/server/import/parsers/next-intl-json.ts
|
|
5012
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
5013
|
-
import { join as
|
|
5361
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync15 } from "fs";
|
|
5362
|
+
import { join as join9 } from "path";
|
|
5014
5363
|
var LOCALE_RE3, nextIntlJson2;
|
|
5015
5364
|
var init_next_intl_json2 = __esm({
|
|
5016
5365
|
"src/server/import/parsers/next-intl-json.ts"() {
|
|
@@ -5030,7 +5379,7 @@ var init_next_intl_json2 = __esm({
|
|
|
5030
5379
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5031
5380
|
let data;
|
|
5032
5381
|
try {
|
|
5033
|
-
data = JSON.parse(
|
|
5382
|
+
data = JSON.parse(readFileSync15(join9(localeRoot, file), "utf8"));
|
|
5034
5383
|
} catch (e) {
|
|
5035
5384
|
warnings.push(`next-intl-json: failed to parse ${file}: ${e.message}`);
|
|
5036
5385
|
continue;
|
|
@@ -5064,16 +5413,16 @@ var init_placeholders2 = __esm({
|
|
|
5064
5413
|
|
|
5065
5414
|
// src/server/import/parsers/laravel-php.ts
|
|
5066
5415
|
import { readdirSync as readdirSync7, statSync as statSync5 } from "fs";
|
|
5067
|
-
import { join as
|
|
5416
|
+
import { join as join10, relative as relative2 } from "path";
|
|
5068
5417
|
import { execFileSync } from "child_process";
|
|
5069
5418
|
function listDirs2(dir) {
|
|
5070
|
-
return readdirSync7(dir).filter((e) => statSync5(
|
|
5419
|
+
return readdirSync7(dir).filter((e) => statSync5(join10(dir, e)).isDirectory());
|
|
5071
5420
|
}
|
|
5072
5421
|
function listPhpFiles(dir) {
|
|
5073
5422
|
const out = [];
|
|
5074
5423
|
const walk = (d) => {
|
|
5075
5424
|
for (const e of readdirSync7(d)) {
|
|
5076
|
-
const full =
|
|
5425
|
+
const full = join10(d, e);
|
|
5077
5426
|
if (statSync5(full).isDirectory()) walk(full);
|
|
5078
5427
|
else if (e.endsWith(".php")) out.push(full);
|
|
5079
5428
|
}
|
|
@@ -5116,7 +5465,7 @@ var init_laravel_php2 = __esm({
|
|
|
5116
5465
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
5117
5466
|
if (locale === "vendor") continue;
|
|
5118
5467
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5119
|
-
const localeDir =
|
|
5468
|
+
const localeDir = join10(localeRoot, locale);
|
|
5120
5469
|
locales.push(locale);
|
|
5121
5470
|
for (const file of listPhpFiles(localeDir)) {
|
|
5122
5471
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -5141,8 +5490,8 @@ var init_laravel_php2 = __esm({
|
|
|
5141
5490
|
});
|
|
5142
5491
|
|
|
5143
5492
|
// src/server/import/parsers/flutter-arb.ts
|
|
5144
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
5145
|
-
import { join as
|
|
5493
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync16 } from "fs";
|
|
5494
|
+
import { join as join11 } from "path";
|
|
5146
5495
|
function localeFromArbName(file) {
|
|
5147
5496
|
const m = file.match(/^(.+)\.arb$/);
|
|
5148
5497
|
if (!m) return null;
|
|
@@ -5182,7 +5531,7 @@ var init_flutter_arb2 = __esm({
|
|
|
5182
5531
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5183
5532
|
let data;
|
|
5184
5533
|
try {
|
|
5185
|
-
data = JSON.parse(
|
|
5534
|
+
data = JSON.parse(readFileSync16(join11(localeRoot, file), "utf8"));
|
|
5186
5535
|
} catch (e) {
|
|
5187
5536
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
5188
5537
|
continue;
|
|
@@ -5209,8 +5558,8 @@ var init_flutter_arb2 = __esm({
|
|
|
5209
5558
|
});
|
|
5210
5559
|
|
|
5211
5560
|
// src/server/import/parsers/apple-strings.ts
|
|
5212
|
-
import { readdirSync as readdirSync9, readFileSync as
|
|
5213
|
-
import { join as
|
|
5561
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
|
|
5562
|
+
import { join as join12 } from "path";
|
|
5214
5563
|
function localeFromLproj(dir) {
|
|
5215
5564
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
5216
5565
|
if (!m) return null;
|
|
@@ -5330,16 +5679,16 @@ var init_apple_strings2 = __esm({
|
|
|
5330
5679
|
const locale = localeFromLproj(dir);
|
|
5331
5680
|
if (!locale) continue;
|
|
5332
5681
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5333
|
-
const file =
|
|
5682
|
+
const file = join12(localeRoot, dir, TABLE);
|
|
5334
5683
|
let text;
|
|
5335
5684
|
try {
|
|
5336
5685
|
if (!statSync6(file).isFile()) continue;
|
|
5337
|
-
text =
|
|
5686
|
+
text = readFileSync17(file, "utf8");
|
|
5338
5687
|
} catch {
|
|
5339
5688
|
continue;
|
|
5340
5689
|
}
|
|
5341
5690
|
locales.push(locale);
|
|
5342
|
-
const others = readdirSync9(
|
|
5691
|
+
const others = readdirSync9(join12(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
5343
5692
|
if (others.length) {
|
|
5344
5693
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
5345
5694
|
}
|
|
@@ -5354,8 +5703,8 @@ var init_apple_strings2 = __esm({
|
|
|
5354
5703
|
});
|
|
5355
5704
|
|
|
5356
5705
|
// src/server/import/parsers/angular-xliff.ts
|
|
5357
|
-
import { readdirSync as readdirSync10, readFileSync as
|
|
5358
|
-
import { join as
|
|
5706
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync18 } from "fs";
|
|
5707
|
+
import { join as join13 } from "path";
|
|
5359
5708
|
function decodeEntities(s) {
|
|
5360
5709
|
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
5710
|
}
|
|
@@ -5426,7 +5775,7 @@ var init_angular_xliff2 = __esm({
|
|
|
5426
5775
|
if (fnameLocale !== void 0 && !LOCALE_RE6.test(fnameLocale)) continue;
|
|
5427
5776
|
let xml;
|
|
5428
5777
|
try {
|
|
5429
|
-
xml =
|
|
5778
|
+
xml = readFileSync18(join13(localeRoot, file), "utf8");
|
|
5430
5779
|
} catch (e) {
|
|
5431
5780
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
5432
5781
|
continue;
|
|
@@ -5473,8 +5822,8 @@ var init_angular_xliff2 = __esm({
|
|
|
5473
5822
|
});
|
|
5474
5823
|
|
|
5475
5824
|
// src/server/import/parsers/gettext-po.ts
|
|
5476
|
-
import { readdirSync as readdirSync11, readFileSync as
|
|
5477
|
-
import { join as
|
|
5825
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync19 } from "fs";
|
|
5826
|
+
import { join as join14 } from "path";
|
|
5478
5827
|
function unescapePo(s) {
|
|
5479
5828
|
return s.replace(
|
|
5480
5829
|
/\\([\\"ntr])/g,
|
|
@@ -5563,17 +5912,17 @@ function discoverPoFiles(root) {
|
|
|
5563
5912
|
for (const e of entries) {
|
|
5564
5913
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
5565
5914
|
const base = e.name.slice(0, -3);
|
|
5566
|
-
found.push({ path:
|
|
5915
|
+
found.push({ path: join14(root, e.name), rel: e.name, locale: LOCALE_RE7.test(base) ? base : null });
|
|
5567
5916
|
} else if (e.isDirectory() && LOCALE_RE7.test(e.name)) {
|
|
5568
|
-
for (const sub of [
|
|
5917
|
+
for (const sub of [join14(e.name, "LC_MESSAGES"), e.name]) {
|
|
5569
5918
|
let names;
|
|
5570
5919
|
try {
|
|
5571
|
-
names = readdirSync11(
|
|
5920
|
+
names = readdirSync11(join14(root, sub)).sort();
|
|
5572
5921
|
} catch {
|
|
5573
5922
|
continue;
|
|
5574
5923
|
}
|
|
5575
5924
|
for (const f of names) {
|
|
5576
|
-
if (f.endsWith(".po")) found.push({ path:
|
|
5925
|
+
if (f.endsWith(".po")) found.push({ path: join14(root, sub, f), rel: join14(sub, f), locale: e.name });
|
|
5577
5926
|
}
|
|
5578
5927
|
}
|
|
5579
5928
|
}
|
|
@@ -5597,7 +5946,7 @@ var init_gettext_po2 = __esm({
|
|
|
5597
5946
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
5598
5947
|
let entries;
|
|
5599
5948
|
try {
|
|
5600
|
-
entries = parseEntries(
|
|
5949
|
+
entries = parseEntries(readFileSync19(file.path, "utf8"));
|
|
5601
5950
|
} catch (e) {
|
|
5602
5951
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
5603
5952
|
continue;
|
|
@@ -5644,8 +5993,8 @@ var init_gettext_po2 = __esm({
|
|
|
5644
5993
|
});
|
|
5645
5994
|
|
|
5646
5995
|
// src/server/import/parsers/i18next-json.ts
|
|
5647
|
-
import { readdirSync as readdirSync12, readFileSync as
|
|
5648
|
-
import { join as
|
|
5996
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync20, statSync as statSync7 } from "fs";
|
|
5997
|
+
import { join as join15 } from "path";
|
|
5649
5998
|
function safeIsDir2(p) {
|
|
5650
5999
|
try {
|
|
5651
6000
|
return statSync7(p).isDirectory();
|
|
@@ -5660,7 +6009,7 @@ function fromI18next(value) {
|
|
|
5660
6009
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
5661
6010
|
let data;
|
|
5662
6011
|
try {
|
|
5663
|
-
data = JSON.parse(
|
|
6012
|
+
data = JSON.parse(readFileSync20(path, "utf8"));
|
|
5664
6013
|
} catch (e) {
|
|
5665
6014
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
5666
6015
|
return false;
|
|
@@ -5713,7 +6062,7 @@ var init_i18next_json2 = __esm({
|
|
|
5713
6062
|
const keys = {};
|
|
5714
6063
|
const locales = [];
|
|
5715
6064
|
for (const entry of readdirSync12(localeRoot).sort()) {
|
|
5716
|
-
const full =
|
|
6065
|
+
const full = join15(localeRoot, entry);
|
|
5717
6066
|
if (safeIsDir2(full)) {
|
|
5718
6067
|
if (!LOCALE_RE8.test(entry)) continue;
|
|
5719
6068
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
@@ -5722,7 +6071,7 @@ var init_i18next_json2 = __esm({
|
|
|
5722
6071
|
if (!file.endsWith(".json")) continue;
|
|
5723
6072
|
const ns = file.slice(0, -".json".length);
|
|
5724
6073
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
5725
|
-
if (ingestFile(
|
|
6074
|
+
if (ingestFile(join15(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
5726
6075
|
}
|
|
5727
6076
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
5728
6077
|
} else if (entry.endsWith(".json")) {
|
|
@@ -5741,8 +6090,8 @@ var init_i18next_json2 = __esm({
|
|
|
5741
6090
|
});
|
|
5742
6091
|
|
|
5743
6092
|
// src/server/import/parsers/rails-yaml.ts
|
|
5744
|
-
import { readdirSync as readdirSync13, readFileSync as
|
|
5745
|
-
import { join as
|
|
6093
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync21 } from "fs";
|
|
6094
|
+
import { join as join16 } from "path";
|
|
5746
6095
|
function makeNode() {
|
|
5747
6096
|
return /* @__PURE__ */ Object.create(null);
|
|
5748
6097
|
}
|
|
@@ -5966,7 +6315,7 @@ var init_rails_yaml2 = __esm({
|
|
|
5966
6315
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5967
6316
|
let text;
|
|
5968
6317
|
try {
|
|
5969
|
-
text =
|
|
6318
|
+
text = readFileSync21(join16(localeRoot, file), "utf8");
|
|
5970
6319
|
} catch (e) {
|
|
5971
6320
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5972
6321
|
continue;
|
|
@@ -5989,8 +6338,8 @@ var init_rails_yaml2 = __esm({
|
|
|
5989
6338
|
});
|
|
5990
6339
|
|
|
5991
6340
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5992
|
-
import { readdirSync as readdirSync14, readFileSync as
|
|
5993
|
-
import { join as
|
|
6341
|
+
import { readdirSync as readdirSync14, readFileSync as readFileSync22, statSync as statSync8 } from "fs";
|
|
6342
|
+
import { join as join17 } from "path";
|
|
5994
6343
|
function localeFromLproj2(dir) {
|
|
5995
6344
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
5996
6345
|
if (!m) return null;
|
|
@@ -6150,16 +6499,16 @@ var init_apple_stringsdict2 = __esm({
|
|
|
6150
6499
|
const locale = localeFromLproj2(dir);
|
|
6151
6500
|
if (!locale) continue;
|
|
6152
6501
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
6153
|
-
const file =
|
|
6502
|
+
const file = join17(localeRoot, dir, TABLE2);
|
|
6154
6503
|
let text;
|
|
6155
6504
|
try {
|
|
6156
6505
|
if (!statSync8(file).isFile()) continue;
|
|
6157
|
-
text =
|
|
6506
|
+
text = readFileSync22(file, "utf8");
|
|
6158
6507
|
} catch {
|
|
6159
6508
|
continue;
|
|
6160
6509
|
}
|
|
6161
6510
|
locales.push(locale);
|
|
6162
|
-
const others = readdirSync14(
|
|
6511
|
+
const others = readdirSync14(join17(localeRoot, dir)).filter(
|
|
6163
6512
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
6164
6513
|
);
|
|
6165
6514
|
if (others.length) {
|
|
@@ -6633,7 +6982,7 @@ var init_run2 = __esm({
|
|
|
6633
6982
|
});
|
|
6634
6983
|
|
|
6635
6984
|
// src/server/lint/outputs.ts
|
|
6636
|
-
import { readFileSync as
|
|
6985
|
+
import { readFileSync as readFileSync23, existsSync as existsSync12 } from "fs";
|
|
6637
6986
|
import { resolve as resolve8 } from "path";
|
|
6638
6987
|
function checkOutputs(state, root) {
|
|
6639
6988
|
const out = [];
|
|
@@ -6641,7 +6990,7 @@ function checkOutputs(state, root) {
|
|
|
6641
6990
|
const result = getAdapter(output.adapter).export(state, output);
|
|
6642
6991
|
for (const file of result.files) {
|
|
6643
6992
|
const abs = resolve8(root, file.path);
|
|
6644
|
-
const current = existsSync12(abs) ?
|
|
6993
|
+
const current = existsSync12(abs) ? readFileSync23(abs, "utf8") : null;
|
|
6645
6994
|
if (current === null) {
|
|
6646
6995
|
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
6647
6996
|
} else if (current !== file.contents) {
|
|
@@ -6749,6 +7098,7 @@ function assemble2(parsed, opts) {
|
|
|
6749
7098
|
spelling: { customWords: [] }
|
|
6750
7099
|
},
|
|
6751
7100
|
glossary: [],
|
|
7101
|
+
glossarySuggestions: [],
|
|
6752
7102
|
keys,
|
|
6753
7103
|
warnings
|
|
6754
7104
|
};
|
|
@@ -7092,13 +7442,63 @@ var init_checks = __esm({
|
|
|
7092
7442
|
}
|
|
7093
7443
|
});
|
|
7094
7444
|
|
|
7445
|
+
// src/server/ai/explain-error.ts
|
|
7446
|
+
function rawMessage(err) {
|
|
7447
|
+
if (err instanceof Error && err.message) return err.message;
|
|
7448
|
+
if (typeof err === "string") return err;
|
|
7449
|
+
if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
|
|
7450
|
+
return err.message;
|
|
7451
|
+
}
|
|
7452
|
+
return String(err ?? "Unknown error");
|
|
7453
|
+
}
|
|
7454
|
+
function explainProviderError(provider, err) {
|
|
7455
|
+
const raw = rawMessage(err);
|
|
7456
|
+
const m = raw.toLowerCase();
|
|
7457
|
+
if (provider === "bedrock") {
|
|
7458
|
+
if (/could not load credentials|unable to locate credentials|credentialsprovider|credentials from any providers/.test(m)) {
|
|
7459
|
+
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`.";
|
|
7460
|
+
}
|
|
7461
|
+
if (/on-demand throughput isn.?t supported/.test(m) || /inference profile/.test(m)) {
|
|
7462
|
+
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).';
|
|
7463
|
+
}
|
|
7464
|
+
if (/not authorized to perform/.test(m) || /no identity-based policy/.test(m) || /bedrock:invoke/.test(m)) {
|
|
7465
|
+
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.";
|
|
7466
|
+
}
|
|
7467
|
+
if (/access to the model|don.?t have access to the model/.test(m)) {
|
|
7468
|
+
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.";
|
|
7469
|
+
}
|
|
7470
|
+
if (/access ?denied/.test(m)) {
|
|
7471
|
+
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.";
|
|
7472
|
+
}
|
|
7473
|
+
if (/region/.test(m)) {
|
|
7474
|
+
return "No AWS region set for Bedrock. Set the Region in AI settings, or AWS_REGION in your environment.";
|
|
7475
|
+
}
|
|
7476
|
+
}
|
|
7477
|
+
const keyEnv = KEY_ENV[provider];
|
|
7478
|
+
if (keyEnv && /api key|unauthorized|\b401\b|authentication|incorrect api key|invalid x-api-key/.test(m)) {
|
|
7479
|
+
return `${provider} rejected the request \u2014 check ${keyEnv}. Set it in your environment or a .env file in the directory you started glotfile from.`;
|
|
7480
|
+
}
|
|
7481
|
+
return raw;
|
|
7482
|
+
}
|
|
7483
|
+
var KEY_ENV;
|
|
7484
|
+
var init_explain_error = __esm({
|
|
7485
|
+
"src/server/ai/explain-error.ts"() {
|
|
7486
|
+
"use strict";
|
|
7487
|
+
KEY_ENV = {
|
|
7488
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
7489
|
+
openai: "OPENAI_API_KEY",
|
|
7490
|
+
openrouter: "OPENROUTER_API_KEY"
|
|
7491
|
+
};
|
|
7492
|
+
}
|
|
7493
|
+
});
|
|
7494
|
+
|
|
7095
7495
|
// src/server/ui-prefs.ts
|
|
7096
|
-
import { readFileSync as
|
|
7097
|
-
import { homedir } from "os";
|
|
7098
|
-
import { join as
|
|
7496
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
7497
|
+
import { homedir as homedir2 } from "os";
|
|
7498
|
+
import { join as join18 } from "path";
|
|
7099
7499
|
function readJson2(path) {
|
|
7100
7500
|
try {
|
|
7101
|
-
const parsed = JSON.parse(
|
|
7501
|
+
const parsed = JSON.parse(readFileSync24(path, "utf8"));
|
|
7102
7502
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
7103
7503
|
} catch {
|
|
7104
7504
|
return {};
|
|
@@ -7123,7 +7523,7 @@ var init_ui_prefs = __esm({
|
|
|
7123
7523
|
THEMES = ["system", "light", "dark"];
|
|
7124
7524
|
isThemeMode = (v) => THEMES.includes(v);
|
|
7125
7525
|
isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
7126
|
-
defaultUiPrefsPath = () =>
|
|
7526
|
+
defaultUiPrefsPath = () => join18(homedir2(), ".glotfile", "ui.json");
|
|
7127
7527
|
DEFAULTS = { theme: "system" };
|
|
7128
7528
|
}
|
|
7129
7529
|
});
|
|
@@ -7157,7 +7557,7 @@ var init_events = __esm({
|
|
|
7157
7557
|
|
|
7158
7558
|
// src/server/watch.ts
|
|
7159
7559
|
import { statSync as statSync9, readdirSync as readdirSync15 } from "fs";
|
|
7160
|
-
import { join as
|
|
7560
|
+
import { join as join19 } from "path";
|
|
7161
7561
|
import { createHash as createHash2 } from "crypto";
|
|
7162
7562
|
function hashState(state) {
|
|
7163
7563
|
return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
|
|
@@ -7173,15 +7573,15 @@ function signature(statePath) {
|
|
|
7173
7573
|
const parts = [];
|
|
7174
7574
|
for (const rel of ["config.json", "keys.json"]) {
|
|
7175
7575
|
try {
|
|
7176
|
-
const s = statSync9(
|
|
7576
|
+
const s = statSync9(join19(dir, rel));
|
|
7177
7577
|
parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
|
|
7178
7578
|
} catch {
|
|
7179
7579
|
}
|
|
7180
7580
|
}
|
|
7181
7581
|
try {
|
|
7182
|
-
for (const name of readdirSync15(
|
|
7582
|
+
for (const name of readdirSync15(join19(dir, "locales")).sort()) {
|
|
7183
7583
|
if (!name.endsWith(".json")) continue;
|
|
7184
|
-
const s = statSync9(
|
|
7584
|
+
const s = statSync9(join19(dir, "locales", name));
|
|
7185
7585
|
parts.push(`${name}:${s.size}:${s.mtimeMs}`);
|
|
7186
7586
|
}
|
|
7187
7587
|
} catch {
|
|
@@ -7260,34 +7660,19 @@ var init_watch = __esm({
|
|
|
7260
7660
|
// src/server/api.ts
|
|
7261
7661
|
import { Hono } from "hono";
|
|
7262
7662
|
import { streamSSE } from "hono/streaming";
|
|
7263
|
-
import { readFileSync as
|
|
7663
|
+
import { readFileSync as readFileSync25, existsSync as existsSync13, readdirSync as readdirSync16, statSync as statSync10, rmSync as rmSync6 } from "fs";
|
|
7264
7664
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
7265
7665
|
function projectName(root) {
|
|
7266
7666
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
7267
7667
|
if (existsSync13(nameFile)) {
|
|
7268
7668
|
try {
|
|
7269
|
-
const name =
|
|
7669
|
+
const name = readFileSync25(nameFile, "utf8").trim();
|
|
7270
7670
|
if (name) return name;
|
|
7271
7671
|
} catch {
|
|
7272
7672
|
}
|
|
7273
7673
|
}
|
|
7274
7674
|
return basename(root);
|
|
7275
7675
|
}
|
|
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
7676
|
function createApi(deps) {
|
|
7292
7677
|
const app = new Hono();
|
|
7293
7678
|
const load = () => loadState(deps.statePath);
|
|
@@ -7419,6 +7804,61 @@ function createApi(deps) {
|
|
|
7419
7804
|
}
|
|
7420
7805
|
return c.json({ ok: true });
|
|
7421
7806
|
});
|
|
7807
|
+
app.post("/ai-test", async (c) => {
|
|
7808
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7809
|
+
const meta = { provider: aiCfg.provider, model: aiCfg.model };
|
|
7810
|
+
let provider;
|
|
7811
|
+
try {
|
|
7812
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7813
|
+
} catch (e) {
|
|
7814
|
+
return c.json({ ok: false, ...meta, error: explainProviderError(aiCfg.provider, e) });
|
|
7815
|
+
}
|
|
7816
|
+
const controller = new AbortController();
|
|
7817
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
7818
|
+
try {
|
|
7819
|
+
const probe = {
|
|
7820
|
+
id: "probe",
|
|
7821
|
+
key: "glotfile.connection-test",
|
|
7822
|
+
source: "Hello",
|
|
7823
|
+
sourceLocale: "en",
|
|
7824
|
+
targetLocale: "es",
|
|
7825
|
+
placeholders: []
|
|
7826
|
+
};
|
|
7827
|
+
await provider.translate([probe], void 0, controller.signal);
|
|
7828
|
+
return c.json({ ok: true, ...meta });
|
|
7829
|
+
} catch (e) {
|
|
7830
|
+
const error = controller.signal.aborted ? "Connection test timed out after 30s \u2014 the provider didn't respond." : explainProviderError(aiCfg.provider, e);
|
|
7831
|
+
return c.json({ ok: false, ...meta, error });
|
|
7832
|
+
} finally {
|
|
7833
|
+
clearTimeout(timer);
|
|
7834
|
+
}
|
|
7835
|
+
});
|
|
7836
|
+
app.get("/prices", (c) => {
|
|
7837
|
+
const cache2 = loadPriceCache();
|
|
7838
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
7839
|
+
const pricing = resolvePricing(ai, cache2);
|
|
7840
|
+
return c.json({
|
|
7841
|
+
source: cache2?.source ?? null,
|
|
7842
|
+
fetchedAt: cache2?.fetchedAt ?? null,
|
|
7843
|
+
modelCount: cache2 ? Object.keys(cache2.models).length : 0,
|
|
7844
|
+
path: defaultPriceCachePath(),
|
|
7845
|
+
resolved: pricing ? { provider: ai.provider, model: ai.model, ...pricing } : null
|
|
7846
|
+
});
|
|
7847
|
+
});
|
|
7848
|
+
app.get("/prices/list", (c) => {
|
|
7849
|
+
const cache2 = loadPriceCache();
|
|
7850
|
+
const models = cache2 ? Object.entries(cache2.models).map(([id, p]) => ({ id, ...p })).sort((a, b) => a.id.localeCompare(b.id)) : [];
|
|
7851
|
+
return c.json({ source: cache2?.source ?? null, fetchedAt: cache2?.fetchedAt ?? null, models });
|
|
7852
|
+
});
|
|
7853
|
+
app.post("/prices/refresh", async (c) => {
|
|
7854
|
+
try {
|
|
7855
|
+
const res = await refreshPrices();
|
|
7856
|
+
invalidatePriceCache();
|
|
7857
|
+
return c.json({ ok: true, ...res });
|
|
7858
|
+
} catch (e) {
|
|
7859
|
+
return c.json({ error: e.message }, 502);
|
|
7860
|
+
}
|
|
7861
|
+
});
|
|
7422
7862
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
7423
7863
|
app.get("/files", (c) => {
|
|
7424
7864
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -7783,6 +8223,90 @@ function createApi(deps) {
|
|
|
7783
8223
|
logChange({ kind: "glossary", summary: `Deleted glossary term "${term}"`, before });
|
|
7784
8224
|
return c.json({ ok: true });
|
|
7785
8225
|
});
|
|
8226
|
+
app.get("/glossary/suggestions", (c) => {
|
|
8227
|
+
const s = load();
|
|
8228
|
+
const pending = s.glossarySuggestions.filter((x) => x.status === "pending");
|
|
8229
|
+
return c.json(pending.map((x) => ({
|
|
8230
|
+
...x,
|
|
8231
|
+
occurrences: sourceKeysForTerm(s, x.term, { caseSensitive: x.caseSensitive, wholeWord: x.wholeWord }).length
|
|
8232
|
+
})));
|
|
8233
|
+
});
|
|
8234
|
+
app.post("/glossary/suggestions/dismiss", async (c) => {
|
|
8235
|
+
const { term } = await c.req.json();
|
|
8236
|
+
if (typeof term !== "string") return c.json({ error: "term must be a string" }, 400);
|
|
8237
|
+
const s = load();
|
|
8238
|
+
dismissGlossarySuggestion(s, term);
|
|
8239
|
+
persist(s);
|
|
8240
|
+
logChange({ kind: "glossary", summary: `Dismissed suggested term "${term}"` });
|
|
8241
|
+
return c.json({ ok: true });
|
|
8242
|
+
});
|
|
8243
|
+
app.delete("/glossary/suggestions/:term", (c) => {
|
|
8244
|
+
const s = load();
|
|
8245
|
+
const term = decodeURIComponent(c.req.param("term"));
|
|
8246
|
+
removeGlossarySuggestion(s, term);
|
|
8247
|
+
persist(s);
|
|
8248
|
+
return c.json({ ok: true });
|
|
8249
|
+
});
|
|
8250
|
+
app.post("/glossary/suggest", async (c) => {
|
|
8251
|
+
const signal = c.req.raw.signal;
|
|
8252
|
+
const body = await c.req.json().catch(() => ({}));
|
|
8253
|
+
return streamSSE(c, async (stream) => {
|
|
8254
|
+
const s0 = load();
|
|
8255
|
+
const sources = selectGlossarySources(s0, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
|
|
8256
|
+
if (!sources.length) {
|
|
8257
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ added: 0, terms: [] }) });
|
|
8258
|
+
return;
|
|
8259
|
+
}
|
|
8260
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8261
|
+
let provider;
|
|
8262
|
+
try {
|
|
8263
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
8264
|
+
} catch (e) {
|
|
8265
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
8266
|
+
return;
|
|
8267
|
+
}
|
|
8268
|
+
const known = knownTermList(s0);
|
|
8269
|
+
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: sources.length }) });
|
|
8270
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
8271
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
8272
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
8273
|
+
const chunks = [];
|
|
8274
|
+
for (let i = 0; i < sources.length; i += batchSize) chunks.push(sources.slice(i, i + batchSize));
|
|
8275
|
+
const all = [];
|
|
8276
|
+
let done = 0;
|
|
8277
|
+
let next = 0;
|
|
8278
|
+
async function worker() {
|
|
8279
|
+
while (next < chunks.length) {
|
|
8280
|
+
if (signal?.aborted) break;
|
|
8281
|
+
const chunkRows = chunks[next++];
|
|
8282
|
+
try {
|
|
8283
|
+
const raw = await provider.complete({ system, content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunkRows, known) }], schema: GLOSSARY_SUGGEST_SCHEMA });
|
|
8284
|
+
all.push(...raw.terms ?? []);
|
|
8285
|
+
} catch (e) {
|
|
8286
|
+
void stream.writeSSE({ event: "warn", data: JSON.stringify({ error: e.message }) });
|
|
8287
|
+
}
|
|
8288
|
+
done += chunkRows.length;
|
|
8289
|
+
void stream.writeSSE({ event: "progress", data: JSON.stringify({ done, total: sources.length }) });
|
|
8290
|
+
}
|
|
8291
|
+
}
|
|
8292
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
8293
|
+
if (signal?.aborted) return;
|
|
8294
|
+
const fresh = load();
|
|
8295
|
+
const added = mergeGlossarySuggestions(fresh, dedupeTerms(all));
|
|
8296
|
+
const usage = provider.takeUsage?.();
|
|
8297
|
+
persist(fresh);
|
|
8298
|
+
appendLog(projectRoot, {
|
|
8299
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8300
|
+
kind: "glossary",
|
|
8301
|
+
summary: `Suggested ${added.length} glossary term(s)`,
|
|
8302
|
+
model: aiCfg.model,
|
|
8303
|
+
system,
|
|
8304
|
+
usage,
|
|
8305
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
8306
|
+
});
|
|
8307
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ added: added.length, terms: added }) });
|
|
8308
|
+
});
|
|
8309
|
+
});
|
|
7786
8310
|
app.post("/keys/:key/screenshot", async (c) => {
|
|
7787
8311
|
const key = c.req.param("key");
|
|
7788
8312
|
const body = await c.req.parseBody();
|
|
@@ -7968,7 +8492,7 @@ function createApi(deps) {
|
|
|
7968
8492
|
try {
|
|
7969
8493
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7970
8494
|
} catch (e) {
|
|
7971
|
-
await stream.writeSSE({ event: "error", data: JSON.stringify({ error:
|
|
8495
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: explainProviderError(aiCfg.provider, e) }) });
|
|
7972
8496
|
return;
|
|
7973
8497
|
}
|
|
7974
8498
|
const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
@@ -7985,58 +8509,65 @@ function createApi(deps) {
|
|
|
7985
8509
|
event: "start",
|
|
7986
8510
|
data: JSON.stringify({ total: reqs.length, locales: [...localeTotals].map(([locale, total]) => ({ locale, total })) })
|
|
7987
8511
|
});
|
|
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
|
-
|
|
8512
|
+
try {
|
|
8513
|
+
await runLocaleParallel(reqs, provider, {
|
|
8514
|
+
// Announce a language the moment a worker picks it up — this is the
|
|
8515
|
+
// signal that "something is happening" during the long first LLM call.
|
|
8516
|
+
onLocaleStart: (locale) => {
|
|
8517
|
+
void stream.writeSSE({ event: "locale-start", data: JSON.stringify({ locale }) });
|
|
8518
|
+
},
|
|
8519
|
+
onBatchComplete: (done, total, batchResults, locale) => {
|
|
8520
|
+
const fresh = load();
|
|
8521
|
+
const { written, errors } = applyResults(fresh, reqs, batchResults);
|
|
8522
|
+
persist(fresh);
|
|
8523
|
+
totalWritten += written;
|
|
8524
|
+
allErrors.push(...errors);
|
|
8525
|
+
const usage = provider.takeUsage?.();
|
|
8526
|
+
appendLog(projectRoot, {
|
|
8527
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8528
|
+
kind: "translate",
|
|
8529
|
+
summary: `Translated ${batchResults.length} item(s)`,
|
|
8530
|
+
model: aiCfg.model,
|
|
8531
|
+
system,
|
|
8532
|
+
items: batchResults.map((r) => {
|
|
8533
|
+
const req = reqById.get(r.id);
|
|
8534
|
+
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 };
|
|
8535
|
+
}),
|
|
8536
|
+
results: batchResults,
|
|
8537
|
+
usage,
|
|
8538
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
8539
|
+
});
|
|
8540
|
+
const ld = (localeDone.get(locale) ?? 0) + batchResults.length;
|
|
8541
|
+
localeDone.set(locale, ld);
|
|
8542
|
+
console.log(`[translate] ${done}/${total}`);
|
|
8543
|
+
void stream.writeSSE({
|
|
8544
|
+
event: "progress",
|
|
8545
|
+
data: JSON.stringify({ done, total, written: totalWritten, errors, locale, localeDone: ld, localeTotal: localeTotals.get(locale) ?? 0 })
|
|
8546
|
+
});
|
|
8547
|
+
},
|
|
8548
|
+
onLocaleDone: (locale) => {
|
|
8549
|
+
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
8550
|
+
},
|
|
8551
|
+
// Record the raw reply so an unparseable model response is diagnosable
|
|
8552
|
+
// from the activity log instead of vanishing into per-item errors.
|
|
8553
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
8554
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
8555
|
+
appendLog(projectRoot, {
|
|
8556
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8557
|
+
kind: "translate",
|
|
8558
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
8559
|
+
model: aiCfg.model,
|
|
8560
|
+
locale,
|
|
8561
|
+
raw
|
|
8562
|
+
});
|
|
8563
|
+
}
|
|
8564
|
+
}, aiCfg.concurrency, signal, aiCfg.batchSize);
|
|
8565
|
+
} catch (e) {
|
|
8566
|
+
if (!signal?.aborted) {
|
|
8567
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: explainProviderError(aiCfg.provider, e) }) });
|
|
8038
8568
|
}
|
|
8039
|
-
|
|
8569
|
+
return;
|
|
8570
|
+
}
|
|
8040
8571
|
if (!signal?.aborted) {
|
|
8041
8572
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
8042
8573
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -8063,23 +8594,28 @@ function createApi(deps) {
|
|
|
8063
8594
|
try {
|
|
8064
8595
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
8065
8596
|
} catch (e) {
|
|
8066
|
-
return c.json({ error:
|
|
8597
|
+
return c.json({ error: explainProviderError(aiCfg.provider, e) }, 400);
|
|
8067
8598
|
}
|
|
8068
8599
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
8069
8600
|
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
|
-
|
|
8601
|
+
let results;
|
|
8602
|
+
try {
|
|
8603
|
+
results = await runLocaleParallel(toTranslate, provider, {
|
|
8604
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
8605
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
8606
|
+
appendLog(projectRoot, {
|
|
8607
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8608
|
+
kind: "translate",
|
|
8609
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
8610
|
+
model: aiCfg.model,
|
|
8611
|
+
locale,
|
|
8612
|
+
raw
|
|
8613
|
+
});
|
|
8614
|
+
}
|
|
8615
|
+
}, aiCfg.concurrency, void 0, aiCfg.batchSize);
|
|
8616
|
+
} catch (e) {
|
|
8617
|
+
return c.json({ error: explainProviderError(aiCfg.provider, e) }, 502);
|
|
8618
|
+
}
|
|
8083
8619
|
const latest = load();
|
|
8084
8620
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
8085
8621
|
const usage = provider.takeUsage?.();
|
|
@@ -8350,6 +8886,22 @@ function createApi(deps) {
|
|
|
8350
8886
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
8351
8887
|
});
|
|
8352
8888
|
});
|
|
8889
|
+
app.post("/context/estimate", async (c) => {
|
|
8890
|
+
const body = await c.req.json().catch(() => ({}));
|
|
8891
|
+
const cache2 = loadUsageCache(projectRoot);
|
|
8892
|
+
if (!cache2) return c.json({ error: "No usage index found. Run 'glotfile scan' first." }, 400);
|
|
8893
|
+
const targets = selectContextTargets(load(), {
|
|
8894
|
+
all: body.all,
|
|
8895
|
+
keyGlob: body.keyGlob,
|
|
8896
|
+
limit: body.limit,
|
|
8897
|
+
since: body.since,
|
|
8898
|
+
keys: body.keys,
|
|
8899
|
+
force: body.force
|
|
8900
|
+
}, cache2, body.lastRunAt);
|
|
8901
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8902
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
8903
|
+
return c.json(estimateContext(targets, aiCfg));
|
|
8904
|
+
});
|
|
8353
8905
|
app.get("/context/batch/status", async (c) => {
|
|
8354
8906
|
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8355
8907
|
let supported = false;
|
|
@@ -8454,6 +9006,8 @@ var init_api = __esm({
|
|
|
8454
9006
|
"src/server/api.ts"() {
|
|
8455
9007
|
"use strict";
|
|
8456
9008
|
init_state();
|
|
9009
|
+
init_glossary_suggest();
|
|
9010
|
+
init_glossary();
|
|
8457
9011
|
init_accept();
|
|
8458
9012
|
init_scan();
|
|
8459
9013
|
init_scanner();
|
|
@@ -8467,12 +9021,15 @@ var init_api = __esm({
|
|
|
8467
9021
|
init_ai();
|
|
8468
9022
|
init_run();
|
|
8469
9023
|
init_provider();
|
|
9024
|
+
init_explain_error();
|
|
8470
9025
|
init_batch_run();
|
|
8471
9026
|
init_pending_batch();
|
|
8472
9027
|
init_context_batch_run();
|
|
8473
9028
|
init_pending_context_batch();
|
|
8474
9029
|
init_estimate();
|
|
8475
9030
|
init_pricing();
|
|
9031
|
+
init_price_fetch();
|
|
9032
|
+
init_price_cache();
|
|
8476
9033
|
init_log();
|
|
8477
9034
|
init_schema();
|
|
8478
9035
|
init_run3();
|
|
@@ -8497,7 +9054,7 @@ __export(server_exports, {
|
|
|
8497
9054
|
import { Hono as Hono2 } from "hono";
|
|
8498
9055
|
import { serve } from "@hono/node-server";
|
|
8499
9056
|
import { fileURLToPath } from "url";
|
|
8500
|
-
import { dirname as dirname4, join as
|
|
9057
|
+
import { dirname as dirname4, join as join20, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
8501
9058
|
import { readFile, stat } from "fs/promises";
|
|
8502
9059
|
import { createServer } from "net";
|
|
8503
9060
|
import open from "open";
|
|
@@ -8553,7 +9110,7 @@ function buildApp(opts) {
|
|
|
8553
9110
|
const file = await readFileResponse(target);
|
|
8554
9111
|
if (file) return file;
|
|
8555
9112
|
}
|
|
8556
|
-
const index = await readFileResponse(
|
|
9113
|
+
const index = await readFileResponse(join20(root, "index.html"));
|
|
8557
9114
|
if (index) return index;
|
|
8558
9115
|
return c.notFound();
|
|
8559
9116
|
});
|
|
@@ -8623,7 +9180,7 @@ var init_server = __esm({
|
|
|
8623
9180
|
init_scanner();
|
|
8624
9181
|
init_usage();
|
|
8625
9182
|
here = dirname4(fileURLToPath(import.meta.url));
|
|
8626
|
-
DEFAULT_UI_DIR =
|
|
9183
|
+
DEFAULT_UI_DIR = join20(here, "..", "ui");
|
|
8627
9184
|
MIME = {
|
|
8628
9185
|
".html": "text/html; charset=utf-8",
|
|
8629
9186
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -8655,8 +9212,8 @@ var init_server = __esm({
|
|
|
8655
9212
|
// src/server/cli.ts
|
|
8656
9213
|
init_state();
|
|
8657
9214
|
init_stats();
|
|
8658
|
-
import { resolve as resolve11, dirname as dirname5, join as
|
|
8659
|
-
import { readFileSync as
|
|
9215
|
+
import { resolve as resolve11, dirname as dirname5, join as join21, basename as basename2 } from "path";
|
|
9216
|
+
import { readFileSync as readFileSync26, existsSync as existsSync14, mkdirSync as mkdirSync6, cpSync } from "fs";
|
|
8660
9217
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8661
9218
|
|
|
8662
9219
|
// src/server/agent-cli.ts
|
|
@@ -8788,13 +9345,17 @@ init_pending_batch();
|
|
|
8788
9345
|
init_context_batch_run();
|
|
8789
9346
|
init_pending_context_batch();
|
|
8790
9347
|
init_estimate();
|
|
9348
|
+
init_glossary_suggest();
|
|
8791
9349
|
init_pricing();
|
|
9350
|
+
init_price_fetch();
|
|
9351
|
+
init_price_cache();
|
|
8792
9352
|
init_log();
|
|
8793
9353
|
init_scan();
|
|
8794
9354
|
init_scanner();
|
|
8795
9355
|
init_usage();
|
|
8796
9356
|
init_context();
|
|
8797
9357
|
init_run2();
|
|
9358
|
+
init_registry();
|
|
8798
9359
|
init_outputs();
|
|
8799
9360
|
|
|
8800
9361
|
// src/server/lint/locate.ts
|
|
@@ -8871,7 +9432,7 @@ function formatSarif(report, ctx) {
|
|
|
8871
9432
|
}
|
|
8872
9433
|
|
|
8873
9434
|
// 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"];
|
|
9435
|
+
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
9436
|
var isCommand = (s) => s != null && COMMANDS.includes(s);
|
|
8876
9437
|
function parseArgs(argv) {
|
|
8877
9438
|
const statePath = resolve11(process.cwd(), "glotfile.json");
|
|
@@ -8947,6 +9508,7 @@ function parseArgs(argv) {
|
|
|
8947
9508
|
else if (flag === "--batch") args.batch = true;
|
|
8948
9509
|
else if (flag === "--wait") args.wait = true;
|
|
8949
9510
|
else if (flag === "--print") args.print = true;
|
|
9511
|
+
else if (flag === "--refresh") args.refresh = true;
|
|
8950
9512
|
else if (flag === "--state" && next) {
|
|
8951
9513
|
args.states = next.split(",");
|
|
8952
9514
|
i++;
|
|
@@ -9042,7 +9604,7 @@ function translateSelection(args) {
|
|
|
9042
9604
|
}
|
|
9043
9605
|
function readStdin() {
|
|
9044
9606
|
try {
|
|
9045
|
-
return
|
|
9607
|
+
return readFileSync26(0, "utf8");
|
|
9046
9608
|
} catch {
|
|
9047
9609
|
return "";
|
|
9048
9610
|
}
|
|
@@ -9310,15 +9872,15 @@ async function runContextBatchAction(args, pending, action, projectRoot) {
|
|
|
9310
9872
|
function sarifContextFor(statePath) {
|
|
9311
9873
|
if (detectFormat(statePath) === "split") {
|
|
9312
9874
|
const dir = splitDirFor(statePath);
|
|
9313
|
-
const keysPath =
|
|
9875
|
+
const keysPath = join21(dir, "keys.json");
|
|
9314
9876
|
return {
|
|
9315
9877
|
keysUri: `${basename2(dir)}/keys.json`,
|
|
9316
|
-
keysRawText: existsSync14(keysPath) ?
|
|
9878
|
+
keysRawText: existsSync14(keysPath) ? readFileSync26(keysPath, "utf8") : ""
|
|
9317
9879
|
};
|
|
9318
9880
|
}
|
|
9319
9881
|
return {
|
|
9320
9882
|
keysUri: basename2(statePath),
|
|
9321
|
-
keysRawText: existsSync14(statePath) ?
|
|
9883
|
+
keysRawText: existsSync14(statePath) ? readFileSync26(statePath, "utf8") : ""
|
|
9322
9884
|
};
|
|
9323
9885
|
}
|
|
9324
9886
|
function printReport(report, format, statePath) {
|
|
@@ -9327,6 +9889,18 @@ function printReport(report, format, statePath) {
|
|
|
9327
9889
|
else console.log(formatText(report).trimEnd());
|
|
9328
9890
|
}
|
|
9329
9891
|
async function runLintCmd(args) {
|
|
9892
|
+
if (args.ruleIds) {
|
|
9893
|
+
const unknown = unknownRuleIds(args.ruleIds);
|
|
9894
|
+
if (unknown.length > 0) {
|
|
9895
|
+
for (const id of unknown) {
|
|
9896
|
+
const hint = suggestRuleId(id);
|
|
9897
|
+
console.error(`Unknown --rule '${id}'.${hint ? ` Did you mean '${hint}'?` : ""}`);
|
|
9898
|
+
}
|
|
9899
|
+
console.error(`Valid rules: ${RULE_IDS.join(", ")}.`);
|
|
9900
|
+
process.exitCode = 1;
|
|
9901
|
+
return;
|
|
9902
|
+
}
|
|
9903
|
+
}
|
|
9330
9904
|
const state = loadState(args.statePath);
|
|
9331
9905
|
if (args.accept) {
|
|
9332
9906
|
const { acceptFindings: acceptFindings2 } = await Promise.resolve().then(() => (init_accept(), accept_exports));
|
|
@@ -9468,29 +10042,30 @@ async function runBuildContext(args) {
|
|
|
9468
10042
|
console.log("No keys need context.");
|
|
9469
10043
|
return;
|
|
9470
10044
|
}
|
|
10045
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
10046
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
10047
|
+
if (args.estimate) {
|
|
10048
|
+
const est = estimateContext(targets, aiCfg);
|
|
10049
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
10050
|
+
console.log(`Estimate for ${fmt(est.keys)} key(s) in ${fmt(est.batches)} batch(es) \u2014 ${aiCfg.provider} \xB7 ${aiCfg.model}`);
|
|
10051
|
+
console.log(`Totals: ~${fmt(est.inputTokens)} input / ~${fmt(est.outputTokens)} output tokens`);
|
|
10052
|
+
if (est.pricing) {
|
|
10053
|
+
const cost = est.estimatedCost;
|
|
10054
|
+
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)`);
|
|
10055
|
+
} else {
|
|
10056
|
+
console.log("No pricing known for this model \u2014 set inputPricePerMTok/outputPricePerMTok in your AI settings for a dollar estimate.");
|
|
10057
|
+
}
|
|
10058
|
+
return;
|
|
10059
|
+
}
|
|
9471
10060
|
let provider;
|
|
9472
10061
|
try {
|
|
9473
|
-
provider = makeProvider(
|
|
10062
|
+
provider = makeProvider(aiCfg);
|
|
9474
10063
|
} catch (e) {
|
|
9475
10064
|
console.error(e.message);
|
|
9476
10065
|
process.exitCode = 1;
|
|
9477
10066
|
return;
|
|
9478
10067
|
}
|
|
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
10068
|
const system = buildContextSystemPrompt();
|
|
9493
|
-
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
9494
10069
|
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
9495
10070
|
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
9496
10071
|
if (args.batch) {
|
|
@@ -9546,6 +10121,71 @@ async function runBuildContext(args) {
|
|
|
9546
10121
|
console.log(`Wrote context for ${written} key(s).`);
|
|
9547
10122
|
for (const e of errors) console.warn(`skip ${e.key}: ${e.error}`);
|
|
9548
10123
|
}
|
|
10124
|
+
async function runSuggestGlossary(args) {
|
|
10125
|
+
const state = loadState(args.statePath);
|
|
10126
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
10127
|
+
const sources = selectGlossarySources(state, { keyGlob: args.keyGlob, limit: args.limit, since: args.since });
|
|
10128
|
+
if (!sources.length) {
|
|
10129
|
+
console.log("No source strings to scan.");
|
|
10130
|
+
return;
|
|
10131
|
+
}
|
|
10132
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
10133
|
+
const known = knownTermList(state);
|
|
10134
|
+
if (args.estimate) {
|
|
10135
|
+
const est = estimateGlossarySuggest(sources, known, aiCfg);
|
|
10136
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
10137
|
+
console.log(`Estimate for ${fmt(est.sources)} source string(s) in ${fmt(est.batches)} batch(es) \u2014 ${aiCfg.provider} \xB7 ${aiCfg.model}`);
|
|
10138
|
+
console.log(`Totals: ~${fmt(est.inputTokens)} input / ~${fmt(est.outputTokens)} output tokens`);
|
|
10139
|
+
if (est.pricing) {
|
|
10140
|
+
const cost = est.estimatedCost;
|
|
10141
|
+
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)`);
|
|
10142
|
+
} else {
|
|
10143
|
+
console.log("No pricing known for this model \u2014 set inputPricePerMTok/outputPricePerMTok in your AI settings for a dollar estimate.");
|
|
10144
|
+
}
|
|
10145
|
+
return;
|
|
10146
|
+
}
|
|
10147
|
+
let provider;
|
|
10148
|
+
try {
|
|
10149
|
+
provider = makeProvider(aiCfg);
|
|
10150
|
+
} catch (e) {
|
|
10151
|
+
console.error(e.message);
|
|
10152
|
+
process.exitCode = 1;
|
|
10153
|
+
return;
|
|
10154
|
+
}
|
|
10155
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
10156
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
10157
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
10158
|
+
const chunks = [];
|
|
10159
|
+
for (let i = 0; i < sources.length; i += batchSize) chunks.push(sources.slice(i, i + batchSize));
|
|
10160
|
+
const all = [];
|
|
10161
|
+
let done = 0;
|
|
10162
|
+
let next = 0;
|
|
10163
|
+
async function worker() {
|
|
10164
|
+
while (next < chunks.length) {
|
|
10165
|
+
const chunkRows = chunks[next++];
|
|
10166
|
+
try {
|
|
10167
|
+
const raw = await provider.complete({ system, content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunkRows, known) }], schema: GLOSSARY_SUGGEST_SCHEMA });
|
|
10168
|
+
const batch = raw;
|
|
10169
|
+
all.push(...batch.terms ?? []);
|
|
10170
|
+
} catch (e) {
|
|
10171
|
+
console.warn(`batch failed: ${e.message}`);
|
|
10172
|
+
}
|
|
10173
|
+
done += chunkRows.length;
|
|
10174
|
+
console.log(`[${done}/${sources.length}] scanned`);
|
|
10175
|
+
}
|
|
10176
|
+
}
|
|
10177
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
10178
|
+
const added = mergeGlossarySuggestions(state, dedupeTerms(all));
|
|
10179
|
+
saveState(args.statePath, state);
|
|
10180
|
+
appendLog(projectRoot, {
|
|
10181
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10182
|
+
kind: "glossary",
|
|
10183
|
+
summary: `Suggested ${added.length} glossary term(s)`,
|
|
10184
|
+
model: aiCfg.model
|
|
10185
|
+
});
|
|
10186
|
+
console.log(`Found ${added.length} new candidate term(s). Review them in the glossary UI.`);
|
|
10187
|
+
for (const s of added) console.log(` \u2022 ${s.term}${s.note ? ` \u2014 ${s.note}` : ""}`);
|
|
10188
|
+
}
|
|
9549
10189
|
async function runScanCmd(args) {
|
|
9550
10190
|
const state = loadState(args.statePath);
|
|
9551
10191
|
const projectRoot = dirname5(resolve11(args.statePath));
|
|
@@ -9619,10 +10259,10 @@ function runSplit(args) {
|
|
|
9619
10259
|
`Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
|
|
9620
10260
|
);
|
|
9621
10261
|
}
|
|
9622
|
-
var SKILL_SRC =
|
|
10262
|
+
var SKILL_SRC = join21(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
|
|
9623
10263
|
function runSkill(args) {
|
|
9624
10264
|
if (args.print) {
|
|
9625
|
-
console.log(
|
|
10265
|
+
console.log(readFileSync26(join21(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
|
|
9626
10266
|
return;
|
|
9627
10267
|
}
|
|
9628
10268
|
const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
|
|
@@ -9801,6 +10441,39 @@ function runApply(args) {
|
|
|
9801
10441
|
console.log(JSON.stringify({ applied: r.applied, keysTouched: r.keysTouched, saved, dryRun: !!args.dryRun, errors: r.errors }, null, 2));
|
|
9802
10442
|
if (r.errors.length) process.exitCode = 1;
|
|
9803
10443
|
}
|
|
10444
|
+
async function runPrices(args) {
|
|
10445
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
10446
|
+
if (args.refresh) {
|
|
10447
|
+
try {
|
|
10448
|
+
const res = await refreshPrices();
|
|
10449
|
+
invalidatePriceCache();
|
|
10450
|
+
console.log(`Updated ${res.modelCount} model price(s) from ${res.source}.`);
|
|
10451
|
+
console.log(`Fetched ${new Date(res.fetchedAt).toLocaleString()} \u2192 ${res.path}`);
|
|
10452
|
+
} catch (e) {
|
|
10453
|
+
console.error(`Could not refresh prices: ${e.message}`);
|
|
10454
|
+
console.error("Existing cached prices (if any) are unchanged.");
|
|
10455
|
+
process.exitCode = 1;
|
|
10456
|
+
}
|
|
10457
|
+
return;
|
|
10458
|
+
}
|
|
10459
|
+
const cache2 = loadPriceCache();
|
|
10460
|
+
if (cache2) {
|
|
10461
|
+
const when = cache2.fetchedAt ? new Date(cache2.fetchedAt).toLocaleString() : "unknown time";
|
|
10462
|
+
console.log(`Price cache: ${Object.keys(cache2.models).length} model(s) from ${cache2.source}, fetched ${when}.`);
|
|
10463
|
+
console.log(`Location: ${defaultPriceCachePath()}`);
|
|
10464
|
+
} else {
|
|
10465
|
+
console.log("No price cache yet. Run `glotfile prices --refresh` to fetch the latest from models.dev.");
|
|
10466
|
+
}
|
|
10467
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
10468
|
+
const pricing = resolvePricing(aiCfg, cache2);
|
|
10469
|
+
if (pricing) {
|
|
10470
|
+
console.log(`
|
|
10471
|
+
${aiCfg.provider} \xB7 ${aiCfg.model}: $${pricing.inputPerMTok}/$${pricing.outputPerMTok} per MTok (${pricing.source}).`);
|
|
10472
|
+
} else {
|
|
10473
|
+
console.log(`
|
|
10474
|
+
No price known for ${aiCfg.provider} \xB7 ${aiCfg.model}. Set inputPricePerMTok/outputPricePerMTok in AI settings, or refresh.`);
|
|
10475
|
+
}
|
|
10476
|
+
}
|
|
9804
10477
|
var GLOBAL_OPTS = [
|
|
9805
10478
|
["-f, --file <path>", "State file to use (default: ./glotfile.json)"],
|
|
9806
10479
|
["-h, --help", "Show this help"]
|
|
@@ -9879,12 +10552,24 @@ var COMMAND_HELP = {
|
|
|
9879
10552
|
},
|
|
9880
10553
|
"build-context": {
|
|
9881
10554
|
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]",
|
|
10555
|
+
usage: "glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>] [--estimate] [--batch]",
|
|
9883
10556
|
options: [
|
|
9884
10557
|
["--all", "(Re)build context for every key, not just those missing it"],
|
|
9885
10558
|
["--key <glob>", "Only keys matching this glob"],
|
|
9886
10559
|
["--limit <n>", "Process at most n keys"],
|
|
9887
|
-
["--since <date>", "Only keys added or changed since this date"]
|
|
10560
|
+
["--since <date>", "Only keys added or changed since this date"],
|
|
10561
|
+
["--estimate", "Print batches, tokens and estimated cost without building"],
|
|
10562
|
+
["--batch", "Submit via the provider's batch API (50% cost, async; anthropic only)"]
|
|
10563
|
+
]
|
|
10564
|
+
},
|
|
10565
|
+
"suggest-glossary": {
|
|
10566
|
+
summary: "AI-scan source strings for candidate glossary terms (adds a review queue; existing terms are skipped).",
|
|
10567
|
+
usage: "glotfile suggest-glossary [--key <glob>] [--limit <n>] [--since <date>] [--estimate]",
|
|
10568
|
+
options: [
|
|
10569
|
+
["--key <glob>", "Only scan keys matching this glob"],
|
|
10570
|
+
["--limit <n>", "Scan at most n source strings"],
|
|
10571
|
+
["--since <date>", "Only keys added since this date"],
|
|
10572
|
+
["--estimate", "Print batches, tokens and estimated cost without scanning"]
|
|
9888
10573
|
]
|
|
9889
10574
|
},
|
|
9890
10575
|
scan: {
|
|
@@ -9923,6 +10608,13 @@ var COMMAND_HELP = {
|
|
|
9923
10608
|
["cancel", "Cancel the pending batch and discard the handle"]
|
|
9924
10609
|
]
|
|
9925
10610
|
},
|
|
10611
|
+
prices: {
|
|
10612
|
+
summary: "Show or refresh the model price cache used for cost estimates (models.dev).",
|
|
10613
|
+
usage: "glotfile prices [--refresh]",
|
|
10614
|
+
options: [
|
|
10615
|
+
["--refresh", "Fetch the latest prices from models.dev into the cache (the only command that hits the network)"]
|
|
10616
|
+
]
|
|
10617
|
+
},
|
|
9926
10618
|
get: {
|
|
9927
10619
|
summary: "Extract values from the catalog (filtered) without loading the whole file. Prints JSON.",
|
|
9928
10620
|
usage: "glotfile get [<key-glob>\u2026] [--key <glob>] [--locale <list>] [--state <list>] [--fields <list>] [--keys-only] [--format json|ndjson]",
|
|
@@ -10012,8 +10704,8 @@ ${formatOpts([...options, ...GLOBAL_OPTS])}`);
|
|
|
10012
10704
|
);
|
|
10013
10705
|
}
|
|
10014
10706
|
function printVersion() {
|
|
10015
|
-
const pkgPath =
|
|
10016
|
-
console.log(JSON.parse(
|
|
10707
|
+
const pkgPath = join21(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
10708
|
+
console.log(JSON.parse(readFileSync26(pkgPath, "utf8")).version);
|
|
10017
10709
|
}
|
|
10018
10710
|
async function main(argv) {
|
|
10019
10711
|
const args = parseArgs(argv);
|
|
@@ -10033,11 +10725,13 @@ async function main(argv) {
|
|
|
10033
10725
|
if (args.command === "import") return runImportCmd(args);
|
|
10034
10726
|
if (args.command === "sync") return runSyncCmd(args);
|
|
10035
10727
|
if (args.command === "build-context") return runBuildContext(args);
|
|
10728
|
+
if (args.command === "suggest-glossary") return runSuggestGlossary(args);
|
|
10036
10729
|
if (args.command === "scan") return runScanCmd(args);
|
|
10037
10730
|
if (args.command === "prune") return runPrune(args);
|
|
10038
10731
|
if (args.command === "split") return runSplit(args);
|
|
10039
10732
|
if (args.command === "skill") return runSkill(args);
|
|
10040
10733
|
if (args.command === "batch") return runBatch(args);
|
|
10734
|
+
if (args.command === "prices") return runPrices(args);
|
|
10041
10735
|
if (args.command === "get") return runGetCmd(args);
|
|
10042
10736
|
if (args.command === "stats") return runStatsCmd(args);
|
|
10043
10737
|
if (args.command === "set") return runSet(args);
|