glotfile 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/cli.js +924 -230
- package/dist/server/server.js +724 -231
- package/dist/ui/assets/index-Bjwiz6KQ.css +1 -0
- package/dist/ui/assets/{index-5Imdw0oX.js → index-Dwn9g3g-.js} +67 -12
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BaHu118N.css +0 -1
package/dist/server/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 join20, 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 readFileSync25, existsSync as existsSync13, readdirSync as readdirSync16, statSync as statSync10, rmSync as rmSync6 } 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
|
}
|
|
@@ -4552,13 +4822,83 @@ function estimateTranslation(state, ai, opts) {
|
|
|
4552
4822
|
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4553
4823
|
};
|
|
4554
4824
|
}
|
|
4825
|
+
var CONTEXT_REPLY_OVERHEAD = 16;
|
|
4826
|
+
var TYPICAL_CONTEXT_TOKENS = 35;
|
|
4827
|
+
function estimateContext(targets, ai) {
|
|
4828
|
+
const batchSize = Math.max(1, ai.contextBatchSize ?? ai.batchSize ?? 10);
|
|
4829
|
+
const batches = chunk(targets, batchSize);
|
|
4830
|
+
const system = buildContextSystemPrompt();
|
|
4831
|
+
let inputTokens = 0;
|
|
4832
|
+
let outputTokens = 0;
|
|
4833
|
+
for (const batch of batches) {
|
|
4834
|
+
inputTokens += estimateTokens(system) + estimateTokens(buildContextBatchPrompt(batch));
|
|
4835
|
+
outputTokens += batch.length * (CONTEXT_REPLY_OVERHEAD + TYPICAL_CONTEXT_TOKENS);
|
|
4836
|
+
}
|
|
4837
|
+
const pricing = resolvePricing(ai);
|
|
4838
|
+
return {
|
|
4839
|
+
keys: targets.length,
|
|
4840
|
+
batches: batches.length,
|
|
4841
|
+
inputTokens,
|
|
4842
|
+
outputTokens,
|
|
4843
|
+
pricing,
|
|
4844
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4845
|
+
};
|
|
4846
|
+
}
|
|
4847
|
+
|
|
4848
|
+
// src/server/ai/price-fetch.ts
|
|
4849
|
+
var MODELS_DEV_URL = "https://models.dev/api.json";
|
|
4850
|
+
var priceUrl = () => process.env.GLOTFILE_PRICES_URL || MODELS_DEV_URL;
|
|
4851
|
+
var PROVIDER_PREFERENCE = ["anthropic", "openai", "google", "meta-llama", "mistral", "deepseek", "xai", "groq"];
|
|
4852
|
+
var providerRank = (provId) => {
|
|
4853
|
+
const i = PROVIDER_PREFERENCE.indexOf(provId);
|
|
4854
|
+
return i === -1 ? PROVIDER_PREFERENCE.length : i;
|
|
4855
|
+
};
|
|
4856
|
+
function normalizeModelsDevPrices(api) {
|
|
4857
|
+
const out = {};
|
|
4858
|
+
const ranks = {};
|
|
4859
|
+
if (!api || typeof api !== "object") return out;
|
|
4860
|
+
for (const [provId, prov] of Object.entries(api)) {
|
|
4861
|
+
const models = prov?.models;
|
|
4862
|
+
if (!models || typeof models !== "object") continue;
|
|
4863
|
+
const rank = providerRank(provId);
|
|
4864
|
+
for (const [modelKey, model] of Object.entries(models)) {
|
|
4865
|
+
const cost = model?.cost;
|
|
4866
|
+
if (!cost || typeof cost.input !== "number" || typeof cost.output !== "number") continue;
|
|
4867
|
+
const bareId = bareModelId(modelKey);
|
|
4868
|
+
if (!bareId) continue;
|
|
4869
|
+
const existingRank = ranks[bareId];
|
|
4870
|
+
if (existingRank !== void 0 && existingRank <= rank) continue;
|
|
4871
|
+
const price = { inputPerMTok: cost.input, outputPerMTok: cost.output };
|
|
4872
|
+
if (typeof cost.cache_read === "number") price.cacheReadPerMTok = cost.cache_read;
|
|
4873
|
+
if (typeof cost.cache_write === "number") price.cacheWritePerMTok = cost.cache_write;
|
|
4874
|
+
out[bareId] = price;
|
|
4875
|
+
ranks[bareId] = rank;
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
return out;
|
|
4879
|
+
}
|
|
4880
|
+
var defaultNow = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
4881
|
+
async function refreshPrices(opts = {}) {
|
|
4882
|
+
const url = opts.url ?? priceUrl();
|
|
4883
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
4884
|
+
const res = await doFetch(url);
|
|
4885
|
+
if (!res.ok) throw new Error(`Failed to fetch prices from ${url}: HTTP ${res.status}`);
|
|
4886
|
+
const api = await res.json();
|
|
4887
|
+
const models = normalizeModelsDevPrices(api);
|
|
4888
|
+
const modelCount = Object.keys(models).length;
|
|
4889
|
+
if (modelCount === 0) throw new Error(`No model prices found in response from ${url}`);
|
|
4890
|
+
const cache2 = { source: "models.dev", fetchedAt: (opts.now ?? defaultNow)(), models };
|
|
4891
|
+
const path = opts.path ?? defaultPriceCachePath();
|
|
4892
|
+
savePriceCache(cache2, path);
|
|
4893
|
+
return { source: cache2.source, fetchedAt: cache2.fetchedAt, modelCount, path };
|
|
4894
|
+
}
|
|
4555
4895
|
|
|
4556
4896
|
// src/server/import/run.ts
|
|
4557
4897
|
import { relative as relative3 } from "path";
|
|
4558
4898
|
|
|
4559
4899
|
// src/server/import/detect.ts
|
|
4560
|
-
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as
|
|
4561
|
-
import { join as
|
|
4900
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
4901
|
+
import { join as join7 } from "path";
|
|
4562
4902
|
var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4563
4903
|
var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
4564
4904
|
function safeIsDir(p) {
|
|
@@ -4569,7 +4909,7 @@ function safeIsDir(p) {
|
|
|
4569
4909
|
}
|
|
4570
4910
|
}
|
|
4571
4911
|
function listDirs(dir) {
|
|
4572
|
-
return readdirSync3(dir).filter((e) => safeIsDir(
|
|
4912
|
+
return readdirSync3(dir).filter((e) => safeIsDir(join7(dir, e)));
|
|
4573
4913
|
}
|
|
4574
4914
|
function fileCount(dir) {
|
|
4575
4915
|
try {
|
|
@@ -4583,23 +4923,23 @@ function pickSource(locales, sizeOf) {
|
|
|
4583
4923
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4584
4924
|
}
|
|
4585
4925
|
function detectLaravel(root) {
|
|
4586
|
-
const localeRoot = [
|
|
4926
|
+
const localeRoot = [join7(root, "resources", "lang"), join7(root, "lang")].find(safeIsDir);
|
|
4587
4927
|
if (!localeRoot) return null;
|
|
4588
4928
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4589
4929
|
if (locales.length === 0) return null;
|
|
4590
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
4930
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join7(localeRoot, loc)));
|
|
4591
4931
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4592
4932
|
}
|
|
4593
4933
|
function detectVue(root, forced = false) {
|
|
4594
4934
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4595
|
-
const localeRoot =
|
|
4935
|
+
const localeRoot = join7(root, rel);
|
|
4596
4936
|
if (!safeIsDir(localeRoot)) continue;
|
|
4597
4937
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4598
4938
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4599
4939
|
if (enough) {
|
|
4600
4940
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4601
4941
|
try {
|
|
4602
|
-
return statSync3(
|
|
4942
|
+
return statSync3(join7(localeRoot, `${loc}.json`)).size;
|
|
4603
4943
|
} catch {
|
|
4604
4944
|
return 0;
|
|
4605
4945
|
}
|
|
@@ -4613,9 +4953,9 @@ var NEXT_INTL_CONFIG_CANDIDATES = ["src/i18n/request.ts", "i18n/request.ts", "sr
|
|
|
4613
4953
|
var NEXT_INTL_ROUTING_CANDIDATES = ["src/i18n/routing.ts", "i18n/routing.ts", "src/i18n/routing.js", "i18n/routing.js"];
|
|
4614
4954
|
var NEXT_INTL_DIR_CANDIDATES = ["messages", "src/messages", "locales", "src/locales", "src/i18n/messages"];
|
|
4615
4955
|
function hasNextIntlSignal(root) {
|
|
4616
|
-
if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync11(
|
|
4956
|
+
if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync11(join7(root, rel)))) return true;
|
|
4617
4957
|
try {
|
|
4618
|
-
const pkg = JSON.parse(
|
|
4958
|
+
const pkg = JSON.parse(readFileSync12(join7(root, "package.json"), "utf8"));
|
|
4619
4959
|
if (pkg.dependencies?.["next-intl"] || pkg.devDependencies?.["next-intl"]) return true;
|
|
4620
4960
|
} catch {
|
|
4621
4961
|
}
|
|
@@ -4624,7 +4964,7 @@ function hasNextIntlSignal(root) {
|
|
|
4624
4964
|
function nextIntlDefaultLocale(root) {
|
|
4625
4965
|
for (const rel of NEXT_INTL_ROUTING_CANDIDATES) {
|
|
4626
4966
|
try {
|
|
4627
|
-
const m =
|
|
4967
|
+
const m = readFileSync12(join7(root, rel), "utf8").match(/defaultLocale\s*:\s*['"]([^'"]+)['"]/);
|
|
4628
4968
|
if (m) return m[1];
|
|
4629
4969
|
} catch {
|
|
4630
4970
|
}
|
|
@@ -4634,14 +4974,14 @@ function nextIntlDefaultLocale(root) {
|
|
|
4634
4974
|
function detectNextIntl(root, forced = false) {
|
|
4635
4975
|
if (!forced && !hasNextIntlSignal(root)) return null;
|
|
4636
4976
|
for (const rel of NEXT_INTL_DIR_CANDIDATES) {
|
|
4637
|
-
const localeRoot =
|
|
4977
|
+
const localeRoot = join7(root, rel);
|
|
4638
4978
|
if (!safeIsDir(localeRoot)) continue;
|
|
4639
4979
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4640
4980
|
if (locales.length === 0) continue;
|
|
4641
4981
|
const def = nextIntlDefaultLocale(root);
|
|
4642
4982
|
const sourceLocale = def && locales.includes(def) ? def : pickSource(locales, (loc) => {
|
|
4643
4983
|
try {
|
|
4644
|
-
return statSync3(
|
|
4984
|
+
return statSync3(join7(localeRoot, `${loc}.json`)).size;
|
|
4645
4985
|
} catch {
|
|
4646
4986
|
return 0;
|
|
4647
4987
|
}
|
|
@@ -4652,7 +4992,7 @@ function detectNextIntl(root, forced = false) {
|
|
|
4652
4992
|
}
|
|
4653
4993
|
function detectArb(root) {
|
|
4654
4994
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4655
|
-
const localeRoot =
|
|
4995
|
+
const localeRoot = join7(root, rel);
|
|
4656
4996
|
if (!safeIsDir(localeRoot)) continue;
|
|
4657
4997
|
const locales = readdirSync3(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4658
4998
|
if (locales.length >= 1) {
|
|
@@ -4662,10 +5002,10 @@ function detectArb(root) {
|
|
|
4662
5002
|
return null;
|
|
4663
5003
|
}
|
|
4664
5004
|
function lprojLocales(dir) {
|
|
4665
|
-
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(
|
|
5005
|
+
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join7(dir, `${l}.lproj`, "Localizable.strings")));
|
|
4666
5006
|
}
|
|
4667
5007
|
function detectApple(root) {
|
|
4668
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5008
|
+
const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
|
|
4669
5009
|
let best = null;
|
|
4670
5010
|
for (const dir of candidates) {
|
|
4671
5011
|
const locales = lprojLocales(dir);
|
|
@@ -4677,7 +5017,7 @@ function detectApple(root) {
|
|
|
4677
5017
|
locales,
|
|
4678
5018
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4679
5019
|
try {
|
|
4680
|
-
return statSync3(
|
|
5020
|
+
return statSync3(join7(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4681
5021
|
} catch {
|
|
4682
5022
|
return 0;
|
|
4683
5023
|
}
|
|
@@ -4690,7 +5030,7 @@ function detectApple(root) {
|
|
|
4690
5030
|
var ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
|
|
4691
5031
|
function detectAngularXliff(root) {
|
|
4692
5032
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4693
|
-
const localeRoot = rel === "." ? root :
|
|
5033
|
+
const localeRoot = rel === "." ? root : join7(root, rel);
|
|
4694
5034
|
if (!safeIsDir(localeRoot)) continue;
|
|
4695
5035
|
const files = readdirSync3(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4696
5036
|
if (files.length === 0) continue;
|
|
@@ -4698,7 +5038,7 @@ function detectAngularXliff(root) {
|
|
|
4698
5038
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4699
5039
|
let sourceLocale;
|
|
4700
5040
|
try {
|
|
4701
|
-
sourceLocale =
|
|
5041
|
+
sourceLocale = readFileSync12(join7(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4702
5042
|
} catch {
|
|
4703
5043
|
}
|
|
4704
5044
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -4709,14 +5049,14 @@ function detectAngularXliff(root) {
|
|
|
4709
5049
|
return null;
|
|
4710
5050
|
}
|
|
4711
5051
|
function detectRails(root) {
|
|
4712
|
-
const localeRoot =
|
|
5052
|
+
const localeRoot = join7(root, "config", "locales");
|
|
4713
5053
|
if (!safeIsDir(localeRoot)) return null;
|
|
4714
5054
|
const locales = [];
|
|
4715
5055
|
for (const file of readdirSync3(localeRoot).sort()) {
|
|
4716
5056
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
4717
5057
|
let text;
|
|
4718
5058
|
try {
|
|
4719
|
-
text =
|
|
5059
|
+
text = readFileSync12(join7(localeRoot, file), "utf8");
|
|
4720
5060
|
} catch {
|
|
4721
5061
|
continue;
|
|
4722
5062
|
}
|
|
@@ -4731,15 +5071,15 @@ function detectRails(root) {
|
|
|
4731
5071
|
var I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
|
|
4732
5072
|
function detectI18next(root) {
|
|
4733
5073
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4734
|
-
const localeRoot =
|
|
5074
|
+
const localeRoot = join7(root, rel);
|
|
4735
5075
|
if (!safeIsDir(localeRoot)) continue;
|
|
4736
5076
|
const locales = listDirs(localeRoot).filter(
|
|
4737
|
-
(d) => LOCALE_RE.test(d) && readdirSync3(
|
|
5077
|
+
(d) => LOCALE_RE.test(d) && readdirSync3(join7(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4738
5078
|
);
|
|
4739
5079
|
if (locales.length === 0) continue;
|
|
4740
5080
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4741
5081
|
try {
|
|
4742
|
-
return readdirSync3(
|
|
5082
|
+
return readdirSync3(join7(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join7(localeRoot, loc, f)).size, 0);
|
|
4743
5083
|
} catch {
|
|
4744
5084
|
return 0;
|
|
4745
5085
|
}
|
|
@@ -4756,8 +5096,8 @@ function gettextLocales(dir) {
|
|
|
4756
5096
|
if (!locales.includes(flat)) locales.push(flat);
|
|
4757
5097
|
continue;
|
|
4758
5098
|
}
|
|
4759
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
4760
|
-
const sub =
|
|
5099
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join7(dir, entry))) continue;
|
|
5100
|
+
const sub = join7(dir, entry);
|
|
4761
5101
|
const hasPo = (d) => {
|
|
4762
5102
|
try {
|
|
4763
5103
|
return readdirSync3(d).some((f) => f.endsWith(".po"));
|
|
@@ -4765,7 +5105,7 @@ function gettextLocales(dir) {
|
|
|
4765
5105
|
return false;
|
|
4766
5106
|
}
|
|
4767
5107
|
};
|
|
4768
|
-
if (hasPo(
|
|
5108
|
+
if (hasPo(join7(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4769
5109
|
if (!locales.includes(entry)) locales.push(entry);
|
|
4770
5110
|
}
|
|
4771
5111
|
}
|
|
@@ -4774,7 +5114,7 @@ function gettextLocales(dir) {
|
|
|
4774
5114
|
var GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
|
|
4775
5115
|
function detectGettext(root) {
|
|
4776
5116
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4777
|
-
const localeRoot =
|
|
5117
|
+
const localeRoot = join7(root, rel);
|
|
4778
5118
|
if (!safeIsDir(localeRoot)) continue;
|
|
4779
5119
|
const locales = gettextLocales(localeRoot);
|
|
4780
5120
|
if (locales.length === 0) continue;
|
|
@@ -4783,10 +5123,10 @@ function detectGettext(root) {
|
|
|
4783
5123
|
return null;
|
|
4784
5124
|
}
|
|
4785
5125
|
function detectAppleStringsdict(root) {
|
|
4786
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5126
|
+
const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
|
|
4787
5127
|
let best = null;
|
|
4788
5128
|
for (const dir of candidates) {
|
|
4789
|
-
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(
|
|
5129
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join7(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
4790
5130
|
if (locales.length === 0) continue;
|
|
4791
5131
|
if (!best || locales.length > best.locales.length) {
|
|
4792
5132
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4833,8 +5173,8 @@ function detect(root, formatOverride) {
|
|
|
4833
5173
|
}
|
|
4834
5174
|
|
|
4835
5175
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4836
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
4837
|
-
import { join as
|
|
5176
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync13 } from "fs";
|
|
5177
|
+
import { join as join8 } from "path";
|
|
4838
5178
|
|
|
4839
5179
|
// src/server/import/flatten.ts
|
|
4840
5180
|
function flattenObject(value, prefix, warnings) {
|
|
@@ -4875,7 +5215,7 @@ var vueI18nJson2 = {
|
|
|
4875
5215
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4876
5216
|
let data;
|
|
4877
5217
|
try {
|
|
4878
|
-
data = JSON.parse(
|
|
5218
|
+
data = JSON.parse(readFileSync13(join8(localeRoot, file), "utf8"));
|
|
4879
5219
|
} catch (e) {
|
|
4880
5220
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4881
5221
|
continue;
|
|
@@ -4890,8 +5230,8 @@ var vueI18nJson2 = {
|
|
|
4890
5230
|
};
|
|
4891
5231
|
|
|
4892
5232
|
// src/server/import/parsers/next-intl-json.ts
|
|
4893
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
4894
|
-
import { join as
|
|
5233
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
|
|
5234
|
+
import { join as join9 } from "path";
|
|
4895
5235
|
var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4896
5236
|
var nextIntlJson2 = {
|
|
4897
5237
|
name: "next-intl-json",
|
|
@@ -4906,7 +5246,7 @@ var nextIntlJson2 = {
|
|
|
4906
5246
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4907
5247
|
let data;
|
|
4908
5248
|
try {
|
|
4909
|
-
data = JSON.parse(
|
|
5249
|
+
data = JSON.parse(readFileSync14(join9(localeRoot, file), "utf8"));
|
|
4910
5250
|
} catch (e) {
|
|
4911
5251
|
warnings.push(`next-intl-json: failed to parse ${file}: ${e.message}`);
|
|
4912
5252
|
continue;
|
|
@@ -4922,7 +5262,7 @@ var nextIntlJson2 = {
|
|
|
4922
5262
|
|
|
4923
5263
|
// src/server/import/parsers/laravel-php.ts
|
|
4924
5264
|
import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
4925
|
-
import { join as
|
|
5265
|
+
import { join as join10, relative as relative2 } from "path";
|
|
4926
5266
|
import { execFileSync } from "child_process";
|
|
4927
5267
|
|
|
4928
5268
|
// src/server/import/placeholders.ts
|
|
@@ -4938,13 +5278,13 @@ function railsToCanonical(value) {
|
|
|
4938
5278
|
|
|
4939
5279
|
// src/server/import/parsers/laravel-php.ts
|
|
4940
5280
|
function listDirs2(dir) {
|
|
4941
|
-
return readdirSync6(dir).filter((e) => statSync4(
|
|
5281
|
+
return readdirSync6(dir).filter((e) => statSync4(join10(dir, e)).isDirectory());
|
|
4942
5282
|
}
|
|
4943
5283
|
function listPhpFiles(dir) {
|
|
4944
5284
|
const out = [];
|
|
4945
5285
|
const walk = (d) => {
|
|
4946
5286
|
for (const e of readdirSync6(d)) {
|
|
4947
|
-
const full =
|
|
5287
|
+
const full = join10(d, e);
|
|
4948
5288
|
if (statSync4(full).isDirectory()) walk(full);
|
|
4949
5289
|
else if (e.endsWith(".php")) out.push(full);
|
|
4950
5290
|
}
|
|
@@ -4981,7 +5321,7 @@ var laravelPhp2 = {
|
|
|
4981
5321
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
4982
5322
|
if (locale === "vendor") continue;
|
|
4983
5323
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4984
|
-
const localeDir =
|
|
5324
|
+
const localeDir = join10(localeRoot, locale);
|
|
4985
5325
|
locales.push(locale);
|
|
4986
5326
|
for (const file of listPhpFiles(localeDir)) {
|
|
4987
5327
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -5004,8 +5344,8 @@ var laravelPhp2 = {
|
|
|
5004
5344
|
};
|
|
5005
5345
|
|
|
5006
5346
|
// src/server/import/parsers/flutter-arb.ts
|
|
5007
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
5008
|
-
import { join as
|
|
5347
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync15 } from "fs";
|
|
5348
|
+
import { join as join11 } from "path";
|
|
5009
5349
|
var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5010
5350
|
function localeFromArbName(file) {
|
|
5011
5351
|
const m = file.match(/^(.+)\.arb$/);
|
|
@@ -5041,7 +5381,7 @@ var flutterArb2 = {
|
|
|
5041
5381
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5042
5382
|
let data;
|
|
5043
5383
|
try {
|
|
5044
|
-
data = JSON.parse(
|
|
5384
|
+
data = JSON.parse(readFileSync15(join11(localeRoot, file), "utf8"));
|
|
5045
5385
|
} catch (e) {
|
|
5046
5386
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
5047
5387
|
continue;
|
|
@@ -5066,8 +5406,8 @@ var flutterArb2 = {
|
|
|
5066
5406
|
};
|
|
5067
5407
|
|
|
5068
5408
|
// src/server/import/parsers/apple-strings.ts
|
|
5069
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
5070
|
-
import { join as
|
|
5409
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync16, statSync as statSync5 } from "fs";
|
|
5410
|
+
import { join as join12 } from "path";
|
|
5071
5411
|
var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5072
5412
|
var TABLE = "Localizable.strings";
|
|
5073
5413
|
function localeFromLproj(dir) {
|
|
@@ -5183,16 +5523,16 @@ var appleStrings2 = {
|
|
|
5183
5523
|
const locale = localeFromLproj(dir);
|
|
5184
5524
|
if (!locale) continue;
|
|
5185
5525
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5186
|
-
const file =
|
|
5526
|
+
const file = join12(localeRoot, dir, TABLE);
|
|
5187
5527
|
let text;
|
|
5188
5528
|
try {
|
|
5189
5529
|
if (!statSync5(file).isFile()) continue;
|
|
5190
|
-
text =
|
|
5530
|
+
text = readFileSync16(file, "utf8");
|
|
5191
5531
|
} catch {
|
|
5192
5532
|
continue;
|
|
5193
5533
|
}
|
|
5194
5534
|
locales.push(locale);
|
|
5195
|
-
const others = readdirSync8(
|
|
5535
|
+
const others = readdirSync8(join12(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
5196
5536
|
if (others.length) {
|
|
5197
5537
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
5198
5538
|
}
|
|
@@ -5205,8 +5545,8 @@ var appleStrings2 = {
|
|
|
5205
5545
|
};
|
|
5206
5546
|
|
|
5207
5547
|
// src/server/import/parsers/angular-xliff.ts
|
|
5208
|
-
import { readdirSync as readdirSync9, readFileSync as
|
|
5209
|
-
import { join as
|
|
5548
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync17 } from "fs";
|
|
5549
|
+
import { join as join13 } from "path";
|
|
5210
5550
|
var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5211
5551
|
var FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
|
|
5212
5552
|
function decodeEntities(s) {
|
|
@@ -5273,7 +5613,7 @@ var angularXliff2 = {
|
|
|
5273
5613
|
if (fnameLocale !== void 0 && !LOCALE_RE6.test(fnameLocale)) continue;
|
|
5274
5614
|
let xml;
|
|
5275
5615
|
try {
|
|
5276
|
-
xml =
|
|
5616
|
+
xml = readFileSync17(join13(localeRoot, file), "utf8");
|
|
5277
5617
|
} catch (e) {
|
|
5278
5618
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
5279
5619
|
continue;
|
|
@@ -5318,8 +5658,8 @@ var angularXliff2 = {
|
|
|
5318
5658
|
};
|
|
5319
5659
|
|
|
5320
5660
|
// src/server/import/parsers/gettext-po.ts
|
|
5321
|
-
import { readdirSync as readdirSync10, readFileSync as
|
|
5322
|
-
import { join as
|
|
5661
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync18 } from "fs";
|
|
5662
|
+
import { join as join14 } from "path";
|
|
5323
5663
|
var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5324
5664
|
var DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
|
|
5325
5665
|
var CONT_RE = /^[ \t]*"(.*)"\s*$/;
|
|
@@ -5411,17 +5751,17 @@ function discoverPoFiles(root) {
|
|
|
5411
5751
|
for (const e of entries) {
|
|
5412
5752
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
5413
5753
|
const base = e.name.slice(0, -3);
|
|
5414
|
-
found.push({ path:
|
|
5754
|
+
found.push({ path: join14(root, e.name), rel: e.name, locale: LOCALE_RE7.test(base) ? base : null });
|
|
5415
5755
|
} else if (e.isDirectory() && LOCALE_RE7.test(e.name)) {
|
|
5416
|
-
for (const sub of [
|
|
5756
|
+
for (const sub of [join14(e.name, "LC_MESSAGES"), e.name]) {
|
|
5417
5757
|
let names;
|
|
5418
5758
|
try {
|
|
5419
|
-
names = readdirSync10(
|
|
5759
|
+
names = readdirSync10(join14(root, sub)).sort();
|
|
5420
5760
|
} catch {
|
|
5421
5761
|
continue;
|
|
5422
5762
|
}
|
|
5423
5763
|
for (const f of names) {
|
|
5424
|
-
if (f.endsWith(".po")) found.push({ path:
|
|
5764
|
+
if (f.endsWith(".po")) found.push({ path: join14(root, sub, f), rel: join14(sub, f), locale: e.name });
|
|
5425
5765
|
}
|
|
5426
5766
|
}
|
|
5427
5767
|
}
|
|
@@ -5437,7 +5777,7 @@ var gettextPo2 = {
|
|
|
5437
5777
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
5438
5778
|
let entries;
|
|
5439
5779
|
try {
|
|
5440
|
-
entries = parseEntries(
|
|
5780
|
+
entries = parseEntries(readFileSync18(file.path, "utf8"));
|
|
5441
5781
|
} catch (e) {
|
|
5442
5782
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
5443
5783
|
continue;
|
|
@@ -5482,8 +5822,8 @@ var gettextPo2 = {
|
|
|
5482
5822
|
};
|
|
5483
5823
|
|
|
5484
5824
|
// src/server/import/parsers/i18next-json.ts
|
|
5485
|
-
import { readdirSync as readdirSync11, readFileSync as
|
|
5486
|
-
import { join as
|
|
5825
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync19, statSync as statSync6 } from "fs";
|
|
5826
|
+
import { join as join15 } from "path";
|
|
5487
5827
|
var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5488
5828
|
var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
5489
5829
|
var PLURAL_ARG = "count";
|
|
@@ -5502,7 +5842,7 @@ function fromI18next(value) {
|
|
|
5502
5842
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
5503
5843
|
let data;
|
|
5504
5844
|
try {
|
|
5505
|
-
data = JSON.parse(
|
|
5845
|
+
data = JSON.parse(readFileSync19(path, "utf8"));
|
|
5506
5846
|
} catch (e) {
|
|
5507
5847
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
5508
5848
|
return false;
|
|
@@ -5544,7 +5884,7 @@ var i18nextJson2 = {
|
|
|
5544
5884
|
const keys = {};
|
|
5545
5885
|
const locales = [];
|
|
5546
5886
|
for (const entry of readdirSync11(localeRoot).sort()) {
|
|
5547
|
-
const full =
|
|
5887
|
+
const full = join15(localeRoot, entry);
|
|
5548
5888
|
if (safeIsDir2(full)) {
|
|
5549
5889
|
if (!LOCALE_RE8.test(entry)) continue;
|
|
5550
5890
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
@@ -5553,7 +5893,7 @@ var i18nextJson2 = {
|
|
|
5553
5893
|
if (!file.endsWith(".json")) continue;
|
|
5554
5894
|
const ns = file.slice(0, -".json".length);
|
|
5555
5895
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
5556
|
-
if (ingestFile(
|
|
5896
|
+
if (ingestFile(join15(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
5557
5897
|
}
|
|
5558
5898
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
5559
5899
|
} else if (entry.endsWith(".json")) {
|
|
@@ -5570,8 +5910,8 @@ var i18nextJson2 = {
|
|
|
5570
5910
|
};
|
|
5571
5911
|
|
|
5572
5912
|
// src/server/import/parsers/rails-yaml.ts
|
|
5573
|
-
import { readdirSync as readdirSync12, readFileSync as
|
|
5574
|
-
import { join as
|
|
5913
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync20 } from "fs";
|
|
5914
|
+
import { join as join16 } from "path";
|
|
5575
5915
|
var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
|
|
5576
5916
|
var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
5577
5917
|
function makeNode() {
|
|
@@ -5789,7 +6129,7 @@ var railsYaml2 = {
|
|
|
5789
6129
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5790
6130
|
let text;
|
|
5791
6131
|
try {
|
|
5792
|
-
text =
|
|
6132
|
+
text = readFileSync20(join16(localeRoot, file), "utf8");
|
|
5793
6133
|
} catch (e) {
|
|
5794
6134
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5795
6135
|
continue;
|
|
@@ -5810,8 +6150,8 @@ var railsYaml2 = {
|
|
|
5810
6150
|
};
|
|
5811
6151
|
|
|
5812
6152
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5813
|
-
import { readdirSync as readdirSync13, readFileSync as
|
|
5814
|
-
import { join as
|
|
6153
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync21, statSync as statSync7 } from "fs";
|
|
6154
|
+
import { join as join17 } from "path";
|
|
5815
6155
|
var LOCALE_RE10 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5816
6156
|
var TABLE2 = "Localizable.stringsdict";
|
|
5817
6157
|
function localeFromLproj2(dir) {
|
|
@@ -5965,16 +6305,16 @@ var appleStringsdict2 = {
|
|
|
5965
6305
|
const locale = localeFromLproj2(dir);
|
|
5966
6306
|
if (!locale) continue;
|
|
5967
6307
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5968
|
-
const file =
|
|
6308
|
+
const file = join17(localeRoot, dir, TABLE2);
|
|
5969
6309
|
let text;
|
|
5970
6310
|
try {
|
|
5971
6311
|
if (!statSync7(file).isFile()) continue;
|
|
5972
|
-
text =
|
|
6312
|
+
text = readFileSync21(file, "utf8");
|
|
5973
6313
|
} catch {
|
|
5974
6314
|
continue;
|
|
5975
6315
|
}
|
|
5976
6316
|
locales.push(locale);
|
|
5977
|
-
const others = readdirSync13(
|
|
6317
|
+
const others = readdirSync13(join17(localeRoot, dir)).filter(
|
|
5978
6318
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5979
6319
|
);
|
|
5980
6320
|
if (others.length) {
|
|
@@ -6098,6 +6438,7 @@ function assemble2(parsed, opts) {
|
|
|
6098
6438
|
spelling: { customWords: [] }
|
|
6099
6439
|
},
|
|
6100
6440
|
glossary: [],
|
|
6441
|
+
glossarySuggestions: [],
|
|
6101
6442
|
keys,
|
|
6102
6443
|
warnings
|
|
6103
6444
|
};
|
|
@@ -6294,7 +6635,7 @@ function refreshLocationUsage(projectRoot, format) {
|
|
|
6294
6635
|
}
|
|
6295
6636
|
|
|
6296
6637
|
// src/server/export-run.ts
|
|
6297
|
-
import { existsSync as existsSync12, readFileSync as
|
|
6638
|
+
import { existsSync as existsSync12, readFileSync as readFileSync22, readdirSync as readdirSync14, rmdirSync, statSync as statSync8, unlinkSync } from "fs";
|
|
6298
6639
|
import { dirname as dirname2, resolve as resolve7, sep } from "path";
|
|
6299
6640
|
function effectiveLocales(config) {
|
|
6300
6641
|
const limit = config.exportLocales;
|
|
@@ -6307,11 +6648,11 @@ function narrowForExport(state) {
|
|
|
6307
6648
|
return { ...state, config: { ...state.config, locales } };
|
|
6308
6649
|
}
|
|
6309
6650
|
var LOCALE_TOKEN = /^[a-z]{2,3}([_-][a-z0-9]+)*$/i;
|
|
6310
|
-
function
|
|
6651
|
+
function escapeRegExp2(s) {
|
|
6311
6652
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6312
6653
|
}
|
|
6313
6654
|
function segmentRegExp(segment) {
|
|
6314
|
-
const pattern =
|
|
6655
|
+
const pattern = escapeRegExp2(segment).replaceAll("\\{locale\\}", "(?<locale>[A-Za-z0-9_-]+)").replaceAll("\\{namespace\\}", "[^/]*");
|
|
6315
6656
|
return new RegExp(`^${pattern}$`);
|
|
6316
6657
|
}
|
|
6317
6658
|
function removeEmptyDirs(dir, stopAt) {
|
|
@@ -6393,7 +6734,7 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
6393
6734
|
writtenPaths.add(abs);
|
|
6394
6735
|
let current = null;
|
|
6395
6736
|
try {
|
|
6396
|
-
current =
|
|
6737
|
+
current = readFileSync22(abs, "utf8");
|
|
6397
6738
|
} catch {
|
|
6398
6739
|
}
|
|
6399
6740
|
if (current === f.contents) {
|
|
@@ -6410,17 +6751,17 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
6410
6751
|
}
|
|
6411
6752
|
|
|
6412
6753
|
// src/server/ui-prefs.ts
|
|
6413
|
-
import { readFileSync as
|
|
6414
|
-
import { homedir } from "os";
|
|
6415
|
-
import { join as
|
|
6754
|
+
import { readFileSync as readFileSync23 } from "fs";
|
|
6755
|
+
import { homedir as homedir2 } from "os";
|
|
6756
|
+
import { join as join18 } from "path";
|
|
6416
6757
|
var THEMES = ["system", "light", "dark"];
|
|
6417
6758
|
var isThemeMode = (v) => THEMES.includes(v);
|
|
6418
6759
|
var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
6419
|
-
var defaultUiPrefsPath = () =>
|
|
6760
|
+
var defaultUiPrefsPath = () => join18(homedir2(), ".glotfile", "ui.json");
|
|
6420
6761
|
var DEFAULTS = { theme: "system" };
|
|
6421
6762
|
function readJson(path) {
|
|
6422
6763
|
try {
|
|
6423
|
-
const parsed = JSON.parse(
|
|
6764
|
+
const parsed = JSON.parse(readFileSync23(path, "utf8"));
|
|
6424
6765
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
6425
6766
|
} catch {
|
|
6426
6767
|
return {};
|
|
@@ -6439,7 +6780,7 @@ function saveUiPrefs(path, prefs) {
|
|
|
6439
6780
|
}
|
|
6440
6781
|
|
|
6441
6782
|
// src/server/local-settings.ts
|
|
6442
|
-
import { readFileSync as
|
|
6783
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
6443
6784
|
import { resolve as resolve8 } from "path";
|
|
6444
6785
|
var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
|
|
6445
6786
|
var isEditorId = (v) => EDITOR_IDS.includes(v);
|
|
@@ -6454,7 +6795,7 @@ var DEFAULT_EDITOR = "vscode";
|
|
|
6454
6795
|
var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
|
|
6455
6796
|
function readJson2(path) {
|
|
6456
6797
|
try {
|
|
6457
|
-
const parsed = JSON.parse(
|
|
6798
|
+
const parsed = JSON.parse(readFileSync24(path, "utf8"));
|
|
6458
6799
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
6459
6800
|
} catch {
|
|
6460
6801
|
return {};
|
|
@@ -6544,7 +6885,7 @@ function createEventHub() {
|
|
|
6544
6885
|
|
|
6545
6886
|
// src/server/watch.ts
|
|
6546
6887
|
import { statSync as statSync9, readdirSync as readdirSync15 } from "fs";
|
|
6547
|
-
import { join as
|
|
6888
|
+
import { join as join19 } from "path";
|
|
6548
6889
|
import { createHash as createHash2 } from "crypto";
|
|
6549
6890
|
function hashState(state) {
|
|
6550
6891
|
return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
|
|
@@ -6560,15 +6901,15 @@ function signature(statePath) {
|
|
|
6560
6901
|
const parts = [];
|
|
6561
6902
|
for (const rel of ["config.json", "keys.json"]) {
|
|
6562
6903
|
try {
|
|
6563
|
-
const s = statSync9(
|
|
6904
|
+
const s = statSync9(join19(dir, rel));
|
|
6564
6905
|
parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
|
|
6565
6906
|
} catch {
|
|
6566
6907
|
}
|
|
6567
6908
|
}
|
|
6568
6909
|
try {
|
|
6569
|
-
for (const name of readdirSync15(
|
|
6910
|
+
for (const name of readdirSync15(join19(dir, "locales")).sort()) {
|
|
6570
6911
|
if (!name.endsWith(".json")) continue;
|
|
6571
|
-
const s = statSync9(
|
|
6912
|
+
const s = statSync9(join19(dir, "locales", name));
|
|
6572
6913
|
parts.push(`${name}:${s.size}:${s.mtimeMs}`);
|
|
6573
6914
|
}
|
|
6574
6915
|
} catch {
|
|
@@ -6643,28 +6984,13 @@ function projectName(root) {
|
|
|
6643
6984
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
6644
6985
|
if (existsSync13(nameFile)) {
|
|
6645
6986
|
try {
|
|
6646
|
-
const name =
|
|
6987
|
+
const name = readFileSync25(nameFile, "utf8").trim();
|
|
6647
6988
|
if (name) return name;
|
|
6648
6989
|
} catch {
|
|
6649
6990
|
}
|
|
6650
6991
|
}
|
|
6651
6992
|
return basename(root);
|
|
6652
6993
|
}
|
|
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
6994
|
function createApi(deps) {
|
|
6669
6995
|
const app = new Hono();
|
|
6670
6996
|
const load = () => loadState(deps.statePath);
|
|
@@ -6796,6 +7122,61 @@ function createApi(deps) {
|
|
|
6796
7122
|
}
|
|
6797
7123
|
return c.json({ ok: true });
|
|
6798
7124
|
});
|
|
7125
|
+
app.post("/ai-test", async (c) => {
|
|
7126
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7127
|
+
const meta = { provider: aiCfg.provider, model: aiCfg.model };
|
|
7128
|
+
let provider;
|
|
7129
|
+
try {
|
|
7130
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7131
|
+
} catch (e) {
|
|
7132
|
+
return c.json({ ok: false, ...meta, error: explainProviderError(aiCfg.provider, e) });
|
|
7133
|
+
}
|
|
7134
|
+
const controller = new AbortController();
|
|
7135
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
7136
|
+
try {
|
|
7137
|
+
const probe = {
|
|
7138
|
+
id: "probe",
|
|
7139
|
+
key: "glotfile.connection-test",
|
|
7140
|
+
source: "Hello",
|
|
7141
|
+
sourceLocale: "en",
|
|
7142
|
+
targetLocale: "es",
|
|
7143
|
+
placeholders: []
|
|
7144
|
+
};
|
|
7145
|
+
await provider.translate([probe], void 0, controller.signal);
|
|
7146
|
+
return c.json({ ok: true, ...meta });
|
|
7147
|
+
} catch (e) {
|
|
7148
|
+
const error = controller.signal.aborted ? "Connection test timed out after 30s \u2014 the provider didn't respond." : explainProviderError(aiCfg.provider, e);
|
|
7149
|
+
return c.json({ ok: false, ...meta, error });
|
|
7150
|
+
} finally {
|
|
7151
|
+
clearTimeout(timer);
|
|
7152
|
+
}
|
|
7153
|
+
});
|
|
7154
|
+
app.get("/prices", (c) => {
|
|
7155
|
+
const cache2 = loadPriceCache();
|
|
7156
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
7157
|
+
const pricing = resolvePricing(ai, cache2);
|
|
7158
|
+
return c.json({
|
|
7159
|
+
source: cache2?.source ?? null,
|
|
7160
|
+
fetchedAt: cache2?.fetchedAt ?? null,
|
|
7161
|
+
modelCount: cache2 ? Object.keys(cache2.models).length : 0,
|
|
7162
|
+
path: defaultPriceCachePath(),
|
|
7163
|
+
resolved: pricing ? { provider: ai.provider, model: ai.model, ...pricing } : null
|
|
7164
|
+
});
|
|
7165
|
+
});
|
|
7166
|
+
app.get("/prices/list", (c) => {
|
|
7167
|
+
const cache2 = loadPriceCache();
|
|
7168
|
+
const models = cache2 ? Object.entries(cache2.models).map(([id, p]) => ({ id, ...p })).sort((a, b) => a.id.localeCompare(b.id)) : [];
|
|
7169
|
+
return c.json({ source: cache2?.source ?? null, fetchedAt: cache2?.fetchedAt ?? null, models });
|
|
7170
|
+
});
|
|
7171
|
+
app.post("/prices/refresh", async (c) => {
|
|
7172
|
+
try {
|
|
7173
|
+
const res = await refreshPrices();
|
|
7174
|
+
invalidatePriceCache();
|
|
7175
|
+
return c.json({ ok: true, ...res });
|
|
7176
|
+
} catch (e) {
|
|
7177
|
+
return c.json({ error: e.message }, 502);
|
|
7178
|
+
}
|
|
7179
|
+
});
|
|
6799
7180
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
6800
7181
|
app.get("/files", (c) => {
|
|
6801
7182
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -7160,6 +7541,90 @@ function createApi(deps) {
|
|
|
7160
7541
|
logChange({ kind: "glossary", summary: `Deleted glossary term "${term}"`, before });
|
|
7161
7542
|
return c.json({ ok: true });
|
|
7162
7543
|
});
|
|
7544
|
+
app.get("/glossary/suggestions", (c) => {
|
|
7545
|
+
const s = load();
|
|
7546
|
+
const pending = s.glossarySuggestions.filter((x) => x.status === "pending");
|
|
7547
|
+
return c.json(pending.map((x) => ({
|
|
7548
|
+
...x,
|
|
7549
|
+
occurrences: sourceKeysForTerm(s, x.term, { caseSensitive: x.caseSensitive, wholeWord: x.wholeWord }).length
|
|
7550
|
+
})));
|
|
7551
|
+
});
|
|
7552
|
+
app.post("/glossary/suggestions/dismiss", async (c) => {
|
|
7553
|
+
const { term } = await c.req.json();
|
|
7554
|
+
if (typeof term !== "string") return c.json({ error: "term must be a string" }, 400);
|
|
7555
|
+
const s = load();
|
|
7556
|
+
dismissGlossarySuggestion(s, term);
|
|
7557
|
+
persist(s);
|
|
7558
|
+
logChange({ kind: "glossary", summary: `Dismissed suggested term "${term}"` });
|
|
7559
|
+
return c.json({ ok: true });
|
|
7560
|
+
});
|
|
7561
|
+
app.delete("/glossary/suggestions/:term", (c) => {
|
|
7562
|
+
const s = load();
|
|
7563
|
+
const term = decodeURIComponent(c.req.param("term"));
|
|
7564
|
+
removeGlossarySuggestion(s, term);
|
|
7565
|
+
persist(s);
|
|
7566
|
+
return c.json({ ok: true });
|
|
7567
|
+
});
|
|
7568
|
+
app.post("/glossary/suggest", async (c) => {
|
|
7569
|
+
const signal = c.req.raw.signal;
|
|
7570
|
+
const body = await c.req.json().catch(() => ({}));
|
|
7571
|
+
return streamSSE(c, async (stream) => {
|
|
7572
|
+
const s0 = load();
|
|
7573
|
+
const sources = selectGlossarySources(s0, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
|
|
7574
|
+
if (!sources.length) {
|
|
7575
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ added: 0, terms: [] }) });
|
|
7576
|
+
return;
|
|
7577
|
+
}
|
|
7578
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7579
|
+
let provider;
|
|
7580
|
+
try {
|
|
7581
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7582
|
+
} catch (e) {
|
|
7583
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
7584
|
+
return;
|
|
7585
|
+
}
|
|
7586
|
+
const known = knownTermList(s0);
|
|
7587
|
+
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: sources.length }) });
|
|
7588
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
7589
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
7590
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
7591
|
+
const chunks = [];
|
|
7592
|
+
for (let i = 0; i < sources.length; i += batchSize) chunks.push(sources.slice(i, i + batchSize));
|
|
7593
|
+
const all = [];
|
|
7594
|
+
let done = 0;
|
|
7595
|
+
let next = 0;
|
|
7596
|
+
async function worker() {
|
|
7597
|
+
while (next < chunks.length) {
|
|
7598
|
+
if (signal?.aborted) break;
|
|
7599
|
+
const chunkRows = chunks[next++];
|
|
7600
|
+
try {
|
|
7601
|
+
const raw = await provider.complete({ system, content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunkRows, known) }], schema: GLOSSARY_SUGGEST_SCHEMA });
|
|
7602
|
+
all.push(...raw.terms ?? []);
|
|
7603
|
+
} catch (e) {
|
|
7604
|
+
void stream.writeSSE({ event: "warn", data: JSON.stringify({ error: e.message }) });
|
|
7605
|
+
}
|
|
7606
|
+
done += chunkRows.length;
|
|
7607
|
+
void stream.writeSSE({ event: "progress", data: JSON.stringify({ done, total: sources.length }) });
|
|
7608
|
+
}
|
|
7609
|
+
}
|
|
7610
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
7611
|
+
if (signal?.aborted) return;
|
|
7612
|
+
const fresh = load();
|
|
7613
|
+
const added = mergeGlossarySuggestions(fresh, dedupeTerms(all));
|
|
7614
|
+
const usage = provider.takeUsage?.();
|
|
7615
|
+
persist(fresh);
|
|
7616
|
+
appendLog(projectRoot, {
|
|
7617
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7618
|
+
kind: "glossary",
|
|
7619
|
+
summary: `Suggested ${added.length} glossary term(s)`,
|
|
7620
|
+
model: aiCfg.model,
|
|
7621
|
+
system,
|
|
7622
|
+
usage,
|
|
7623
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
7624
|
+
});
|
|
7625
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ added: added.length, terms: added }) });
|
|
7626
|
+
});
|
|
7627
|
+
});
|
|
7163
7628
|
app.post("/keys/:key/screenshot", async (c) => {
|
|
7164
7629
|
const key = c.req.param("key");
|
|
7165
7630
|
const body = await c.req.parseBody();
|
|
@@ -7345,7 +7810,7 @@ function createApi(deps) {
|
|
|
7345
7810
|
try {
|
|
7346
7811
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7347
7812
|
} catch (e) {
|
|
7348
|
-
await stream.writeSSE({ event: "error", data: JSON.stringify({ error:
|
|
7813
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: explainProviderError(aiCfg.provider, e) }) });
|
|
7349
7814
|
return;
|
|
7350
7815
|
}
|
|
7351
7816
|
const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
@@ -7362,58 +7827,65 @@ function createApi(deps) {
|
|
|
7362
7827
|
event: "start",
|
|
7363
7828
|
data: JSON.stringify({ total: reqs.length, locales: [...localeTotals].map(([locale, total]) => ({ locale, total })) })
|
|
7364
7829
|
});
|
|
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
|
-
|
|
7830
|
+
try {
|
|
7831
|
+
await runLocaleParallel(reqs, provider, {
|
|
7832
|
+
// Announce a language the moment a worker picks it up — this is the
|
|
7833
|
+
// signal that "something is happening" during the long first LLM call.
|
|
7834
|
+
onLocaleStart: (locale) => {
|
|
7835
|
+
void stream.writeSSE({ event: "locale-start", data: JSON.stringify({ locale }) });
|
|
7836
|
+
},
|
|
7837
|
+
onBatchComplete: (done, total, batchResults, locale) => {
|
|
7838
|
+
const fresh = load();
|
|
7839
|
+
const { written, errors } = applyResults(fresh, reqs, batchResults);
|
|
7840
|
+
persist(fresh);
|
|
7841
|
+
totalWritten += written;
|
|
7842
|
+
allErrors.push(...errors);
|
|
7843
|
+
const usage = provider.takeUsage?.();
|
|
7844
|
+
appendLog(projectRoot, {
|
|
7845
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7846
|
+
kind: "translate",
|
|
7847
|
+
summary: `Translated ${batchResults.length} item(s)`,
|
|
7848
|
+
model: aiCfg.model,
|
|
7849
|
+
system,
|
|
7850
|
+
items: batchResults.map((r) => {
|
|
7851
|
+
const req = reqById.get(r.id);
|
|
7852
|
+
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 };
|
|
7853
|
+
}),
|
|
7854
|
+
results: batchResults,
|
|
7855
|
+
usage,
|
|
7856
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
7857
|
+
});
|
|
7858
|
+
const ld = (localeDone.get(locale) ?? 0) + batchResults.length;
|
|
7859
|
+
localeDone.set(locale, ld);
|
|
7860
|
+
console.log(`[translate] ${done}/${total}`);
|
|
7861
|
+
void stream.writeSSE({
|
|
7862
|
+
event: "progress",
|
|
7863
|
+
data: JSON.stringify({ done, total, written: totalWritten, errors, locale, localeDone: ld, localeTotal: localeTotals.get(locale) ?? 0 })
|
|
7864
|
+
});
|
|
7865
|
+
},
|
|
7866
|
+
onLocaleDone: (locale) => {
|
|
7867
|
+
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
7868
|
+
},
|
|
7869
|
+
// Record the raw reply so an unparseable model response is diagnosable
|
|
7870
|
+
// from the activity log instead of vanishing into per-item errors.
|
|
7871
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
7872
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
7873
|
+
appendLog(projectRoot, {
|
|
7874
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7875
|
+
kind: "translate",
|
|
7876
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
7877
|
+
model: aiCfg.model,
|
|
7878
|
+
locale,
|
|
7879
|
+
raw
|
|
7880
|
+
});
|
|
7881
|
+
}
|
|
7882
|
+
}, aiCfg.concurrency, signal, aiCfg.batchSize);
|
|
7883
|
+
} catch (e) {
|
|
7884
|
+
if (!signal?.aborted) {
|
|
7885
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: explainProviderError(aiCfg.provider, e) }) });
|
|
7415
7886
|
}
|
|
7416
|
-
|
|
7887
|
+
return;
|
|
7888
|
+
}
|
|
7417
7889
|
if (!signal?.aborted) {
|
|
7418
7890
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
7419
7891
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -7440,23 +7912,28 @@ function createApi(deps) {
|
|
|
7440
7912
|
try {
|
|
7441
7913
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7442
7914
|
} catch (e) {
|
|
7443
|
-
return c.json({ error:
|
|
7915
|
+
return c.json({ error: explainProviderError(aiCfg.provider, e) }, 400);
|
|
7444
7916
|
}
|
|
7445
7917
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
7446
7918
|
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
|
-
|
|
7919
|
+
let results;
|
|
7920
|
+
try {
|
|
7921
|
+
results = await runLocaleParallel(toTranslate, provider, {
|
|
7922
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
7923
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
7924
|
+
appendLog(projectRoot, {
|
|
7925
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7926
|
+
kind: "translate",
|
|
7927
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
7928
|
+
model: aiCfg.model,
|
|
7929
|
+
locale,
|
|
7930
|
+
raw
|
|
7931
|
+
});
|
|
7932
|
+
}
|
|
7933
|
+
}, aiCfg.concurrency, void 0, aiCfg.batchSize);
|
|
7934
|
+
} catch (e) {
|
|
7935
|
+
return c.json({ error: explainProviderError(aiCfg.provider, e) }, 502);
|
|
7936
|
+
}
|
|
7460
7937
|
const latest = load();
|
|
7461
7938
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
7462
7939
|
const usage = provider.takeUsage?.();
|
|
@@ -7727,6 +8204,22 @@ function createApi(deps) {
|
|
|
7727
8204
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
7728
8205
|
});
|
|
7729
8206
|
});
|
|
8207
|
+
app.post("/context/estimate", async (c) => {
|
|
8208
|
+
const body = await c.req.json().catch(() => ({}));
|
|
8209
|
+
const cache2 = loadUsageCache(projectRoot);
|
|
8210
|
+
if (!cache2) return c.json({ error: "No usage index found. Run 'glotfile scan' first." }, 400);
|
|
8211
|
+
const targets = selectContextTargets(load(), {
|
|
8212
|
+
all: body.all,
|
|
8213
|
+
keyGlob: body.keyGlob,
|
|
8214
|
+
limit: body.limit,
|
|
8215
|
+
since: body.since,
|
|
8216
|
+
keys: body.keys,
|
|
8217
|
+
force: body.force
|
|
8218
|
+
}, cache2, body.lastRunAt);
|
|
8219
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8220
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
8221
|
+
return c.json(estimateContext(targets, aiCfg));
|
|
8222
|
+
});
|
|
7730
8223
|
app.get("/context/batch/status", async (c) => {
|
|
7731
8224
|
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7732
8225
|
let supported = false;
|
|
@@ -7829,7 +8322,7 @@ function createApi(deps) {
|
|
|
7829
8322
|
|
|
7830
8323
|
// src/server/server.ts
|
|
7831
8324
|
var here = dirname4(fileURLToPath(import.meta.url));
|
|
7832
|
-
var DEFAULT_UI_DIR =
|
|
8325
|
+
var DEFAULT_UI_DIR = join20(here, "..", "ui");
|
|
7833
8326
|
var MIME = {
|
|
7834
8327
|
".html": "text/html; charset=utf-8",
|
|
7835
8328
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7896,7 +8389,7 @@ function buildApp(opts) {
|
|
|
7896
8389
|
const file = await readFileResponse(target);
|
|
7897
8390
|
if (file) return file;
|
|
7898
8391
|
}
|
|
7899
|
-
const index = await readFileResponse(
|
|
8392
|
+
const index = await readFileResponse(join20(root, "index.html"));
|
|
7900
8393
|
if (index) return index;
|
|
7901
8394
|
return c.notFound();
|
|
7902
8395
|
});
|