glotfile 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/cli.js +1250 -254
- package/dist/server/server.js +1032 -309
- package/dist/ui/assets/index-Bjwiz6KQ.css +1 -0
- package/dist/ui/assets/{index-5Imdw0oX.js → index-CrceYqHe.js} +79 -12
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BaHu118N.css +0 -1
package/dist/server/server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Hono as Hono2 } from "hono";
|
|
3
3
|
import { serve } from "@hono/node-server";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import { dirname as dirname4, join as
|
|
5
|
+
import { dirname as dirname4, join as join21, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
6
6
|
import { readFile, stat } from "fs/promises";
|
|
7
7
|
import { createServer } from "net";
|
|
8
8
|
import open from "open";
|
|
@@ -270,7 +270,8 @@ function validate(raw) {
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
if (raw.glossary !== void 0 && !Array.isArray(raw.glossary)) fail("glossary must be an array");
|
|
273
|
-
|
|
273
|
+
if (raw.glossarySuggestions !== void 0 && !Array.isArray(raw.glossarySuggestions)) fail("glossarySuggestions must be an array");
|
|
274
|
+
const state = { glossary: [], glossarySuggestions: [], ...raw };
|
|
274
275
|
return state;
|
|
275
276
|
}
|
|
276
277
|
function defaultState() {
|
|
@@ -289,6 +290,7 @@ function defaultState() {
|
|
|
289
290
|
autoExport: true
|
|
290
291
|
},
|
|
291
292
|
glossary: [],
|
|
293
|
+
glossarySuggestions: [],
|
|
292
294
|
keys: {}
|
|
293
295
|
};
|
|
294
296
|
}
|
|
@@ -756,6 +758,39 @@ function upsertGlossaryEntry(state, entry) {
|
|
|
756
758
|
function deleteGlossaryEntry(state, term) {
|
|
757
759
|
state.glossary = state.glossary.filter((e) => e.term !== term);
|
|
758
760
|
}
|
|
761
|
+
function normGlossaryTerm(term) {
|
|
762
|
+
return term.trim().toLowerCase();
|
|
763
|
+
}
|
|
764
|
+
function mergeGlossarySuggestions(state, found) {
|
|
765
|
+
const known = /* @__PURE__ */ new Set();
|
|
766
|
+
for (const g of state.glossary) known.add(normGlossaryTerm(g.term));
|
|
767
|
+
for (const s of state.glossarySuggestions) known.add(normGlossaryTerm(s.term));
|
|
768
|
+
const added = [];
|
|
769
|
+
for (const f of found) {
|
|
770
|
+
const term = f.term.trim();
|
|
771
|
+
if (!term) continue;
|
|
772
|
+
const key = normGlossaryTerm(term);
|
|
773
|
+
if (known.has(key)) continue;
|
|
774
|
+
known.add(key);
|
|
775
|
+
const sug = { term, status: "pending" };
|
|
776
|
+
if (f.note?.trim()) sug.note = f.note.trim();
|
|
777
|
+
if (f.doNotTranslate) sug.doNotTranslate = true;
|
|
778
|
+
if (f.caseSensitive) sug.caseSensitive = true;
|
|
779
|
+
if (f.wholeWord === false) sug.wholeWord = false;
|
|
780
|
+
state.glossarySuggestions.push(sug);
|
|
781
|
+
added.push(sug);
|
|
782
|
+
}
|
|
783
|
+
return added;
|
|
784
|
+
}
|
|
785
|
+
function dismissGlossarySuggestion(state, term) {
|
|
786
|
+
const key = normGlossaryTerm(term);
|
|
787
|
+
const s = state.glossarySuggestions.find((x) => normGlossaryTerm(x.term) === key);
|
|
788
|
+
if (s) s.status = "dismissed";
|
|
789
|
+
}
|
|
790
|
+
function removeGlossarySuggestion(state, term) {
|
|
791
|
+
const key = normGlossaryTerm(term);
|
|
792
|
+
state.glossarySuggestions = state.glossarySuggestions.filter((x) => normGlossaryTerm(x.term) !== key);
|
|
793
|
+
}
|
|
759
794
|
function addCustomWord(state, word) {
|
|
760
795
|
const w = word.trim();
|
|
761
796
|
if (!w) return;
|
|
@@ -786,6 +821,158 @@ function applyMachineTranslationForms(state, key, locale, forms, clock = systemC
|
|
|
786
821
|
return true;
|
|
787
822
|
}
|
|
788
823
|
|
|
824
|
+
// src/server/ai/glossary-suggest.ts
|
|
825
|
+
function globToRegExp(glob) {
|
|
826
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
827
|
+
return new RegExp(`^${escaped}$`);
|
|
828
|
+
}
|
|
829
|
+
function selectGlossarySources(state, opts) {
|
|
830
|
+
const keyRe = opts.keyGlob ? globToRegExp(opts.keyGlob) : null;
|
|
831
|
+
let rows = [];
|
|
832
|
+
for (const key of Object.keys(state.keys)) {
|
|
833
|
+
if (keyRe && !keyRe.test(key)) continue;
|
|
834
|
+
const entry = state.keys[key];
|
|
835
|
+
if (opts.since) {
|
|
836
|
+
if (!entry.createdAt || entry.createdAt < opts.since) continue;
|
|
837
|
+
}
|
|
838
|
+
const lv = entry.values[state.config.sourceLocale];
|
|
839
|
+
const source = (lv?.value ?? lv?.forms?.other ?? "").trim();
|
|
840
|
+
if (!source) continue;
|
|
841
|
+
rows.push({ key, source });
|
|
842
|
+
}
|
|
843
|
+
rows.sort((a, b) => {
|
|
844
|
+
const ta = state.keys[a.key].createdAt ?? "";
|
|
845
|
+
const tb = state.keys[b.key].createdAt ?? "";
|
|
846
|
+
return tb.localeCompare(ta) || a.key.localeCompare(b.key);
|
|
847
|
+
});
|
|
848
|
+
if (opts.limit !== void 0) rows = rows.slice(0, opts.limit);
|
|
849
|
+
return rows;
|
|
850
|
+
}
|
|
851
|
+
function knownTermList(state) {
|
|
852
|
+
const out = /* @__PURE__ */ new Set();
|
|
853
|
+
for (const g of state.glossary) out.add(g.term);
|
|
854
|
+
for (const s of state.glossarySuggestions) out.add(s.term);
|
|
855
|
+
return [...out];
|
|
856
|
+
}
|
|
857
|
+
function buildGlossarySuggestSystemPrompt() {
|
|
858
|
+
return [
|
|
859
|
+
"You identify GLOSSARY-CANDIDATE terms in a UI string catalog so they translate consistently.",
|
|
860
|
+
"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).",
|
|
861
|
+
"You are given source strings (the app's original language). Return the candidate terms you find.",
|
|
862
|
+
"Rules:",
|
|
863
|
+
"- Only surface terms a translator would benefit from pinning. IGNORE ordinary words, verbs, and generic UI labels (e.g. 'Save', 'Cancel', 'Welcome').",
|
|
864
|
+
"- Prefer terms that recur or are clearly proper nouns / product names / acronyms.",
|
|
865
|
+
"- Set doNotTranslate: true for brand/product names, code identifiers, and acronyms that must stay verbatim in every language.",
|
|
866
|
+
"- Set caseSensitive: true only when casing is meaningful (e.g. an all-caps acronym that must not match a lowercase common word).",
|
|
867
|
+
"- Set wholeWord: false ONLY if the term should also match inside larger words; otherwise omit it (whole-word is the default).",
|
|
868
|
+
"- note: one short phrase on why it's a term (e.g. 'product name', 'industry acronym', 'recurring UI concept'). Keep it under 80 characters.",
|
|
869
|
+
"- Do NOT return any term in the provided 'Already known' list.",
|
|
870
|
+
"- Return the term exactly as it appears in the source (preserve casing)."
|
|
871
|
+
].join("\n");
|
|
872
|
+
}
|
|
873
|
+
function buildGlossarySuggestBatchPrompt(sources, knownTerms) {
|
|
874
|
+
const known = knownTerms.length ? knownTerms.join(", ") : "(none yet)";
|
|
875
|
+
const lines = sources.map((s) => `- [${s.key}] ${s.source}`).join("\n");
|
|
876
|
+
return [
|
|
877
|
+
`Already known (do NOT return these): ${known}`,
|
|
878
|
+
"",
|
|
879
|
+
"Source strings:",
|
|
880
|
+
lines,
|
|
881
|
+
"",
|
|
882
|
+
'Return JSON {"terms":[{"term","note?","doNotTranslate?","caseSensitive?","wholeWord?"}]}. Return an empty array if you find no good candidates.'
|
|
883
|
+
].join("\n");
|
|
884
|
+
}
|
|
885
|
+
var GLOSSARY_SUGGEST_SCHEMA = {
|
|
886
|
+
type: "object",
|
|
887
|
+
properties: {
|
|
888
|
+
terms: {
|
|
889
|
+
type: "array",
|
|
890
|
+
items: {
|
|
891
|
+
type: "object",
|
|
892
|
+
properties: {
|
|
893
|
+
term: { type: "string" },
|
|
894
|
+
note: { type: "string" },
|
|
895
|
+
doNotTranslate: { type: "boolean" },
|
|
896
|
+
caseSensitive: { type: "boolean" },
|
|
897
|
+
wholeWord: { type: "boolean" }
|
|
898
|
+
},
|
|
899
|
+
required: ["term"],
|
|
900
|
+
additionalProperties: false
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
},
|
|
904
|
+
required: ["terms"],
|
|
905
|
+
additionalProperties: false
|
|
906
|
+
};
|
|
907
|
+
function dedupeTerms(terms) {
|
|
908
|
+
const seen = /* @__PURE__ */ new Set();
|
|
909
|
+
const out = [];
|
|
910
|
+
for (const t of terms) {
|
|
911
|
+
const term = t.term?.trim();
|
|
912
|
+
if (!term) continue;
|
|
913
|
+
const key = term.toLowerCase();
|
|
914
|
+
if (seen.has(key)) continue;
|
|
915
|
+
seen.add(key);
|
|
916
|
+
out.push({ ...t, term });
|
|
917
|
+
}
|
|
918
|
+
return out;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/server/glossary.ts
|
|
922
|
+
function escapeRegExp(s) {
|
|
923
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
924
|
+
}
|
|
925
|
+
function contains(haystack, needle, caseSensitive, wholeWord) {
|
|
926
|
+
if (!wholeWord) {
|
|
927
|
+
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
928
|
+
}
|
|
929
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp(needle)}(?![\\p{L}\\p{N}])`, caseSensitive ? "u" : "iu");
|
|
930
|
+
return re.test(haystack);
|
|
931
|
+
}
|
|
932
|
+
function termInSource(source, entry) {
|
|
933
|
+
return contains(source, entry.term, entry.caseSensitive, entry.wholeWord ?? true);
|
|
934
|
+
}
|
|
935
|
+
function relevantGlossary(source, targetLocale, glossary) {
|
|
936
|
+
const hints = [];
|
|
937
|
+
for (const entry of glossary) {
|
|
938
|
+
if (!termInSource(source, entry)) continue;
|
|
939
|
+
hints.push({
|
|
940
|
+
term: entry.term,
|
|
941
|
+
doNotTranslate: entry.doNotTranslate,
|
|
942
|
+
forced: entry.translations?.[targetLocale],
|
|
943
|
+
notes: entry.notes
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
return hints;
|
|
947
|
+
}
|
|
948
|
+
function sourceKeysForTerm(state, term, opts = {}) {
|
|
949
|
+
const pseudo = { term, caseSensitive: opts.caseSensitive, wholeWord: opts.wholeWord };
|
|
950
|
+
const out = [];
|
|
951
|
+
for (const [key, entry] of Object.entries(state.keys)) {
|
|
952
|
+
const lv = entry.values[state.config.sourceLocale];
|
|
953
|
+
const text = lv?.value ?? lv?.forms?.other ?? "";
|
|
954
|
+
if (text && termInSource(text, pseudo)) out.push(key);
|
|
955
|
+
}
|
|
956
|
+
return out;
|
|
957
|
+
}
|
|
958
|
+
function glossaryViolations(source, value, targetLocale, glossary) {
|
|
959
|
+
const out = [];
|
|
960
|
+
for (const entry of glossary) {
|
|
961
|
+
if (!termInSource(source, entry)) continue;
|
|
962
|
+
if (entry.doNotTranslate) {
|
|
963
|
+
if (!contains(value, entry.term, entry.caseSensitive)) {
|
|
964
|
+
out.push({ term: entry.term, expected: entry.term, kind: "do-not-translate" });
|
|
965
|
+
}
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const forced = entry.translations?.[targetLocale];
|
|
969
|
+
if (forced && !contains(value, forced, entry.caseSensitive)) {
|
|
970
|
+
out.push({ term: entry.term, expected: forced, kind: "forced" });
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return out;
|
|
974
|
+
}
|
|
975
|
+
|
|
789
976
|
// src/server/lint/accept.ts
|
|
790
977
|
function acceptFindings(state, findings, opts = {}, clock = systemClock) {
|
|
791
978
|
const byRule = {};
|
|
@@ -824,7 +1011,7 @@ function ensureGlotfileDir(projectRoot) {
|
|
|
824
1011
|
}
|
|
825
1012
|
|
|
826
1013
|
// src/server/glob.ts
|
|
827
|
-
function
|
|
1014
|
+
function globToRegExp2(glob) {
|
|
828
1015
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
829
1016
|
return new RegExp(`^${escaped}$`);
|
|
830
1017
|
}
|
|
@@ -875,7 +1062,7 @@ function computeUsedKeys(state, cache2) {
|
|
|
875
1062
|
matchers.push(literalMatcher(l.literal));
|
|
876
1063
|
}
|
|
877
1064
|
}
|
|
878
|
-
const keep = (state.config.scan?.keep ?? []).map(
|
|
1065
|
+
const keep = (state.config.scan?.keep ?? []).map(globToRegExp2);
|
|
879
1066
|
return Object.keys(state.keys).filter((key) => exact.has(key) || prefixes.some((p) => key.startsWith(p)) || matchers.some((matches) => matches(key)) || keep.some((re) => re.test(key))).sort();
|
|
880
1067
|
}
|
|
881
1068
|
function escapeRe(s) {
|
|
@@ -1403,7 +1590,7 @@ var MAX_CONTEXT_LENGTH = 500;
|
|
|
1403
1590
|
var SNIPPET_WINDOW = 15;
|
|
1404
1591
|
var MAX_SNIPPETS = 3;
|
|
1405
1592
|
var EXCLUDED_DIRS = ["node_modules/", "vendor/", "dist/", ".git/", ".glotfile/"];
|
|
1406
|
-
function
|
|
1593
|
+
function globToRegExp3(glob) {
|
|
1407
1594
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1408
1595
|
return new RegExp(`^${escaped}$`);
|
|
1409
1596
|
}
|
|
@@ -1433,6 +1620,21 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
1433
1620
|
}
|
|
1434
1621
|
return snippets;
|
|
1435
1622
|
}
|
|
1623
|
+
function attachUsageSnippets(targets, cache2, projectRoot) {
|
|
1624
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
1625
|
+
for (const target of targets) {
|
|
1626
|
+
const allRefs = Object.entries(cache2.files).flatMap(
|
|
1627
|
+
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
1628
|
+
key: r.key,
|
|
1629
|
+
file,
|
|
1630
|
+
line: r.line,
|
|
1631
|
+
col: r.col,
|
|
1632
|
+
scanner: r.scanner
|
|
1633
|
+
}))
|
|
1634
|
+
);
|
|
1635
|
+
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1436
1638
|
function buildUsageIndex(cache2) {
|
|
1437
1639
|
const index = /* @__PURE__ */ new Map();
|
|
1438
1640
|
for (const [file, entry] of Object.entries(cache2.files)) {
|
|
@@ -1446,7 +1648,7 @@ function buildUsageIndex(cache2) {
|
|
|
1446
1648
|
}
|
|
1447
1649
|
function selectContextTargets(state, opts, cache2, lastRunAt) {
|
|
1448
1650
|
const cutoff = opts.all ? void 0 : opts.since ?? lastRunAt;
|
|
1449
|
-
const keyRe = opts.keyGlob ?
|
|
1651
|
+
const keyRe = opts.keyGlob ? globToRegExp3(opts.keyGlob) : null;
|
|
1450
1652
|
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
1451
1653
|
const usageIndex = buildUsageIndex(cache2);
|
|
1452
1654
|
let candidates = [];
|
|
@@ -1668,41 +1870,6 @@ function computeStats(state) {
|
|
|
1668
1870
|
};
|
|
1669
1871
|
}
|
|
1670
1872
|
|
|
1671
|
-
// src/server/glossary.ts
|
|
1672
|
-
function contains(haystack, needle, caseSensitive) {
|
|
1673
|
-
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
1674
|
-
}
|
|
1675
|
-
function relevantGlossary(source, targetLocale, glossary) {
|
|
1676
|
-
const hints = [];
|
|
1677
|
-
for (const entry of glossary) {
|
|
1678
|
-
if (!contains(source, entry.term, entry.caseSensitive)) continue;
|
|
1679
|
-
hints.push({
|
|
1680
|
-
term: entry.term,
|
|
1681
|
-
doNotTranslate: entry.doNotTranslate,
|
|
1682
|
-
forced: entry.translations?.[targetLocale],
|
|
1683
|
-
notes: entry.notes
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
|
-
return hints;
|
|
1687
|
-
}
|
|
1688
|
-
function glossaryViolations(source, value, targetLocale, glossary) {
|
|
1689
|
-
const out = [];
|
|
1690
|
-
for (const entry of glossary) {
|
|
1691
|
-
if (!contains(source, entry.term, entry.caseSensitive)) continue;
|
|
1692
|
-
if (entry.doNotTranslate) {
|
|
1693
|
-
if (!contains(value, entry.term, entry.caseSensitive)) {
|
|
1694
|
-
out.push({ term: entry.term, expected: entry.term, kind: "do-not-translate" });
|
|
1695
|
-
}
|
|
1696
|
-
continue;
|
|
1697
|
-
}
|
|
1698
|
-
const forced = entry.translations?.[targetLocale];
|
|
1699
|
-
if (forced && !contains(value, forced, entry.caseSensitive)) {
|
|
1700
|
-
out.push({ term: entry.term, expected: forced, kind: "forced" });
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
return out;
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
1873
|
// src/server/spell.ts
|
|
1707
1874
|
var instances = /* @__PURE__ */ new Map();
|
|
1708
1875
|
var loading = /* @__PURE__ */ new Set();
|
|
@@ -2134,7 +2301,7 @@ async function runLint(state, options = {}) {
|
|
|
2134
2301
|
spellers,
|
|
2135
2302
|
allowWords
|
|
2136
2303
|
};
|
|
2137
|
-
const ignoreRes = (config.ignore ?? []).map(
|
|
2304
|
+
const ignoreRes = (config.ignore ?? []).map(globToRegExp2);
|
|
2138
2305
|
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
2139
2306
|
const findings = [];
|
|
2140
2307
|
let suppressed = 0;
|
|
@@ -3084,7 +3251,7 @@ function checkOutputs(state, root) {
|
|
|
3084
3251
|
}
|
|
3085
3252
|
|
|
3086
3253
|
// src/server/api.ts
|
|
3087
|
-
import { readFileSync as
|
|
3254
|
+
import { readFileSync as readFileSync26, existsSync as existsSync14, readdirSync as readdirSync16, statSync as statSync10, rmSync as rmSync7 } from "fs";
|
|
3088
3255
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
3089
3256
|
|
|
3090
3257
|
// src/server/ai/anthropic.ts
|
|
@@ -3330,6 +3497,48 @@ async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, o
|
|
|
3330
3497
|
return results;
|
|
3331
3498
|
}
|
|
3332
3499
|
|
|
3500
|
+
// src/server/ai/price-cache.ts
|
|
3501
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
3502
|
+
import { homedir } from "os";
|
|
3503
|
+
import { join as join4 } from "path";
|
|
3504
|
+
var defaultPriceCachePath = () => process.env.GLOTFILE_PRICES_PATH || join4(homedir(), ".glotfile", "model-prices.json");
|
|
3505
|
+
function isModelPrice(v) {
|
|
3506
|
+
if (!v || typeof v !== "object") return false;
|
|
3507
|
+
const p = v;
|
|
3508
|
+
return typeof p.inputPerMTok === "number" && typeof p.outputPerMTok === "number";
|
|
3509
|
+
}
|
|
3510
|
+
function loadPriceCache(path = defaultPriceCachePath()) {
|
|
3511
|
+
let parsed;
|
|
3512
|
+
try {
|
|
3513
|
+
parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
3514
|
+
} catch {
|
|
3515
|
+
return null;
|
|
3516
|
+
}
|
|
3517
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
3518
|
+
const raw = parsed;
|
|
3519
|
+
if (!raw.models || typeof raw.models !== "object") return null;
|
|
3520
|
+
const models = {};
|
|
3521
|
+
for (const [id, price] of Object.entries(raw.models)) {
|
|
3522
|
+
if (isModelPrice(price)) models[id] = price;
|
|
3523
|
+
}
|
|
3524
|
+
return {
|
|
3525
|
+
source: typeof raw.source === "string" ? raw.source : "unknown",
|
|
3526
|
+
fetchedAt: typeof raw.fetchedAt === "string" ? raw.fetchedAt : "",
|
|
3527
|
+
models
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
function savePriceCache(cache2, path = defaultPriceCachePath()) {
|
|
3531
|
+
writeFileAtomic(path, JSON.stringify(cache2, null, 2) + "\n");
|
|
3532
|
+
}
|
|
3533
|
+
var memo;
|
|
3534
|
+
function getPriceCache() {
|
|
3535
|
+
if (memo === void 0) memo = loadPriceCache();
|
|
3536
|
+
return memo;
|
|
3537
|
+
}
|
|
3538
|
+
function invalidatePriceCache() {
|
|
3539
|
+
memo = void 0;
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3333
3542
|
// src/server/ai/pricing.ts
|
|
3334
3543
|
function addUsage(into, add) {
|
|
3335
3544
|
into.inputTokens += add.inputTokens;
|
|
@@ -3346,7 +3555,9 @@ function usageCostUsd(usage, ai, multiplier = 1) {
|
|
|
3346
3555
|
return pricing ? estimateUsageCostUsd(usage, pricing, multiplier) : void 0;
|
|
3347
3556
|
}
|
|
3348
3557
|
function estimateUsageCostUsd(usage, pricing, multiplier = 1) {
|
|
3349
|
-
const
|
|
3558
|
+
const writeRate = pricing.cacheWritePerMTok ?? pricing.inputPerMTok * CACHE_WRITE_MULTIPLIER;
|
|
3559
|
+
const readRate = pricing.cacheReadPerMTok ?? pricing.inputPerMTok * CACHE_READ_MULTIPLIER;
|
|
3560
|
+
const inputCost = usage.inputTokens * pricing.inputPerMTok + usage.cacheCreationInputTokens * writeRate + usage.cacheReadInputTokens * readRate;
|
|
3350
3561
|
return (inputCost + usage.outputTokens * pricing.outputPerMTok) / 1e6 * multiplier;
|
|
3351
3562
|
}
|
|
3352
3563
|
var PRICE_TABLE = [
|
|
@@ -3378,12 +3589,27 @@ function bareModelId(model) {
|
|
|
3378
3589
|
if (anth !== -1) id = id.slice(anth + "anthropic.".length);
|
|
3379
3590
|
return id;
|
|
3380
3591
|
}
|
|
3381
|
-
function
|
|
3592
|
+
function lookupCachePrice(cache2, id) {
|
|
3593
|
+
const exact = cache2.models[id];
|
|
3594
|
+
if (exact) return { source: "cache", ...exact };
|
|
3595
|
+
let best;
|
|
3596
|
+
for (const [cid, price] of Object.entries(cache2.models)) {
|
|
3597
|
+
if (id.startsWith(cid) && (!best || cid.length > best.id.length)) {
|
|
3598
|
+
best = { id: cid, price: { source: "cache", ...price } };
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
return best ? best.price : null;
|
|
3602
|
+
}
|
|
3603
|
+
function resolvePricing(ai, cache2 = getPriceCache()) {
|
|
3382
3604
|
if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
|
|
3383
3605
|
return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
|
|
3384
3606
|
}
|
|
3385
3607
|
if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
|
|
3386
3608
|
const id = bareModelId(ai.model);
|
|
3609
|
+
if (cache2) {
|
|
3610
|
+
const cached = lookupCachePrice(cache2, id);
|
|
3611
|
+
if (cached) return cached;
|
|
3612
|
+
}
|
|
3387
3613
|
let best;
|
|
3388
3614
|
for (const row of PRICE_TABLE) {
|
|
3389
3615
|
if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
|
|
@@ -3931,7 +4157,7 @@ function makeProvider(ai) {
|
|
|
3931
4157
|
}
|
|
3932
4158
|
|
|
3933
4159
|
// src/server/ai/run.ts
|
|
3934
|
-
import { readFileSync as
|
|
4160
|
+
import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
|
|
3935
4161
|
import { resolve as resolve5, extname as extname2 } from "path";
|
|
3936
4162
|
|
|
3937
4163
|
// src/server/cell-state.ts
|
|
@@ -3950,7 +4176,7 @@ function cellState(entry, locale, sourceLocale) {
|
|
|
3950
4176
|
// src/server/ai/run.ts
|
|
3951
4177
|
function selectRequests(state, opts) {
|
|
3952
4178
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
3953
|
-
const keyRe = opts.keyGlob ?
|
|
4179
|
+
const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
|
|
3954
4180
|
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
3955
4181
|
const stateSet = opts.states ? new Set(opts.states) : null;
|
|
3956
4182
|
const skip = (st) => stateSet ? !stateSet.has(st) : !!opts.onlyMissing && st !== "missing";
|
|
@@ -4028,7 +4254,7 @@ function attachScreenshots(reqs, state, projectRoot) {
|
|
|
4028
4254
|
if (!existsSync7(abs)) {
|
|
4029
4255
|
cache2.set(screenshot, null);
|
|
4030
4256
|
} else {
|
|
4031
|
-
const buf =
|
|
4257
|
+
const buf = readFileSync8(abs);
|
|
4032
4258
|
cache2.set(screenshot, buf.length > MAX_IMAGE_BYTES ? null : { mediaType, base64: buf.toString("base64") });
|
|
4033
4259
|
}
|
|
4034
4260
|
}
|
|
@@ -4152,17 +4378,61 @@ function applyResults(state, reqs, results, clock = systemClock, force = false)
|
|
|
4152
4378
|
return { written, errors };
|
|
4153
4379
|
}
|
|
4154
4380
|
|
|
4381
|
+
// src/server/ai/explain-error.ts
|
|
4382
|
+
var KEY_ENV = {
|
|
4383
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
4384
|
+
openai: "OPENAI_API_KEY",
|
|
4385
|
+
openrouter: "OPENROUTER_API_KEY"
|
|
4386
|
+
};
|
|
4387
|
+
function rawMessage(err) {
|
|
4388
|
+
if (err instanceof Error && err.message) return err.message;
|
|
4389
|
+
if (typeof err === "string") return err;
|
|
4390
|
+
if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
|
|
4391
|
+
return err.message;
|
|
4392
|
+
}
|
|
4393
|
+
return String(err ?? "Unknown error");
|
|
4394
|
+
}
|
|
4395
|
+
function explainProviderError(provider, err) {
|
|
4396
|
+
const raw = rawMessage(err);
|
|
4397
|
+
const m = raw.toLowerCase();
|
|
4398
|
+
if (provider === "bedrock") {
|
|
4399
|
+
if (/could not load credentials|unable to locate credentials|credentialsprovider|credentials from any providers/.test(m)) {
|
|
4400
|
+
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`.";
|
|
4401
|
+
}
|
|
4402
|
+
if (/on-demand throughput isn.?t supported/.test(m) || /inference profile/.test(m)) {
|
|
4403
|
+
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).';
|
|
4404
|
+
}
|
|
4405
|
+
if (/not authorized to perform/.test(m) || /no identity-based policy/.test(m) || /bedrock:invoke/.test(m)) {
|
|
4406
|
+
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.";
|
|
4407
|
+
}
|
|
4408
|
+
if (/access to the model|don.?t have access to the model/.test(m)) {
|
|
4409
|
+
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.";
|
|
4410
|
+
}
|
|
4411
|
+
if (/access ?denied/.test(m)) {
|
|
4412
|
+
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.";
|
|
4413
|
+
}
|
|
4414
|
+
if (/region/.test(m)) {
|
|
4415
|
+
return "No AWS region set for Bedrock. Set the Region in AI settings, or AWS_REGION in your environment.";
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
const keyEnv = KEY_ENV[provider];
|
|
4419
|
+
if (keyEnv && /api key|unauthorized|\b401\b|authentication|incorrect api key|invalid x-api-key/.test(m)) {
|
|
4420
|
+
return `${provider} rejected the request \u2014 check ${keyEnv}. Set it in your environment or a .env file in the directory you started glotfile from.`;
|
|
4421
|
+
}
|
|
4422
|
+
return raw;
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4155
4425
|
// src/server/ai/pending-batch.ts
|
|
4156
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as
|
|
4157
|
-
import { join as
|
|
4426
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync3, rmSync as rmSync4 } from "fs";
|
|
4427
|
+
import { join as join5 } from "path";
|
|
4158
4428
|
function pendingBatchPath(projectRoot) {
|
|
4159
|
-
return
|
|
4429
|
+
return join5(projectRoot, ".glotfile", "batch.json");
|
|
4160
4430
|
}
|
|
4161
4431
|
function loadPendingBatch(projectRoot) {
|
|
4162
4432
|
const path = pendingBatchPath(projectRoot);
|
|
4163
4433
|
if (!existsSync8(path)) return void 0;
|
|
4164
4434
|
try {
|
|
4165
|
-
const parsed = JSON.parse(
|
|
4435
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
4166
4436
|
if (parsed?.version !== 1) return void 0;
|
|
4167
4437
|
return parsed;
|
|
4168
4438
|
} catch {
|
|
@@ -4170,9 +4440,9 @@ function loadPendingBatch(projectRoot) {
|
|
|
4170
4440
|
}
|
|
4171
4441
|
}
|
|
4172
4442
|
function savePendingBatch(projectRoot, pending) {
|
|
4173
|
-
const dir =
|
|
4443
|
+
const dir = join5(projectRoot, ".glotfile");
|
|
4174
4444
|
mkdirSync4(dir, { recursive: true });
|
|
4175
|
-
const gitignore =
|
|
4445
|
+
const gitignore = join5(dir, ".gitignore");
|
|
4176
4446
|
if (!existsSync8(gitignore)) writeFileSync3(gitignore, "*\n");
|
|
4177
4447
|
writeFileSync3(pendingBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
4178
4448
|
}
|
|
@@ -4181,7 +4451,7 @@ function clearPendingBatch(projectRoot) {
|
|
|
4181
4451
|
}
|
|
4182
4452
|
|
|
4183
4453
|
// src/server/log.ts
|
|
4184
|
-
import { appendFileSync, existsSync as existsSync9, openSync, fstatSync, readSync, closeSync, statSync as statSync2, readFileSync as
|
|
4454
|
+
import { appendFileSync, existsSync as existsSync9, openSync, fstatSync, readSync, closeSync, statSync as statSync2, readFileSync as readFileSync10 } from "fs";
|
|
4185
4455
|
import { resolve as resolve6 } from "path";
|
|
4186
4456
|
function logPath(projectRoot) {
|
|
4187
4457
|
return resolve6(projectRoot, ".glotfile", "log.jsonl");
|
|
@@ -4196,7 +4466,7 @@ function appendLog(projectRoot, entry) {
|
|
|
4196
4466
|
}
|
|
4197
4467
|
function trimLog(path, maxBytes = MAX_LOG_BYTES, targetBytes = TRIM_LOG_TO_BYTES) {
|
|
4198
4468
|
if (!existsSync9(path) || statSync2(path).size <= maxBytes) return;
|
|
4199
|
-
const lines =
|
|
4469
|
+
const lines = readFileSync10(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
4200
4470
|
const kept = [];
|
|
4201
4471
|
let bytes = 0;
|
|
4202
4472
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -4374,16 +4644,16 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
|
|
|
4374
4644
|
}
|
|
4375
4645
|
|
|
4376
4646
|
// src/server/ai/pending-context-batch.ts
|
|
4377
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as
|
|
4378
|
-
import { join as
|
|
4647
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync4, rmSync as rmSync5 } from "fs";
|
|
4648
|
+
import { join as join6 } from "path";
|
|
4379
4649
|
function pendingContextBatchPath(projectRoot) {
|
|
4380
|
-
return
|
|
4650
|
+
return join6(projectRoot, ".glotfile", "context-batch.json");
|
|
4381
4651
|
}
|
|
4382
4652
|
function loadPendingContextBatch(projectRoot) {
|
|
4383
4653
|
const path = pendingContextBatchPath(projectRoot);
|
|
4384
4654
|
if (!existsSync10(path)) return void 0;
|
|
4385
4655
|
try {
|
|
4386
|
-
const parsed = JSON.parse(
|
|
4656
|
+
const parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
4387
4657
|
if (parsed?.version !== 1) return void 0;
|
|
4388
4658
|
return parsed;
|
|
4389
4659
|
} catch {
|
|
@@ -4391,9 +4661,9 @@ function loadPendingContextBatch(projectRoot) {
|
|
|
4391
4661
|
}
|
|
4392
4662
|
}
|
|
4393
4663
|
function savePendingContextBatch(projectRoot, pending) {
|
|
4394
|
-
const dir =
|
|
4664
|
+
const dir = join6(projectRoot, ".glotfile");
|
|
4395
4665
|
mkdirSync5(dir, { recursive: true });
|
|
4396
|
-
const gitignore =
|
|
4666
|
+
const gitignore = join6(dir, ".gitignore");
|
|
4397
4667
|
if (!existsSync10(gitignore)) writeFileSync4(gitignore, "*\n");
|
|
4398
4668
|
writeFileSync4(pendingContextBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
4399
4669
|
}
|
|
@@ -4500,106 +4770,319 @@ async function applyContextBatchResults(load, persist, provider, pending, projec
|
|
|
4500
4770
|
return { written, errors, retried: retryChunks.length };
|
|
4501
4771
|
}
|
|
4502
4772
|
|
|
4503
|
-
// src/server/ai/
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
return
|
|
4508
|
-
}
|
|
4509
|
-
var EXPANSION = 1.2;
|
|
4510
|
-
var ITEM_REPLY_OVERHEAD = 16;
|
|
4511
|
-
var FORM_REPLY_OVERHEAD = 8;
|
|
4512
|
-
function estimateOutputTokens(req) {
|
|
4513
|
-
const translated = Math.ceil(estimateTokens(req.source) * EXPANSION);
|
|
4514
|
-
if (req.plural) {
|
|
4515
|
-
return ITEM_REPLY_OVERHEAD + req.plural.categories.length * (translated + FORM_REPLY_OVERHEAD);
|
|
4516
|
-
}
|
|
4517
|
-
return ITEM_REPLY_OVERHEAD + translated;
|
|
4518
|
-
}
|
|
4519
|
-
function estimateTranslation(state, ai, opts) {
|
|
4520
|
-
const reqs = selectRequests(state, opts);
|
|
4521
|
-
const byLocale = /* @__PURE__ */ new Map();
|
|
4522
|
-
for (const r of reqs) {
|
|
4523
|
-
let group = byLocale.get(r.targetLocale);
|
|
4524
|
-
if (!group) {
|
|
4525
|
-
group = [];
|
|
4526
|
-
byLocale.set(r.targetLocale, group);
|
|
4527
|
-
}
|
|
4528
|
-
group.push(r);
|
|
4529
|
-
}
|
|
4530
|
-
const perLocale = [];
|
|
4531
|
-
for (const [locale, group] of byLocale) {
|
|
4532
|
-
let inputTokens2 = 0;
|
|
4533
|
-
let outputTokens2 = 0;
|
|
4534
|
-
const batches = chunk(group, Math.max(1, ai.batchSize));
|
|
4535
|
-
for (const batch of batches) {
|
|
4536
|
-
const system = buildSystemPrompt(batch.some((r) => r.plural !== void 0));
|
|
4537
|
-
inputTokens2 += estimateTokens(system) + estimateTokens(buildBatchPrompt(batch));
|
|
4538
|
-
for (const r of batch) outputTokens2 += estimateOutputTokens(r);
|
|
4539
|
-
}
|
|
4540
|
-
perLocale.push({ locale, requests: group.length, batches: batches.length, inputTokens: inputTokens2, outputTokens: outputTokens2 });
|
|
4541
|
-
}
|
|
4542
|
-
const inputTokens = perLocale.reduce((n, l) => n + l.inputTokens, 0);
|
|
4543
|
-
const outputTokens = perLocale.reduce((n, l) => n + l.outputTokens, 0);
|
|
4544
|
-
const pricing = resolvePricing(ai);
|
|
4545
|
-
return {
|
|
4546
|
-
requests: reqs.length,
|
|
4547
|
-
batches: perLocale.reduce((n, l) => n + l.batches, 0),
|
|
4548
|
-
perLocale,
|
|
4549
|
-
inputTokens,
|
|
4550
|
-
outputTokens,
|
|
4551
|
-
pricing,
|
|
4552
|
-
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4553
|
-
};
|
|
4773
|
+
// src/server/ai/pending-glossary-batch.ts
|
|
4774
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync5, rmSync as rmSync6 } from "fs";
|
|
4775
|
+
import { join as join7 } from "path";
|
|
4776
|
+
function pendingGlossaryBatchPath(projectRoot) {
|
|
4777
|
+
return join7(projectRoot, ".glotfile", "glossary-suggest-batch.json");
|
|
4554
4778
|
}
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
// src/server/import/detect.ts
|
|
4560
|
-
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync3 } from "fs";
|
|
4561
|
-
import { join as join6 } from "path";
|
|
4562
|
-
var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4563
|
-
var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
4564
|
-
function safeIsDir(p) {
|
|
4779
|
+
function loadPendingGlossaryBatch(projectRoot) {
|
|
4780
|
+
const path = pendingGlossaryBatchPath(projectRoot);
|
|
4781
|
+
if (!existsSync11(path)) return void 0;
|
|
4565
4782
|
try {
|
|
4566
|
-
|
|
4783
|
+
const parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
4784
|
+
if (parsed?.version !== 1) return void 0;
|
|
4785
|
+
return parsed;
|
|
4567
4786
|
} catch {
|
|
4568
|
-
return
|
|
4787
|
+
return void 0;
|
|
4569
4788
|
}
|
|
4570
4789
|
}
|
|
4571
|
-
function
|
|
4572
|
-
|
|
4790
|
+
function savePendingGlossaryBatch(projectRoot, pending) {
|
|
4791
|
+
const dir = join7(projectRoot, ".glotfile");
|
|
4792
|
+
mkdirSync6(dir, { recursive: true });
|
|
4793
|
+
const gitignore = join7(dir, ".gitignore");
|
|
4794
|
+
if (!existsSync11(gitignore)) writeFileSync5(gitignore, "*\n");
|
|
4795
|
+
writeFileSync5(pendingGlossaryBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
4573
4796
|
}
|
|
4574
|
-
function
|
|
4575
|
-
|
|
4576
|
-
return readdirSync3(dir).length;
|
|
4577
|
-
} catch {
|
|
4578
|
-
return 0;
|
|
4579
|
-
}
|
|
4797
|
+
function clearPendingGlossaryBatch(projectRoot) {
|
|
4798
|
+
rmSync6(pendingGlossaryBatchPath(projectRoot), { force: true });
|
|
4580
4799
|
}
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4800
|
+
|
|
4801
|
+
// src/server/ai/glossary-batch-run.ts
|
|
4802
|
+
function completionRequestFor2(chunk2, knownTerms) {
|
|
4803
|
+
return {
|
|
4804
|
+
system: buildGlossarySuggestSystemPrompt(),
|
|
4805
|
+
content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunk2, knownTerms) }],
|
|
4806
|
+
schema: GLOSSARY_SUGGEST_SCHEMA
|
|
4807
|
+
};
|
|
4808
|
+
}
|
|
4809
|
+
async function submitGlossarySuggestBatch(provider, sources, knownTerms, batchSize, model, projectRoot) {
|
|
4810
|
+
if (loadPendingGlossaryBatch(projectRoot)) {
|
|
4811
|
+
throw new Error("A glossary suggestion batch is already pending. Apply or cancel it first.");
|
|
4812
|
+
}
|
|
4813
|
+
const chunks = [];
|
|
4814
|
+
const size = Math.max(1, batchSize);
|
|
4815
|
+
for (let i = 0; i < sources.length; i += size) chunks.push(sources.slice(i, i + size));
|
|
4816
|
+
const jobs = chunks.map((chunk2, i) => ({ customId: `gloss_${i}`, chunk: chunk2 }));
|
|
4817
|
+
const batchId = await provider.submitCompletionBatch(
|
|
4818
|
+
jobs.map((j) => ({ customId: j.customId, request: completionRequestFor2(j.chunk, knownTerms) }))
|
|
4819
|
+
);
|
|
4820
|
+
const pending = {
|
|
4821
|
+
version: 1,
|
|
4822
|
+
// Only Anthropic implements completion batches today.
|
|
4823
|
+
provider: "anthropic",
|
|
4824
|
+
model,
|
|
4825
|
+
batchId,
|
|
4826
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4827
|
+
total: sources.length,
|
|
4828
|
+
knownTerms,
|
|
4829
|
+
jobs: jobs.map((j) => ({
|
|
4830
|
+
customId: j.customId,
|
|
4831
|
+
requests: j.chunk
|
|
4832
|
+
}))
|
|
4833
|
+
};
|
|
4834
|
+
savePendingGlossaryBatch(projectRoot, pending);
|
|
4835
|
+
return pending;
|
|
4836
|
+
}
|
|
4837
|
+
async function applyGlossarySuggestBatchResults(load, persist, provider, pending, projectRoot, ai) {
|
|
4838
|
+
provider.takeUsage?.();
|
|
4839
|
+
const outcomes = await provider.completionBatchResults(pending.batchId);
|
|
4840
|
+
const batchUsage = provider.takeUsage?.();
|
|
4841
|
+
const allTerms = [];
|
|
4842
|
+
const errors = [];
|
|
4843
|
+
const jobFailures = [];
|
|
4844
|
+
const retryChunks = [];
|
|
4845
|
+
for (const job of pending.jobs) {
|
|
4846
|
+
const outcome = outcomes.get(job.customId);
|
|
4847
|
+
if (outcome?.type === "json") {
|
|
4848
|
+
const batch = outcome.value;
|
|
4849
|
+
allTerms.push(...batch.terms ?? []);
|
|
4850
|
+
continue;
|
|
4851
|
+
}
|
|
4852
|
+
if (!outcome) jobFailures.push({ customId: job.customId, locale: "", type: "missing" });
|
|
4853
|
+
else if (outcome.type === "malformed") jobFailures.push({ customId: job.customId, locale: "", type: "malformed", raw: outcome.raw });
|
|
4854
|
+
else jobFailures.push({ customId: job.customId, locale: "", type: "failed", error: outcome.error });
|
|
4855
|
+
retryChunks.push(job.requests);
|
|
4856
|
+
}
|
|
4857
|
+
for (const chunk2 of retryChunks) {
|
|
4858
|
+
try {
|
|
4859
|
+
const raw = await provider.complete(completionRequestFor2(chunk2, pending.knownTerms));
|
|
4860
|
+
const batch = raw;
|
|
4861
|
+
allTerms.push(...batch.terms ?? []);
|
|
4862
|
+
} catch (e) {
|
|
4863
|
+
errors.push({ error: e.message });
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
const retryUsage = provider.takeUsage?.();
|
|
4867
|
+
const pricing = resolvePricing({ ...ai, model: pending.model });
|
|
4868
|
+
let estimatedCostUsd;
|
|
4869
|
+
if (pricing && (batchUsage || retryUsage)) {
|
|
4870
|
+
estimatedCostUsd = (batchUsage ? estimateUsageCostUsd(batchUsage, pricing, BATCH_PRICE_MULTIPLIER) : 0) + (retryUsage ? estimateUsageCostUsd(retryUsage, pricing) : 0);
|
|
4871
|
+
}
|
|
4872
|
+
let usage;
|
|
4873
|
+
if (batchUsage || retryUsage) {
|
|
4874
|
+
usage = batchUsage ?? { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
|
|
4875
|
+
if (retryUsage) addUsage(usage, retryUsage);
|
|
4876
|
+
}
|
|
4877
|
+
const fresh = load();
|
|
4878
|
+
const added = mergeGlossarySuggestions(fresh, dedupeTerms(allTerms));
|
|
4879
|
+
persist(fresh);
|
|
4880
|
+
clearPendingGlossaryBatch(projectRoot);
|
|
4881
|
+
const costSuffix = estimatedCostUsd !== void 0 ? ` (~$${estimatedCostUsd.toFixed(2)})` : "";
|
|
4882
|
+
appendLog(projectRoot, {
|
|
4883
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4884
|
+
kind: "glossary",
|
|
4885
|
+
summary: `Applied glossary suggestion batch ${pending.batchId}: ${added.length} new term(s), ${errors.length} error(s), ${retryChunks.length} job(s) retried${costSuffix}`,
|
|
4886
|
+
model: pending.model,
|
|
4887
|
+
jobFailures: jobFailures.length ? jobFailures : void 0,
|
|
4888
|
+
usage,
|
|
4889
|
+
estimatedCostUsd
|
|
4890
|
+
});
|
|
4891
|
+
return { added: added.length, errors, retried: retryChunks.length };
|
|
4892
|
+
}
|
|
4893
|
+
|
|
4894
|
+
// src/server/ai/estimate.ts
|
|
4895
|
+
var CJK_RE = /[ -鿿가-豈-]/g;
|
|
4896
|
+
function estimateTokens(text) {
|
|
4897
|
+
const cjk = text.match(CJK_RE)?.length ?? 0;
|
|
4898
|
+
return Math.ceil((text.length - cjk) / 4 + cjk / 2);
|
|
4899
|
+
}
|
|
4900
|
+
var EXPANSION = 1.2;
|
|
4901
|
+
var ITEM_REPLY_OVERHEAD = 16;
|
|
4902
|
+
var FORM_REPLY_OVERHEAD = 8;
|
|
4903
|
+
function estimateOutputTokens(req) {
|
|
4904
|
+
const translated = Math.ceil(estimateTokens(req.source) * EXPANSION);
|
|
4905
|
+
if (req.plural) {
|
|
4906
|
+
return ITEM_REPLY_OVERHEAD + req.plural.categories.length * (translated + FORM_REPLY_OVERHEAD);
|
|
4907
|
+
}
|
|
4908
|
+
return ITEM_REPLY_OVERHEAD + translated;
|
|
4909
|
+
}
|
|
4910
|
+
function estimateTranslation(state, ai, opts) {
|
|
4911
|
+
const reqs = selectRequests(state, opts);
|
|
4912
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
4913
|
+
for (const r of reqs) {
|
|
4914
|
+
let group = byLocale.get(r.targetLocale);
|
|
4915
|
+
if (!group) {
|
|
4916
|
+
group = [];
|
|
4917
|
+
byLocale.set(r.targetLocale, group);
|
|
4918
|
+
}
|
|
4919
|
+
group.push(r);
|
|
4920
|
+
}
|
|
4921
|
+
const perLocale = [];
|
|
4922
|
+
for (const [locale, group] of byLocale) {
|
|
4923
|
+
let inputTokens2 = 0;
|
|
4924
|
+
let outputTokens2 = 0;
|
|
4925
|
+
const batches = chunk(group, Math.max(1, ai.batchSize));
|
|
4926
|
+
for (const batch of batches) {
|
|
4927
|
+
const system = buildSystemPrompt(batch.some((r) => r.plural !== void 0));
|
|
4928
|
+
inputTokens2 += estimateTokens(system) + estimateTokens(buildBatchPrompt(batch));
|
|
4929
|
+
for (const r of batch) outputTokens2 += estimateOutputTokens(r);
|
|
4930
|
+
}
|
|
4931
|
+
perLocale.push({ locale, requests: group.length, batches: batches.length, inputTokens: inputTokens2, outputTokens: outputTokens2 });
|
|
4932
|
+
}
|
|
4933
|
+
const inputTokens = perLocale.reduce((n, l) => n + l.inputTokens, 0);
|
|
4934
|
+
const outputTokens = perLocale.reduce((n, l) => n + l.outputTokens, 0);
|
|
4935
|
+
const pricing = resolvePricing(ai);
|
|
4936
|
+
return {
|
|
4937
|
+
requests: reqs.length,
|
|
4938
|
+
batches: perLocale.reduce((n, l) => n + l.batches, 0),
|
|
4939
|
+
perLocale,
|
|
4940
|
+
inputTokens,
|
|
4941
|
+
outputTokens,
|
|
4942
|
+
pricing,
|
|
4943
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4944
|
+
};
|
|
4945
|
+
}
|
|
4946
|
+
var CONTEXT_REPLY_OVERHEAD = 16;
|
|
4947
|
+
var TYPICAL_CONTEXT_TOKENS = 35;
|
|
4948
|
+
function estimateContext(targets, ai) {
|
|
4949
|
+
const batchSize = Math.max(1, ai.contextBatchSize ?? ai.batchSize ?? 10);
|
|
4950
|
+
const batches = chunk(targets, batchSize);
|
|
4951
|
+
const system = buildContextSystemPrompt();
|
|
4952
|
+
let inputTokens = 0;
|
|
4953
|
+
let outputTokens = 0;
|
|
4954
|
+
for (const batch of batches) {
|
|
4955
|
+
inputTokens += estimateTokens(system) + estimateTokens(buildContextBatchPrompt(batch));
|
|
4956
|
+
outputTokens += batch.length * (CONTEXT_REPLY_OVERHEAD + TYPICAL_CONTEXT_TOKENS);
|
|
4957
|
+
}
|
|
4958
|
+
const pricing = resolvePricing(ai);
|
|
4959
|
+
return {
|
|
4960
|
+
keys: targets.length,
|
|
4961
|
+
batches: batches.length,
|
|
4962
|
+
inputTokens,
|
|
4963
|
+
outputTokens,
|
|
4964
|
+
pricing,
|
|
4965
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4966
|
+
};
|
|
4967
|
+
}
|
|
4968
|
+
var TERM_REPLY_TOKENS = 24;
|
|
4969
|
+
var TERM_YIELD = 0.15;
|
|
4970
|
+
function estimateGlossarySuggest(sources, knownTerms, ai) {
|
|
4971
|
+
const batchSize = Math.max(1, ai.contextBatchSize ?? ai.batchSize ?? 10);
|
|
4972
|
+
const batches = chunk(sources, batchSize);
|
|
4973
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
4974
|
+
let inputTokens = 0;
|
|
4975
|
+
let outputTokens = 0;
|
|
4976
|
+
for (const batch of batches) {
|
|
4977
|
+
inputTokens += estimateTokens(system) + estimateTokens(buildGlossarySuggestBatchPrompt(batch, knownTerms));
|
|
4978
|
+
outputTokens += Math.ceil(batch.length * TERM_YIELD) * TERM_REPLY_TOKENS;
|
|
4979
|
+
}
|
|
4980
|
+
const pricing = resolvePricing(ai);
|
|
4981
|
+
return {
|
|
4982
|
+
sources: sources.length,
|
|
4983
|
+
batches: batches.length,
|
|
4984
|
+
inputTokens,
|
|
4985
|
+
outputTokens,
|
|
4986
|
+
pricing,
|
|
4987
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4988
|
+
};
|
|
4989
|
+
}
|
|
4990
|
+
|
|
4991
|
+
// src/server/ai/price-fetch.ts
|
|
4992
|
+
var MODELS_DEV_URL = "https://models.dev/api.json";
|
|
4993
|
+
var priceUrl = () => process.env.GLOTFILE_PRICES_URL || MODELS_DEV_URL;
|
|
4994
|
+
var PROVIDER_PREFERENCE = ["anthropic", "openai", "google", "meta-llama", "mistral", "deepseek", "xai", "groq"];
|
|
4995
|
+
var providerRank = (provId) => {
|
|
4996
|
+
const i = PROVIDER_PREFERENCE.indexOf(provId);
|
|
4997
|
+
return i === -1 ? PROVIDER_PREFERENCE.length : i;
|
|
4998
|
+
};
|
|
4999
|
+
function normalizeModelsDevPrices(api) {
|
|
5000
|
+
const out = {};
|
|
5001
|
+
const ranks = {};
|
|
5002
|
+
if (!api || typeof api !== "object") return out;
|
|
5003
|
+
for (const [provId, prov] of Object.entries(api)) {
|
|
5004
|
+
const models = prov?.models;
|
|
5005
|
+
if (!models || typeof models !== "object") continue;
|
|
5006
|
+
const rank = providerRank(provId);
|
|
5007
|
+
for (const [modelKey, model] of Object.entries(models)) {
|
|
5008
|
+
const cost = model?.cost;
|
|
5009
|
+
if (!cost || typeof cost.input !== "number" || typeof cost.output !== "number") continue;
|
|
5010
|
+
const bareId = bareModelId(modelKey);
|
|
5011
|
+
if (!bareId) continue;
|
|
5012
|
+
const existingRank = ranks[bareId];
|
|
5013
|
+
if (existingRank !== void 0 && existingRank <= rank) continue;
|
|
5014
|
+
const price = { inputPerMTok: cost.input, outputPerMTok: cost.output };
|
|
5015
|
+
if (typeof cost.cache_read === "number") price.cacheReadPerMTok = cost.cache_read;
|
|
5016
|
+
if (typeof cost.cache_write === "number") price.cacheWritePerMTok = cost.cache_write;
|
|
5017
|
+
out[bareId] = price;
|
|
5018
|
+
ranks[bareId] = rank;
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
return out;
|
|
5022
|
+
}
|
|
5023
|
+
var defaultNow = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
5024
|
+
async function refreshPrices(opts = {}) {
|
|
5025
|
+
const url = opts.url ?? priceUrl();
|
|
5026
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
5027
|
+
const res = await doFetch(url);
|
|
5028
|
+
if (!res.ok) throw new Error(`Failed to fetch prices from ${url}: HTTP ${res.status}`);
|
|
5029
|
+
const api = await res.json();
|
|
5030
|
+
const models = normalizeModelsDevPrices(api);
|
|
5031
|
+
const modelCount = Object.keys(models).length;
|
|
5032
|
+
if (modelCount === 0) throw new Error(`No model prices found in response from ${url}`);
|
|
5033
|
+
const cache2 = { source: "models.dev", fetchedAt: (opts.now ?? defaultNow)(), models };
|
|
5034
|
+
const path = opts.path ?? defaultPriceCachePath();
|
|
5035
|
+
savePriceCache(cache2, path);
|
|
5036
|
+
return { source: cache2.source, fetchedAt: cache2.fetchedAt, modelCount, path };
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
// src/server/import/run.ts
|
|
5040
|
+
import { relative as relative3 } from "path";
|
|
5041
|
+
|
|
5042
|
+
// src/server/import/detect.ts
|
|
5043
|
+
import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
|
|
5044
|
+
import { join as join8 } from "path";
|
|
5045
|
+
var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5046
|
+
var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
5047
|
+
function safeIsDir(p) {
|
|
5048
|
+
try {
|
|
5049
|
+
return statSync3(p).isDirectory();
|
|
5050
|
+
} catch {
|
|
5051
|
+
return false;
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
function listDirs(dir) {
|
|
5055
|
+
return readdirSync3(dir).filter((e) => safeIsDir(join8(dir, e)));
|
|
5056
|
+
}
|
|
5057
|
+
function fileCount(dir) {
|
|
5058
|
+
try {
|
|
5059
|
+
return readdirSync3(dir).length;
|
|
5060
|
+
} catch {
|
|
5061
|
+
return 0;
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
function pickSource(locales, sizeOf) {
|
|
5065
|
+
if (locales.includes("en")) return "en";
|
|
5066
|
+
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4584
5067
|
}
|
|
4585
5068
|
function detectLaravel(root) {
|
|
4586
|
-
const localeRoot = [
|
|
5069
|
+
const localeRoot = [join8(root, "resources", "lang"), join8(root, "lang")].find(safeIsDir);
|
|
4587
5070
|
if (!localeRoot) return null;
|
|
4588
5071
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4589
5072
|
if (locales.length === 0) return null;
|
|
4590
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
5073
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join8(localeRoot, loc)));
|
|
4591
5074
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4592
5075
|
}
|
|
4593
5076
|
function detectVue(root, forced = false) {
|
|
4594
5077
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4595
|
-
const localeRoot =
|
|
5078
|
+
const localeRoot = join8(root, rel);
|
|
4596
5079
|
if (!safeIsDir(localeRoot)) continue;
|
|
4597
5080
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4598
5081
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4599
5082
|
if (enough) {
|
|
4600
5083
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4601
5084
|
try {
|
|
4602
|
-
return statSync3(
|
|
5085
|
+
return statSync3(join8(localeRoot, `${loc}.json`)).size;
|
|
4603
5086
|
} catch {
|
|
4604
5087
|
return 0;
|
|
4605
5088
|
}
|
|
@@ -4613,9 +5096,9 @@ var NEXT_INTL_CONFIG_CANDIDATES = ["src/i18n/request.ts", "i18n/request.ts", "sr
|
|
|
4613
5096
|
var NEXT_INTL_ROUTING_CANDIDATES = ["src/i18n/routing.ts", "i18n/routing.ts", "src/i18n/routing.js", "i18n/routing.js"];
|
|
4614
5097
|
var NEXT_INTL_DIR_CANDIDATES = ["messages", "src/messages", "locales", "src/locales", "src/i18n/messages"];
|
|
4615
5098
|
function hasNextIntlSignal(root) {
|
|
4616
|
-
if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) =>
|
|
5099
|
+
if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync12(join8(root, rel)))) return true;
|
|
4617
5100
|
try {
|
|
4618
|
-
const pkg = JSON.parse(
|
|
5101
|
+
const pkg = JSON.parse(readFileSync13(join8(root, "package.json"), "utf8"));
|
|
4619
5102
|
if (pkg.dependencies?.["next-intl"] || pkg.devDependencies?.["next-intl"]) return true;
|
|
4620
5103
|
} catch {
|
|
4621
5104
|
}
|
|
@@ -4624,7 +5107,7 @@ function hasNextIntlSignal(root) {
|
|
|
4624
5107
|
function nextIntlDefaultLocale(root) {
|
|
4625
5108
|
for (const rel of NEXT_INTL_ROUTING_CANDIDATES) {
|
|
4626
5109
|
try {
|
|
4627
|
-
const m =
|
|
5110
|
+
const m = readFileSync13(join8(root, rel), "utf8").match(/defaultLocale\s*:\s*['"]([^'"]+)['"]/);
|
|
4628
5111
|
if (m) return m[1];
|
|
4629
5112
|
} catch {
|
|
4630
5113
|
}
|
|
@@ -4634,14 +5117,14 @@ function nextIntlDefaultLocale(root) {
|
|
|
4634
5117
|
function detectNextIntl(root, forced = false) {
|
|
4635
5118
|
if (!forced && !hasNextIntlSignal(root)) return null;
|
|
4636
5119
|
for (const rel of NEXT_INTL_DIR_CANDIDATES) {
|
|
4637
|
-
const localeRoot =
|
|
5120
|
+
const localeRoot = join8(root, rel);
|
|
4638
5121
|
if (!safeIsDir(localeRoot)) continue;
|
|
4639
5122
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4640
5123
|
if (locales.length === 0) continue;
|
|
4641
5124
|
const def = nextIntlDefaultLocale(root);
|
|
4642
5125
|
const sourceLocale = def && locales.includes(def) ? def : pickSource(locales, (loc) => {
|
|
4643
5126
|
try {
|
|
4644
|
-
return statSync3(
|
|
5127
|
+
return statSync3(join8(localeRoot, `${loc}.json`)).size;
|
|
4645
5128
|
} catch {
|
|
4646
5129
|
return 0;
|
|
4647
5130
|
}
|
|
@@ -4652,7 +5135,7 @@ function detectNextIntl(root, forced = false) {
|
|
|
4652
5135
|
}
|
|
4653
5136
|
function detectArb(root) {
|
|
4654
5137
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4655
|
-
const localeRoot =
|
|
5138
|
+
const localeRoot = join8(root, rel);
|
|
4656
5139
|
if (!safeIsDir(localeRoot)) continue;
|
|
4657
5140
|
const locales = readdirSync3(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4658
5141
|
if (locales.length >= 1) {
|
|
@@ -4662,10 +5145,10 @@ function detectArb(root) {
|
|
|
4662
5145
|
return null;
|
|
4663
5146
|
}
|
|
4664
5147
|
function lprojLocales(dir) {
|
|
4665
|
-
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
5148
|
+
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync12(join8(dir, `${l}.lproj`, "Localizable.strings")));
|
|
4666
5149
|
}
|
|
4667
5150
|
function detectApple(root) {
|
|
4668
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5151
|
+
const candidates = [root, ...listDirs(root).map((d) => join8(root, d))];
|
|
4669
5152
|
let best = null;
|
|
4670
5153
|
for (const dir of candidates) {
|
|
4671
5154
|
const locales = lprojLocales(dir);
|
|
@@ -4677,7 +5160,7 @@ function detectApple(root) {
|
|
|
4677
5160
|
locales,
|
|
4678
5161
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4679
5162
|
try {
|
|
4680
|
-
return statSync3(
|
|
5163
|
+
return statSync3(join8(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4681
5164
|
} catch {
|
|
4682
5165
|
return 0;
|
|
4683
5166
|
}
|
|
@@ -4690,7 +5173,7 @@ function detectApple(root) {
|
|
|
4690
5173
|
var ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
|
|
4691
5174
|
function detectAngularXliff(root) {
|
|
4692
5175
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4693
|
-
const localeRoot = rel === "." ? root :
|
|
5176
|
+
const localeRoot = rel === "." ? root : join8(root, rel);
|
|
4694
5177
|
if (!safeIsDir(localeRoot)) continue;
|
|
4695
5178
|
const files = readdirSync3(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4696
5179
|
if (files.length === 0) continue;
|
|
@@ -4698,7 +5181,7 @@ function detectAngularXliff(root) {
|
|
|
4698
5181
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4699
5182
|
let sourceLocale;
|
|
4700
5183
|
try {
|
|
4701
|
-
sourceLocale =
|
|
5184
|
+
sourceLocale = readFileSync13(join8(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4702
5185
|
} catch {
|
|
4703
5186
|
}
|
|
4704
5187
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -4709,14 +5192,14 @@ function detectAngularXliff(root) {
|
|
|
4709
5192
|
return null;
|
|
4710
5193
|
}
|
|
4711
5194
|
function detectRails(root) {
|
|
4712
|
-
const localeRoot =
|
|
5195
|
+
const localeRoot = join8(root, "config", "locales");
|
|
4713
5196
|
if (!safeIsDir(localeRoot)) return null;
|
|
4714
5197
|
const locales = [];
|
|
4715
5198
|
for (const file of readdirSync3(localeRoot).sort()) {
|
|
4716
5199
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
4717
5200
|
let text;
|
|
4718
5201
|
try {
|
|
4719
|
-
text =
|
|
5202
|
+
text = readFileSync13(join8(localeRoot, file), "utf8");
|
|
4720
5203
|
} catch {
|
|
4721
5204
|
continue;
|
|
4722
5205
|
}
|
|
@@ -4731,15 +5214,15 @@ function detectRails(root) {
|
|
|
4731
5214
|
var I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
|
|
4732
5215
|
function detectI18next(root) {
|
|
4733
5216
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4734
|
-
const localeRoot =
|
|
5217
|
+
const localeRoot = join8(root, rel);
|
|
4735
5218
|
if (!safeIsDir(localeRoot)) continue;
|
|
4736
5219
|
const locales = listDirs(localeRoot).filter(
|
|
4737
|
-
(d) => LOCALE_RE.test(d) && readdirSync3(
|
|
5220
|
+
(d) => LOCALE_RE.test(d) && readdirSync3(join8(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4738
5221
|
);
|
|
4739
5222
|
if (locales.length === 0) continue;
|
|
4740
5223
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4741
5224
|
try {
|
|
4742
|
-
return readdirSync3(
|
|
5225
|
+
return readdirSync3(join8(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join8(localeRoot, loc, f)).size, 0);
|
|
4743
5226
|
} catch {
|
|
4744
5227
|
return 0;
|
|
4745
5228
|
}
|
|
@@ -4756,8 +5239,8 @@ function gettextLocales(dir) {
|
|
|
4756
5239
|
if (!locales.includes(flat)) locales.push(flat);
|
|
4757
5240
|
continue;
|
|
4758
5241
|
}
|
|
4759
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
4760
|
-
const sub =
|
|
5242
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join8(dir, entry))) continue;
|
|
5243
|
+
const sub = join8(dir, entry);
|
|
4761
5244
|
const hasPo = (d) => {
|
|
4762
5245
|
try {
|
|
4763
5246
|
return readdirSync3(d).some((f) => f.endsWith(".po"));
|
|
@@ -4765,7 +5248,7 @@ function gettextLocales(dir) {
|
|
|
4765
5248
|
return false;
|
|
4766
5249
|
}
|
|
4767
5250
|
};
|
|
4768
|
-
if (hasPo(
|
|
5251
|
+
if (hasPo(join8(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4769
5252
|
if (!locales.includes(entry)) locales.push(entry);
|
|
4770
5253
|
}
|
|
4771
5254
|
}
|
|
@@ -4774,7 +5257,7 @@ function gettextLocales(dir) {
|
|
|
4774
5257
|
var GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
|
|
4775
5258
|
function detectGettext(root) {
|
|
4776
5259
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4777
|
-
const localeRoot =
|
|
5260
|
+
const localeRoot = join8(root, rel);
|
|
4778
5261
|
if (!safeIsDir(localeRoot)) continue;
|
|
4779
5262
|
const locales = gettextLocales(localeRoot);
|
|
4780
5263
|
if (locales.length === 0) continue;
|
|
@@ -4783,10 +5266,10 @@ function detectGettext(root) {
|
|
|
4783
5266
|
return null;
|
|
4784
5267
|
}
|
|
4785
5268
|
function detectAppleStringsdict(root) {
|
|
4786
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5269
|
+
const candidates = [root, ...listDirs(root).map((d) => join8(root, d))];
|
|
4787
5270
|
let best = null;
|
|
4788
5271
|
for (const dir of candidates) {
|
|
4789
|
-
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
5272
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync12(join8(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
4790
5273
|
if (locales.length === 0) continue;
|
|
4791
5274
|
if (!best || locales.length > best.locales.length) {
|
|
4792
5275
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4819,7 +5302,7 @@ var BY_FORMAT = {
|
|
|
4819
5302
|
"apple-stringsdict": detectAppleStringsdict
|
|
4820
5303
|
};
|
|
4821
5304
|
function detect(root, formatOverride) {
|
|
4822
|
-
if (!
|
|
5305
|
+
if (!existsSync12(root)) return null;
|
|
4823
5306
|
if (formatOverride) {
|
|
4824
5307
|
const fn = BY_FORMAT[formatOverride];
|
|
4825
5308
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -4833,8 +5316,8 @@ function detect(root, formatOverride) {
|
|
|
4833
5316
|
}
|
|
4834
5317
|
|
|
4835
5318
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4836
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
4837
|
-
import { join as
|
|
5319
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync14 } from "fs";
|
|
5320
|
+
import { join as join9 } from "path";
|
|
4838
5321
|
|
|
4839
5322
|
// src/server/import/flatten.ts
|
|
4840
5323
|
function flattenObject(value, prefix, warnings) {
|
|
@@ -4875,7 +5358,7 @@ var vueI18nJson2 = {
|
|
|
4875
5358
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4876
5359
|
let data;
|
|
4877
5360
|
try {
|
|
4878
|
-
data = JSON.parse(
|
|
5361
|
+
data = JSON.parse(readFileSync14(join9(localeRoot, file), "utf8"));
|
|
4879
5362
|
} catch (e) {
|
|
4880
5363
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4881
5364
|
continue;
|
|
@@ -4890,8 +5373,8 @@ var vueI18nJson2 = {
|
|
|
4890
5373
|
};
|
|
4891
5374
|
|
|
4892
5375
|
// src/server/import/parsers/next-intl-json.ts
|
|
4893
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
4894
|
-
import { join as
|
|
5376
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync15 } from "fs";
|
|
5377
|
+
import { join as join10 } from "path";
|
|
4895
5378
|
var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4896
5379
|
var nextIntlJson2 = {
|
|
4897
5380
|
name: "next-intl-json",
|
|
@@ -4906,7 +5389,7 @@ var nextIntlJson2 = {
|
|
|
4906
5389
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4907
5390
|
let data;
|
|
4908
5391
|
try {
|
|
4909
|
-
data = JSON.parse(
|
|
5392
|
+
data = JSON.parse(readFileSync15(join10(localeRoot, file), "utf8"));
|
|
4910
5393
|
} catch (e) {
|
|
4911
5394
|
warnings.push(`next-intl-json: failed to parse ${file}: ${e.message}`);
|
|
4912
5395
|
continue;
|
|
@@ -4922,7 +5405,7 @@ var nextIntlJson2 = {
|
|
|
4922
5405
|
|
|
4923
5406
|
// src/server/import/parsers/laravel-php.ts
|
|
4924
5407
|
import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
4925
|
-
import { join as
|
|
5408
|
+
import { join as join11, relative as relative2 } from "path";
|
|
4926
5409
|
import { execFileSync } from "child_process";
|
|
4927
5410
|
|
|
4928
5411
|
// src/server/import/placeholders.ts
|
|
@@ -4938,13 +5421,13 @@ function railsToCanonical(value) {
|
|
|
4938
5421
|
|
|
4939
5422
|
// src/server/import/parsers/laravel-php.ts
|
|
4940
5423
|
function listDirs2(dir) {
|
|
4941
|
-
return readdirSync6(dir).filter((e) => statSync4(
|
|
5424
|
+
return readdirSync6(dir).filter((e) => statSync4(join11(dir, e)).isDirectory());
|
|
4942
5425
|
}
|
|
4943
5426
|
function listPhpFiles(dir) {
|
|
4944
5427
|
const out = [];
|
|
4945
5428
|
const walk = (d) => {
|
|
4946
5429
|
for (const e of readdirSync6(d)) {
|
|
4947
|
-
const full =
|
|
5430
|
+
const full = join11(d, e);
|
|
4948
5431
|
if (statSync4(full).isDirectory()) walk(full);
|
|
4949
5432
|
else if (e.endsWith(".php")) out.push(full);
|
|
4950
5433
|
}
|
|
@@ -4981,7 +5464,7 @@ var laravelPhp2 = {
|
|
|
4981
5464
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
4982
5465
|
if (locale === "vendor") continue;
|
|
4983
5466
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4984
|
-
const localeDir =
|
|
5467
|
+
const localeDir = join11(localeRoot, locale);
|
|
4985
5468
|
locales.push(locale);
|
|
4986
5469
|
for (const file of listPhpFiles(localeDir)) {
|
|
4987
5470
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -5004,8 +5487,8 @@ var laravelPhp2 = {
|
|
|
5004
5487
|
};
|
|
5005
5488
|
|
|
5006
5489
|
// src/server/import/parsers/flutter-arb.ts
|
|
5007
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
5008
|
-
import { join as
|
|
5490
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync16 } from "fs";
|
|
5491
|
+
import { join as join12 } from "path";
|
|
5009
5492
|
var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5010
5493
|
function localeFromArbName(file) {
|
|
5011
5494
|
const m = file.match(/^(.+)\.arb$/);
|
|
@@ -5041,7 +5524,7 @@ var flutterArb2 = {
|
|
|
5041
5524
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5042
5525
|
let data;
|
|
5043
5526
|
try {
|
|
5044
|
-
data = JSON.parse(
|
|
5527
|
+
data = JSON.parse(readFileSync16(join12(localeRoot, file), "utf8"));
|
|
5045
5528
|
} catch (e) {
|
|
5046
5529
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
5047
5530
|
continue;
|
|
@@ -5066,8 +5549,8 @@ var flutterArb2 = {
|
|
|
5066
5549
|
};
|
|
5067
5550
|
|
|
5068
5551
|
// src/server/import/parsers/apple-strings.ts
|
|
5069
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
5070
|
-
import { join as
|
|
5552
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync17, statSync as statSync5 } from "fs";
|
|
5553
|
+
import { join as join13 } from "path";
|
|
5071
5554
|
var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5072
5555
|
var TABLE = "Localizable.strings";
|
|
5073
5556
|
function localeFromLproj(dir) {
|
|
@@ -5183,16 +5666,16 @@ var appleStrings2 = {
|
|
|
5183
5666
|
const locale = localeFromLproj(dir);
|
|
5184
5667
|
if (!locale) continue;
|
|
5185
5668
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5186
|
-
const file =
|
|
5669
|
+
const file = join13(localeRoot, dir, TABLE);
|
|
5187
5670
|
let text;
|
|
5188
5671
|
try {
|
|
5189
5672
|
if (!statSync5(file).isFile()) continue;
|
|
5190
|
-
text =
|
|
5673
|
+
text = readFileSync17(file, "utf8");
|
|
5191
5674
|
} catch {
|
|
5192
5675
|
continue;
|
|
5193
5676
|
}
|
|
5194
5677
|
locales.push(locale);
|
|
5195
|
-
const others = readdirSync8(
|
|
5678
|
+
const others = readdirSync8(join13(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
5196
5679
|
if (others.length) {
|
|
5197
5680
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
5198
5681
|
}
|
|
@@ -5205,8 +5688,8 @@ var appleStrings2 = {
|
|
|
5205
5688
|
};
|
|
5206
5689
|
|
|
5207
5690
|
// src/server/import/parsers/angular-xliff.ts
|
|
5208
|
-
import { readdirSync as readdirSync9, readFileSync as
|
|
5209
|
-
import { join as
|
|
5691
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
|
|
5692
|
+
import { join as join14 } from "path";
|
|
5210
5693
|
var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5211
5694
|
var FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
|
|
5212
5695
|
function decodeEntities(s) {
|
|
@@ -5273,7 +5756,7 @@ var angularXliff2 = {
|
|
|
5273
5756
|
if (fnameLocale !== void 0 && !LOCALE_RE6.test(fnameLocale)) continue;
|
|
5274
5757
|
let xml;
|
|
5275
5758
|
try {
|
|
5276
|
-
xml =
|
|
5759
|
+
xml = readFileSync18(join14(localeRoot, file), "utf8");
|
|
5277
5760
|
} catch (e) {
|
|
5278
5761
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
5279
5762
|
continue;
|
|
@@ -5318,8 +5801,8 @@ var angularXliff2 = {
|
|
|
5318
5801
|
};
|
|
5319
5802
|
|
|
5320
5803
|
// src/server/import/parsers/gettext-po.ts
|
|
5321
|
-
import { readdirSync as readdirSync10, readFileSync as
|
|
5322
|
-
import { join as
|
|
5804
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync19 } from "fs";
|
|
5805
|
+
import { join as join15 } from "path";
|
|
5323
5806
|
var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5324
5807
|
var DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
|
|
5325
5808
|
var CONT_RE = /^[ \t]*"(.*)"\s*$/;
|
|
@@ -5411,17 +5894,17 @@ function discoverPoFiles(root) {
|
|
|
5411
5894
|
for (const e of entries) {
|
|
5412
5895
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
5413
5896
|
const base = e.name.slice(0, -3);
|
|
5414
|
-
found.push({ path:
|
|
5897
|
+
found.push({ path: join15(root, e.name), rel: e.name, locale: LOCALE_RE7.test(base) ? base : null });
|
|
5415
5898
|
} else if (e.isDirectory() && LOCALE_RE7.test(e.name)) {
|
|
5416
|
-
for (const sub of [
|
|
5899
|
+
for (const sub of [join15(e.name, "LC_MESSAGES"), e.name]) {
|
|
5417
5900
|
let names;
|
|
5418
5901
|
try {
|
|
5419
|
-
names = readdirSync10(
|
|
5902
|
+
names = readdirSync10(join15(root, sub)).sort();
|
|
5420
5903
|
} catch {
|
|
5421
5904
|
continue;
|
|
5422
5905
|
}
|
|
5423
5906
|
for (const f of names) {
|
|
5424
|
-
if (f.endsWith(".po")) found.push({ path:
|
|
5907
|
+
if (f.endsWith(".po")) found.push({ path: join15(root, sub, f), rel: join15(sub, f), locale: e.name });
|
|
5425
5908
|
}
|
|
5426
5909
|
}
|
|
5427
5910
|
}
|
|
@@ -5437,7 +5920,7 @@ var gettextPo2 = {
|
|
|
5437
5920
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
5438
5921
|
let entries;
|
|
5439
5922
|
try {
|
|
5440
|
-
entries = parseEntries(
|
|
5923
|
+
entries = parseEntries(readFileSync19(file.path, "utf8"));
|
|
5441
5924
|
} catch (e) {
|
|
5442
5925
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
5443
5926
|
continue;
|
|
@@ -5482,8 +5965,8 @@ var gettextPo2 = {
|
|
|
5482
5965
|
};
|
|
5483
5966
|
|
|
5484
5967
|
// src/server/import/parsers/i18next-json.ts
|
|
5485
|
-
import { readdirSync as readdirSync11, readFileSync as
|
|
5486
|
-
import { join as
|
|
5968
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync20, statSync as statSync6 } from "fs";
|
|
5969
|
+
import { join as join16 } from "path";
|
|
5487
5970
|
var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5488
5971
|
var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
5489
5972
|
var PLURAL_ARG = "count";
|
|
@@ -5502,7 +5985,7 @@ function fromI18next(value) {
|
|
|
5502
5985
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
5503
5986
|
let data;
|
|
5504
5987
|
try {
|
|
5505
|
-
data = JSON.parse(
|
|
5988
|
+
data = JSON.parse(readFileSync20(path, "utf8"));
|
|
5506
5989
|
} catch (e) {
|
|
5507
5990
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
5508
5991
|
return false;
|
|
@@ -5544,7 +6027,7 @@ var i18nextJson2 = {
|
|
|
5544
6027
|
const keys = {};
|
|
5545
6028
|
const locales = [];
|
|
5546
6029
|
for (const entry of readdirSync11(localeRoot).sort()) {
|
|
5547
|
-
const full =
|
|
6030
|
+
const full = join16(localeRoot, entry);
|
|
5548
6031
|
if (safeIsDir2(full)) {
|
|
5549
6032
|
if (!LOCALE_RE8.test(entry)) continue;
|
|
5550
6033
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
@@ -5553,7 +6036,7 @@ var i18nextJson2 = {
|
|
|
5553
6036
|
if (!file.endsWith(".json")) continue;
|
|
5554
6037
|
const ns = file.slice(0, -".json".length);
|
|
5555
6038
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
5556
|
-
if (ingestFile(
|
|
6039
|
+
if (ingestFile(join16(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
5557
6040
|
}
|
|
5558
6041
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
5559
6042
|
} else if (entry.endsWith(".json")) {
|
|
@@ -5570,8 +6053,8 @@ var i18nextJson2 = {
|
|
|
5570
6053
|
};
|
|
5571
6054
|
|
|
5572
6055
|
// src/server/import/parsers/rails-yaml.ts
|
|
5573
|
-
import { readdirSync as readdirSync12, readFileSync as
|
|
5574
|
-
import { join as
|
|
6056
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync21 } from "fs";
|
|
6057
|
+
import { join as join17 } from "path";
|
|
5575
6058
|
var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
|
|
5576
6059
|
var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
5577
6060
|
function makeNode() {
|
|
@@ -5789,7 +6272,7 @@ var railsYaml2 = {
|
|
|
5789
6272
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5790
6273
|
let text;
|
|
5791
6274
|
try {
|
|
5792
|
-
text =
|
|
6275
|
+
text = readFileSync21(join17(localeRoot, file), "utf8");
|
|
5793
6276
|
} catch (e) {
|
|
5794
6277
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5795
6278
|
continue;
|
|
@@ -5810,8 +6293,8 @@ var railsYaml2 = {
|
|
|
5810
6293
|
};
|
|
5811
6294
|
|
|
5812
6295
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5813
|
-
import { readdirSync as readdirSync13, readFileSync as
|
|
5814
|
-
import { join as
|
|
6296
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync22, statSync as statSync7 } from "fs";
|
|
6297
|
+
import { join as join18 } from "path";
|
|
5815
6298
|
var LOCALE_RE10 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5816
6299
|
var TABLE2 = "Localizable.stringsdict";
|
|
5817
6300
|
function localeFromLproj2(dir) {
|
|
@@ -5965,16 +6448,16 @@ var appleStringsdict2 = {
|
|
|
5965
6448
|
const locale = localeFromLproj2(dir);
|
|
5966
6449
|
if (!locale) continue;
|
|
5967
6450
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5968
|
-
const file =
|
|
6451
|
+
const file = join18(localeRoot, dir, TABLE2);
|
|
5969
6452
|
let text;
|
|
5970
6453
|
try {
|
|
5971
6454
|
if (!statSync7(file).isFile()) continue;
|
|
5972
|
-
text =
|
|
6455
|
+
text = readFileSync22(file, "utf8");
|
|
5973
6456
|
} catch {
|
|
5974
6457
|
continue;
|
|
5975
6458
|
}
|
|
5976
6459
|
locales.push(locale);
|
|
5977
|
-
const others = readdirSync13(
|
|
6460
|
+
const others = readdirSync13(join18(localeRoot, dir)).filter(
|
|
5978
6461
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5979
6462
|
);
|
|
5980
6463
|
if (others.length) {
|
|
@@ -6098,6 +6581,7 @@ function assemble2(parsed, opts) {
|
|
|
6098
6581
|
spelling: { customWords: [] }
|
|
6099
6582
|
},
|
|
6100
6583
|
glossary: [],
|
|
6584
|
+
glossarySuggestions: [],
|
|
6101
6585
|
keys,
|
|
6102
6586
|
warnings
|
|
6103
6587
|
};
|
|
@@ -6294,7 +6778,7 @@ function refreshLocationUsage(projectRoot, format) {
|
|
|
6294
6778
|
}
|
|
6295
6779
|
|
|
6296
6780
|
// src/server/export-run.ts
|
|
6297
|
-
import { existsSync as
|
|
6781
|
+
import { existsSync as existsSync13, readFileSync as readFileSync23, readdirSync as readdirSync14, rmdirSync, statSync as statSync8, unlinkSync } from "fs";
|
|
6298
6782
|
import { dirname as dirname2, resolve as resolve7, sep } from "path";
|
|
6299
6783
|
function effectiveLocales(config) {
|
|
6300
6784
|
const limit = config.exportLocales;
|
|
@@ -6307,11 +6791,11 @@ function narrowForExport(state) {
|
|
|
6307
6791
|
return { ...state, config: { ...state.config, locales } };
|
|
6308
6792
|
}
|
|
6309
6793
|
var LOCALE_TOKEN = /^[a-z]{2,3}([_-][a-z0-9]+)*$/i;
|
|
6310
|
-
function
|
|
6794
|
+
function escapeRegExp2(s) {
|
|
6311
6795
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6312
6796
|
}
|
|
6313
6797
|
function segmentRegExp(segment) {
|
|
6314
|
-
const pattern =
|
|
6798
|
+
const pattern = escapeRegExp2(segment).replaceAll("\\{locale\\}", "(?<locale>[A-Za-z0-9_-]+)").replaceAll("\\{namespace\\}", "[^/]*");
|
|
6315
6799
|
return new RegExp(`^${pattern}$`);
|
|
6316
6800
|
}
|
|
6317
6801
|
function removeEmptyDirs(dir, stopAt) {
|
|
@@ -6337,7 +6821,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
|
6337
6821
|
if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
|
|
6338
6822
|
const next = resolve7(dir, segment);
|
|
6339
6823
|
if (isLast) {
|
|
6340
|
-
if (stale(locale) &&
|
|
6824
|
+
if (stale(locale) && existsSync13(next) && statSync8(next).isFile()) {
|
|
6341
6825
|
unlinkSync(next);
|
|
6342
6826
|
deleted++;
|
|
6343
6827
|
removeEmptyDirs(dir, root);
|
|
@@ -6393,7 +6877,7 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
6393
6877
|
writtenPaths.add(abs);
|
|
6394
6878
|
let current = null;
|
|
6395
6879
|
try {
|
|
6396
|
-
current =
|
|
6880
|
+
current = readFileSync23(abs, "utf8");
|
|
6397
6881
|
} catch {
|
|
6398
6882
|
}
|
|
6399
6883
|
if (current === f.contents) {
|
|
@@ -6410,17 +6894,17 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
6410
6894
|
}
|
|
6411
6895
|
|
|
6412
6896
|
// src/server/ui-prefs.ts
|
|
6413
|
-
import { readFileSync as
|
|
6414
|
-
import { homedir } from "os";
|
|
6415
|
-
import { join as
|
|
6897
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
6898
|
+
import { homedir as homedir2 } from "os";
|
|
6899
|
+
import { join as join19 } from "path";
|
|
6416
6900
|
var THEMES = ["system", "light", "dark"];
|
|
6417
6901
|
var isThemeMode = (v) => THEMES.includes(v);
|
|
6418
6902
|
var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
6419
|
-
var defaultUiPrefsPath = () =>
|
|
6903
|
+
var defaultUiPrefsPath = () => join19(homedir2(), ".glotfile", "ui.json");
|
|
6420
6904
|
var DEFAULTS = { theme: "system" };
|
|
6421
6905
|
function readJson(path) {
|
|
6422
6906
|
try {
|
|
6423
|
-
const parsed = JSON.parse(
|
|
6907
|
+
const parsed = JSON.parse(readFileSync24(path, "utf8"));
|
|
6424
6908
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
6425
6909
|
} catch {
|
|
6426
6910
|
return {};
|
|
@@ -6439,7 +6923,7 @@ function saveUiPrefs(path, prefs) {
|
|
|
6439
6923
|
}
|
|
6440
6924
|
|
|
6441
6925
|
// src/server/local-settings.ts
|
|
6442
|
-
import { readFileSync as
|
|
6926
|
+
import { readFileSync as readFileSync25 } from "fs";
|
|
6443
6927
|
import { resolve as resolve8 } from "path";
|
|
6444
6928
|
var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
|
|
6445
6929
|
var isEditorId = (v) => EDITOR_IDS.includes(v);
|
|
@@ -6454,7 +6938,7 @@ var DEFAULT_EDITOR = "vscode";
|
|
|
6454
6938
|
var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
|
|
6455
6939
|
function readJson2(path) {
|
|
6456
6940
|
try {
|
|
6457
|
-
const parsed = JSON.parse(
|
|
6941
|
+
const parsed = JSON.parse(readFileSync25(path, "utf8"));
|
|
6458
6942
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
6459
6943
|
} catch {
|
|
6460
6944
|
return {};
|
|
@@ -6544,7 +7028,7 @@ function createEventHub() {
|
|
|
6544
7028
|
|
|
6545
7029
|
// src/server/watch.ts
|
|
6546
7030
|
import { statSync as statSync9, readdirSync as readdirSync15 } from "fs";
|
|
6547
|
-
import { join as
|
|
7031
|
+
import { join as join20 } from "path";
|
|
6548
7032
|
import { createHash as createHash2 } from "crypto";
|
|
6549
7033
|
function hashState(state) {
|
|
6550
7034
|
return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
|
|
@@ -6560,15 +7044,15 @@ function signature(statePath) {
|
|
|
6560
7044
|
const parts = [];
|
|
6561
7045
|
for (const rel of ["config.json", "keys.json"]) {
|
|
6562
7046
|
try {
|
|
6563
|
-
const s = statSync9(
|
|
7047
|
+
const s = statSync9(join20(dir, rel));
|
|
6564
7048
|
parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
|
|
6565
7049
|
} catch {
|
|
6566
7050
|
}
|
|
6567
7051
|
}
|
|
6568
7052
|
try {
|
|
6569
|
-
for (const name of readdirSync15(
|
|
7053
|
+
for (const name of readdirSync15(join20(dir, "locales")).sort()) {
|
|
6570
7054
|
if (!name.endsWith(".json")) continue;
|
|
6571
|
-
const s = statSync9(
|
|
7055
|
+
const s = statSync9(join20(dir, "locales", name));
|
|
6572
7056
|
parts.push(`${name}:${s.size}:${s.mtimeMs}`);
|
|
6573
7057
|
}
|
|
6574
7058
|
} catch {
|
|
@@ -6641,30 +7125,15 @@ var sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
|
6641
7125
|
var screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
6642
7126
|
function projectName(root) {
|
|
6643
7127
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
6644
|
-
if (
|
|
7128
|
+
if (existsSync14(nameFile)) {
|
|
6645
7129
|
try {
|
|
6646
|
-
const name =
|
|
7130
|
+
const name = readFileSync26(nameFile, "utf8").trim();
|
|
6647
7131
|
if (name) return name;
|
|
6648
7132
|
} catch {
|
|
6649
7133
|
}
|
|
6650
7134
|
}
|
|
6651
7135
|
return basename(root);
|
|
6652
7136
|
}
|
|
6653
|
-
function attachUsageSnippets(targets, cache2, projectRoot) {
|
|
6654
|
-
const fileCache = /* @__PURE__ */ new Map();
|
|
6655
|
-
for (const target of targets) {
|
|
6656
|
-
const allRefs = Object.entries(cache2.files).flatMap(
|
|
6657
|
-
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
6658
|
-
key: r.key,
|
|
6659
|
-
file,
|
|
6660
|
-
line: r.line,
|
|
6661
|
-
col: r.col,
|
|
6662
|
-
scanner: r.scanner
|
|
6663
|
-
}))
|
|
6664
|
-
);
|
|
6665
|
-
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
6666
|
-
}
|
|
6667
|
-
}
|
|
6668
7137
|
function createApi(deps) {
|
|
6669
7138
|
const app = new Hono();
|
|
6670
7139
|
const load = () => loadState(deps.statePath);
|
|
@@ -6796,6 +7265,61 @@ function createApi(deps) {
|
|
|
6796
7265
|
}
|
|
6797
7266
|
return c.json({ ok: true });
|
|
6798
7267
|
});
|
|
7268
|
+
app.post("/ai-test", async (c) => {
|
|
7269
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7270
|
+
const meta = { provider: aiCfg.provider, model: aiCfg.model };
|
|
7271
|
+
let provider;
|
|
7272
|
+
try {
|
|
7273
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7274
|
+
} catch (e) {
|
|
7275
|
+
return c.json({ ok: false, ...meta, error: explainProviderError(aiCfg.provider, e) });
|
|
7276
|
+
}
|
|
7277
|
+
const controller = new AbortController();
|
|
7278
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
7279
|
+
try {
|
|
7280
|
+
const probe = {
|
|
7281
|
+
id: "probe",
|
|
7282
|
+
key: "glotfile.connection-test",
|
|
7283
|
+
source: "Hello",
|
|
7284
|
+
sourceLocale: "en",
|
|
7285
|
+
targetLocale: "es",
|
|
7286
|
+
placeholders: []
|
|
7287
|
+
};
|
|
7288
|
+
await provider.translate([probe], void 0, controller.signal);
|
|
7289
|
+
return c.json({ ok: true, ...meta });
|
|
7290
|
+
} catch (e) {
|
|
7291
|
+
const error = controller.signal.aborted ? "Connection test timed out after 30s \u2014 the provider didn't respond." : explainProviderError(aiCfg.provider, e);
|
|
7292
|
+
return c.json({ ok: false, ...meta, error });
|
|
7293
|
+
} finally {
|
|
7294
|
+
clearTimeout(timer);
|
|
7295
|
+
}
|
|
7296
|
+
});
|
|
7297
|
+
app.get("/prices", (c) => {
|
|
7298
|
+
const cache2 = loadPriceCache();
|
|
7299
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
7300
|
+
const pricing = resolvePricing(ai, cache2);
|
|
7301
|
+
return c.json({
|
|
7302
|
+
source: cache2?.source ?? null,
|
|
7303
|
+
fetchedAt: cache2?.fetchedAt ?? null,
|
|
7304
|
+
modelCount: cache2 ? Object.keys(cache2.models).length : 0,
|
|
7305
|
+
path: defaultPriceCachePath(),
|
|
7306
|
+
resolved: pricing ? { provider: ai.provider, model: ai.model, ...pricing } : null
|
|
7307
|
+
});
|
|
7308
|
+
});
|
|
7309
|
+
app.get("/prices/list", (c) => {
|
|
7310
|
+
const cache2 = loadPriceCache();
|
|
7311
|
+
const models = cache2 ? Object.entries(cache2.models).map(([id, p]) => ({ id, ...p })).sort((a, b) => a.id.localeCompare(b.id)) : [];
|
|
7312
|
+
return c.json({ source: cache2?.source ?? null, fetchedAt: cache2?.fetchedAt ?? null, models });
|
|
7313
|
+
});
|
|
7314
|
+
app.post("/prices/refresh", async (c) => {
|
|
7315
|
+
try {
|
|
7316
|
+
const res = await refreshPrices();
|
|
7317
|
+
invalidatePriceCache();
|
|
7318
|
+
return c.json({ ok: true, ...res });
|
|
7319
|
+
} catch (e) {
|
|
7320
|
+
return c.json({ error: e.message }, 502);
|
|
7321
|
+
}
|
|
7322
|
+
});
|
|
6799
7323
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
6800
7324
|
app.get("/files", (c) => {
|
|
6801
7325
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -6817,7 +7341,7 @@ function createApi(deps) {
|
|
|
6817
7341
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
6818
7342
|
const abs = resolve9(dir, name);
|
|
6819
7343
|
let filePath = null;
|
|
6820
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
7344
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync14(resolve9(abs, "config.json"))) {
|
|
6821
7345
|
filePath = resolve9(dir, `${name}.json`);
|
|
6822
7346
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
6823
7347
|
filePath = abs;
|
|
@@ -6851,7 +7375,7 @@ function createApi(deps) {
|
|
|
6851
7375
|
const resolved = resolve9(projectRoot, path);
|
|
6852
7376
|
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
6853
7377
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
6854
|
-
if (!
|
|
7378
|
+
if (!existsSync14(resolved)) return c.json({ error: "file not found" }, 400);
|
|
6855
7379
|
loadState(resolved);
|
|
6856
7380
|
deps.statePath = resolved;
|
|
6857
7381
|
watcher.retarget(resolved);
|
|
@@ -6913,9 +7437,9 @@ function createApi(deps) {
|
|
|
6913
7437
|
const abs = resolve9(root, screenshot);
|
|
6914
7438
|
const rel = relative4(root, abs);
|
|
6915
7439
|
const seg0 = rel.split(sep2)[0] ?? "";
|
|
6916
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
7440
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync14(abs)) {
|
|
6917
7441
|
try {
|
|
6918
|
-
|
|
7442
|
+
rmSync7(abs);
|
|
6919
7443
|
} catch {
|
|
6920
7444
|
}
|
|
6921
7445
|
}
|
|
@@ -7160,6 +7684,177 @@ function createApi(deps) {
|
|
|
7160
7684
|
logChange({ kind: "glossary", summary: `Deleted glossary term "${term}"`, before });
|
|
7161
7685
|
return c.json({ ok: true });
|
|
7162
7686
|
});
|
|
7687
|
+
app.get("/glossary/suggestions", (c) => {
|
|
7688
|
+
const s = load();
|
|
7689
|
+
const pending = s.glossarySuggestions.filter((x) => x.status === "pending");
|
|
7690
|
+
return c.json(pending.map((x) => ({
|
|
7691
|
+
...x,
|
|
7692
|
+
occurrences: sourceKeysForTerm(s, x.term, { caseSensitive: x.caseSensitive, wholeWord: x.wholeWord }).length
|
|
7693
|
+
})));
|
|
7694
|
+
});
|
|
7695
|
+
app.post("/glossary/suggestions/dismiss", async (c) => {
|
|
7696
|
+
const { term } = await c.req.json();
|
|
7697
|
+
if (typeof term !== "string") return c.json({ error: "term must be a string" }, 400);
|
|
7698
|
+
const s = load();
|
|
7699
|
+
dismissGlossarySuggestion(s, term);
|
|
7700
|
+
persist(s);
|
|
7701
|
+
logChange({ kind: "glossary", summary: `Dismissed suggested term "${term}"` });
|
|
7702
|
+
return c.json({ ok: true });
|
|
7703
|
+
});
|
|
7704
|
+
app.delete("/glossary/suggestions/:term", (c) => {
|
|
7705
|
+
const s = load();
|
|
7706
|
+
const term = decodeURIComponent(c.req.param("term"));
|
|
7707
|
+
removeGlossarySuggestion(s, term);
|
|
7708
|
+
persist(s);
|
|
7709
|
+
return c.json({ ok: true });
|
|
7710
|
+
});
|
|
7711
|
+
app.post("/glossary/suggest", async (c) => {
|
|
7712
|
+
const signal = c.req.raw.signal;
|
|
7713
|
+
const body = await c.req.json().catch(() => ({}));
|
|
7714
|
+
return streamSSE(c, async (stream) => {
|
|
7715
|
+
const s0 = load();
|
|
7716
|
+
const sources = selectGlossarySources(s0, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
|
|
7717
|
+
if (!sources.length) {
|
|
7718
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ added: 0, terms: [] }) });
|
|
7719
|
+
return;
|
|
7720
|
+
}
|
|
7721
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7722
|
+
let provider;
|
|
7723
|
+
try {
|
|
7724
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7725
|
+
} catch (e) {
|
|
7726
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
7727
|
+
return;
|
|
7728
|
+
}
|
|
7729
|
+
const known = knownTermList(s0);
|
|
7730
|
+
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: sources.length }) });
|
|
7731
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
7732
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
7733
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
7734
|
+
const chunks = [];
|
|
7735
|
+
for (let i = 0; i < sources.length; i += batchSize) chunks.push(sources.slice(i, i + batchSize));
|
|
7736
|
+
const all = [];
|
|
7737
|
+
let done = 0;
|
|
7738
|
+
let next = 0;
|
|
7739
|
+
async function worker() {
|
|
7740
|
+
while (next < chunks.length) {
|
|
7741
|
+
if (signal?.aborted) break;
|
|
7742
|
+
const chunkRows = chunks[next++];
|
|
7743
|
+
try {
|
|
7744
|
+
const raw = await provider.complete({ system, content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunkRows, known) }], schema: GLOSSARY_SUGGEST_SCHEMA });
|
|
7745
|
+
all.push(...raw.terms ?? []);
|
|
7746
|
+
} catch (e) {
|
|
7747
|
+
void stream.writeSSE({ event: "warn", data: JSON.stringify({ error: e.message }) });
|
|
7748
|
+
}
|
|
7749
|
+
done += chunkRows.length;
|
|
7750
|
+
void stream.writeSSE({ event: "progress", data: JSON.stringify({ done, total: sources.length }) });
|
|
7751
|
+
}
|
|
7752
|
+
}
|
|
7753
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
7754
|
+
if (signal?.aborted) return;
|
|
7755
|
+
const fresh = load();
|
|
7756
|
+
const added = mergeGlossarySuggestions(fresh, dedupeTerms(all));
|
|
7757
|
+
const usage = provider.takeUsage?.();
|
|
7758
|
+
persist(fresh);
|
|
7759
|
+
appendLog(projectRoot, {
|
|
7760
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7761
|
+
kind: "glossary",
|
|
7762
|
+
summary: `Suggested ${added.length} glossary term(s)`,
|
|
7763
|
+
model: aiCfg.model,
|
|
7764
|
+
system,
|
|
7765
|
+
usage,
|
|
7766
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
7767
|
+
});
|
|
7768
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ added: added.length, terms: added }) });
|
|
7769
|
+
});
|
|
7770
|
+
});
|
|
7771
|
+
app.post("/glossary/suggest/estimate", async (c) => {
|
|
7772
|
+
const body = await c.req.json().catch(() => ({}));
|
|
7773
|
+
const s = load();
|
|
7774
|
+
const sources = selectGlossarySources(s, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
|
|
7775
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7776
|
+
return c.json(estimateGlossarySuggest(sources, knownTermList(s), aiCfg));
|
|
7777
|
+
});
|
|
7778
|
+
app.get("/glossary/suggest/batch/status", async (c) => {
|
|
7779
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7780
|
+
let supported = false;
|
|
7781
|
+
let provider;
|
|
7782
|
+
try {
|
|
7783
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7784
|
+
supported = supportsBatchComplete(provider);
|
|
7785
|
+
} catch {
|
|
7786
|
+
}
|
|
7787
|
+
const pending = loadPendingGlossaryBatch(projectRoot);
|
|
7788
|
+
if (!pending) return c.json({ supported, pending: null });
|
|
7789
|
+
const base = { batchId: pending.batchId, createdAt: pending.createdAt, model: pending.model, total: pending.total };
|
|
7790
|
+
if (!provider || !supportsBatchComplete(provider)) {
|
|
7791
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null } });
|
|
7792
|
+
}
|
|
7793
|
+
try {
|
|
7794
|
+
const status = await provider.translationBatchStatus(pending.batchId);
|
|
7795
|
+
return c.json({ supported, pending: { ...base, status: status.status, counts: status.counts } });
|
|
7796
|
+
} catch (e) {
|
|
7797
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null, error: e.message } });
|
|
7798
|
+
}
|
|
7799
|
+
});
|
|
7800
|
+
app.post("/glossary/suggest/batch", (c) => withTranslateLock(async () => {
|
|
7801
|
+
const body = await c.req.json().catch(() => ({}));
|
|
7802
|
+
const s = load();
|
|
7803
|
+
const sources = selectGlossarySources(s, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
|
|
7804
|
+
if (!sources.length) return c.json({ error: "No source strings to scan." }, 400);
|
|
7805
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7806
|
+
let provider;
|
|
7807
|
+
try {
|
|
7808
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7809
|
+
} catch (e) {
|
|
7810
|
+
return c.json({ error: e.message }, 400);
|
|
7811
|
+
}
|
|
7812
|
+
if (!supportsBatchComplete(provider)) {
|
|
7813
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
7814
|
+
}
|
|
7815
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
7816
|
+
let pending;
|
|
7817
|
+
try {
|
|
7818
|
+
pending = await submitGlossarySuggestBatch(provider, sources, knownTermList(s), batchSize, aiCfg.model, projectRoot);
|
|
7819
|
+
} catch (e) {
|
|
7820
|
+
return c.json({ error: e.message }, 409);
|
|
7821
|
+
}
|
|
7822
|
+
appendLog(projectRoot, {
|
|
7823
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7824
|
+
kind: "glossary",
|
|
7825
|
+
summary: `Submitted glossary suggestion batch ${pending.batchId} (${pending.total} sources)`,
|
|
7826
|
+
model: aiCfg.model
|
|
7827
|
+
});
|
|
7828
|
+
return c.json({ batchId: pending.batchId, total: pending.total });
|
|
7829
|
+
}));
|
|
7830
|
+
app.post("/glossary/suggest/batch/apply", (c) => withTranslateLock(async () => {
|
|
7831
|
+
const pending = loadPendingGlossaryBatch(projectRoot);
|
|
7832
|
+
if (!pending) return c.json({ error: "No pending glossary suggestion batch." }, 404);
|
|
7833
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7834
|
+
let provider;
|
|
7835
|
+
try {
|
|
7836
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7837
|
+
} catch (e) {
|
|
7838
|
+
return c.json({ error: e.message }, 400);
|
|
7839
|
+
}
|
|
7840
|
+
if (!supportsBatchComplete(provider)) {
|
|
7841
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
7842
|
+
}
|
|
7843
|
+
const outcome = await applyGlossarySuggestBatchResults(load, persist, provider, pending, projectRoot, aiCfg);
|
|
7844
|
+
return c.json(outcome);
|
|
7845
|
+
}));
|
|
7846
|
+
app.post("/glossary/suggest/batch/cancel", async (c) => {
|
|
7847
|
+
const pending = loadPendingGlossaryBatch(projectRoot);
|
|
7848
|
+
if (!pending) return c.json({ error: "No pending glossary suggestion batch." }, 404);
|
|
7849
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7850
|
+
try {
|
|
7851
|
+
const provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7852
|
+
if (supportsBatchComplete(provider)) await provider.cancelTranslationBatch(pending.batchId);
|
|
7853
|
+
} catch {
|
|
7854
|
+
}
|
|
7855
|
+
clearPendingGlossaryBatch(projectRoot);
|
|
7856
|
+
return c.json({ canceled: pending.batchId });
|
|
7857
|
+
});
|
|
7163
7858
|
app.post("/keys/:key/screenshot", async (c) => {
|
|
7164
7859
|
const key = c.req.param("key");
|
|
7165
7860
|
const body = await c.req.parseBody();
|
|
@@ -7345,7 +8040,7 @@ function createApi(deps) {
|
|
|
7345
8040
|
try {
|
|
7346
8041
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7347
8042
|
} catch (e) {
|
|
7348
|
-
await stream.writeSSE({ event: "error", data: JSON.stringify({ error:
|
|
8043
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: explainProviderError(aiCfg.provider, e) }) });
|
|
7349
8044
|
return;
|
|
7350
8045
|
}
|
|
7351
8046
|
const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
@@ -7362,58 +8057,65 @@ function createApi(deps) {
|
|
|
7362
8057
|
event: "start",
|
|
7363
8058
|
data: JSON.stringify({ total: reqs.length, locales: [...localeTotals].map(([locale, total]) => ({ locale, total })) })
|
|
7364
8059
|
});
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
8060
|
+
try {
|
|
8061
|
+
await runLocaleParallel(reqs, provider, {
|
|
8062
|
+
// Announce a language the moment a worker picks it up — this is the
|
|
8063
|
+
// signal that "something is happening" during the long first LLM call.
|
|
8064
|
+
onLocaleStart: (locale) => {
|
|
8065
|
+
void stream.writeSSE({ event: "locale-start", data: JSON.stringify({ locale }) });
|
|
8066
|
+
},
|
|
8067
|
+
onBatchComplete: (done, total, batchResults, locale) => {
|
|
8068
|
+
const fresh = load();
|
|
8069
|
+
const { written, errors } = applyResults(fresh, reqs, batchResults);
|
|
8070
|
+
persist(fresh);
|
|
8071
|
+
totalWritten += written;
|
|
8072
|
+
allErrors.push(...errors);
|
|
8073
|
+
const usage = provider.takeUsage?.();
|
|
8074
|
+
appendLog(projectRoot, {
|
|
8075
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8076
|
+
kind: "translate",
|
|
8077
|
+
summary: `Translated ${batchResults.length} item(s)`,
|
|
8078
|
+
model: aiCfg.model,
|
|
8079
|
+
system,
|
|
8080
|
+
items: batchResults.map((r) => {
|
|
8081
|
+
const req = reqById.get(r.id);
|
|
8082
|
+
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 };
|
|
8083
|
+
}),
|
|
8084
|
+
results: batchResults,
|
|
8085
|
+
usage,
|
|
8086
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
8087
|
+
});
|
|
8088
|
+
const ld = (localeDone.get(locale) ?? 0) + batchResults.length;
|
|
8089
|
+
localeDone.set(locale, ld);
|
|
8090
|
+
console.log(`[translate] ${done}/${total}`);
|
|
8091
|
+
void stream.writeSSE({
|
|
8092
|
+
event: "progress",
|
|
8093
|
+
data: JSON.stringify({ done, total, written: totalWritten, errors, locale, localeDone: ld, localeTotal: localeTotals.get(locale) ?? 0 })
|
|
8094
|
+
});
|
|
8095
|
+
},
|
|
8096
|
+
onLocaleDone: (locale) => {
|
|
8097
|
+
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
8098
|
+
},
|
|
8099
|
+
// Record the raw reply so an unparseable model response is diagnosable
|
|
8100
|
+
// from the activity log instead of vanishing into per-item errors.
|
|
8101
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
8102
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
8103
|
+
appendLog(projectRoot, {
|
|
8104
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8105
|
+
kind: "translate",
|
|
8106
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
8107
|
+
model: aiCfg.model,
|
|
8108
|
+
locale,
|
|
8109
|
+
raw
|
|
8110
|
+
});
|
|
8111
|
+
}
|
|
8112
|
+
}, aiCfg.concurrency, signal, aiCfg.batchSize);
|
|
8113
|
+
} catch (e) {
|
|
8114
|
+
if (!signal?.aborted) {
|
|
8115
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: explainProviderError(aiCfg.provider, e) }) });
|
|
7415
8116
|
}
|
|
7416
|
-
|
|
8117
|
+
return;
|
|
8118
|
+
}
|
|
7417
8119
|
if (!signal?.aborted) {
|
|
7418
8120
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
7419
8121
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -7440,23 +8142,28 @@ function createApi(deps) {
|
|
|
7440
8142
|
try {
|
|
7441
8143
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7442
8144
|
} catch (e) {
|
|
7443
|
-
return c.json({ error:
|
|
8145
|
+
return c.json({ error: explainProviderError(aiCfg.provider, e) }, 400);
|
|
7444
8146
|
}
|
|
7445
8147
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
7446
8148
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
8149
|
+
let results;
|
|
8150
|
+
try {
|
|
8151
|
+
results = await runLocaleParallel(toTranslate, provider, {
|
|
8152
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
8153
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
8154
|
+
appendLog(projectRoot, {
|
|
8155
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8156
|
+
kind: "translate",
|
|
8157
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
8158
|
+
model: aiCfg.model,
|
|
8159
|
+
locale,
|
|
8160
|
+
raw
|
|
8161
|
+
});
|
|
8162
|
+
}
|
|
8163
|
+
}, aiCfg.concurrency, void 0, aiCfg.batchSize);
|
|
8164
|
+
} catch (e) {
|
|
8165
|
+
return c.json({ error: explainProviderError(aiCfg.provider, e) }, 502);
|
|
8166
|
+
}
|
|
7460
8167
|
const latest = load();
|
|
7461
8168
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
7462
8169
|
const usage = provider.takeUsage?.();
|
|
@@ -7727,6 +8434,22 @@ function createApi(deps) {
|
|
|
7727
8434
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
7728
8435
|
});
|
|
7729
8436
|
});
|
|
8437
|
+
app.post("/context/estimate", async (c) => {
|
|
8438
|
+
const body = await c.req.json().catch(() => ({}));
|
|
8439
|
+
const cache2 = loadUsageCache(projectRoot);
|
|
8440
|
+
if (!cache2) return c.json({ error: "No usage index found. Run 'glotfile scan' first." }, 400);
|
|
8441
|
+
const targets = selectContextTargets(load(), {
|
|
8442
|
+
all: body.all,
|
|
8443
|
+
keyGlob: body.keyGlob,
|
|
8444
|
+
limit: body.limit,
|
|
8445
|
+
since: body.since,
|
|
8446
|
+
keys: body.keys,
|
|
8447
|
+
force: body.force
|
|
8448
|
+
}, cache2, body.lastRunAt);
|
|
8449
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8450
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
8451
|
+
return c.json(estimateContext(targets, aiCfg));
|
|
8452
|
+
});
|
|
7730
8453
|
app.get("/context/batch/status", async (c) => {
|
|
7731
8454
|
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7732
8455
|
let supported = false;
|
|
@@ -7829,7 +8552,7 @@ function createApi(deps) {
|
|
|
7829
8552
|
|
|
7830
8553
|
// src/server/server.ts
|
|
7831
8554
|
var here = dirname4(fileURLToPath(import.meta.url));
|
|
7832
|
-
var DEFAULT_UI_DIR =
|
|
8555
|
+
var DEFAULT_UI_DIR = join21(here, "..", "ui");
|
|
7833
8556
|
var MIME = {
|
|
7834
8557
|
".html": "text/html; charset=utf-8",
|
|
7835
8558
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7896,7 +8619,7 @@ function buildApp(opts) {
|
|
|
7896
8619
|
const file = await readFileResponse(target);
|
|
7897
8620
|
if (file) return file;
|
|
7898
8621
|
}
|
|
7899
|
-
const index = await readFileResponse(
|
|
8622
|
+
const index = await readFileResponse(join21(root, "index.html"));
|
|
7900
8623
|
if (index) return index;
|
|
7901
8624
|
return c.notFound();
|
|
7902
8625
|
});
|