glotfile 1.0.0 → 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/README.md +1 -1
- package/dist/server/cli.js +1228 -307
- package/dist/server/server.js +997 -297
- package/dist/ui/assets/index-Bjwiz6KQ.css +1 -0
- package/dist/ui/assets/{index-B7j9yFsz.js → index-Dwn9g3g-.js} +113 -15
- 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) {
|
|
@@ -959,7 +1146,7 @@ var PREFIX_PATTERNS = {
|
|
|
959
1146
|
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
|
|
960
1147
|
]
|
|
961
1148
|
};
|
|
962
|
-
var CACHE_VERSION =
|
|
1149
|
+
var CACHE_VERSION = 8;
|
|
963
1150
|
var EXT_SCANNER = {
|
|
964
1151
|
".php": "laravel",
|
|
965
1152
|
".vue": "js-i18n",
|
|
@@ -1031,6 +1218,73 @@ function customPatterns(opts) {
|
|
|
1031
1218
|
}
|
|
1032
1219
|
return out;
|
|
1033
1220
|
}
|
|
1221
|
+
var NEXT_INTL_IMPORT = /(?:from\s*|require\(\s*)['"]next-intl(?:\/[\w-]+)?['"]/;
|
|
1222
|
+
function isNextIntlFile(content) {
|
|
1223
|
+
return NEXT_INTL_IMPORT.test(content);
|
|
1224
|
+
}
|
|
1225
|
+
var NI_BIND = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?(?:useTranslations|getTranslations)\s*\(\s*(?:['"]([^'"]*)['"])?\s*\)/g;
|
|
1226
|
+
var NI_BIND_OBJ = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?getTranslations\s*\(\s*\{[^}]*?\bnamespace\s*:\s*['"]([^'"]+)['"][^}]*?\}\s*\)/g;
|
|
1227
|
+
var NI_METHOD = "(?:\\.(?:rich|markup|raw|has))?";
|
|
1228
|
+
function nextIntlBindings(content) {
|
|
1229
|
+
const out = [];
|
|
1230
|
+
for (const m of content.matchAll(NI_BIND)) out.push({ name: m[1], ns: m[2] ?? "", index: m.index });
|
|
1231
|
+
for (const m of content.matchAll(NI_BIND_OBJ)) out.push({ name: m[1], ns: m[2], index: m.index });
|
|
1232
|
+
return out;
|
|
1233
|
+
}
|
|
1234
|
+
function nsForBindingAt(bindings, name, index) {
|
|
1235
|
+
let best = null;
|
|
1236
|
+
for (const b of bindings) {
|
|
1237
|
+
if (b.name === name && b.index < index && (!best || b.index > best.index)) best = b;
|
|
1238
|
+
}
|
|
1239
|
+
return best ? best.ns : null;
|
|
1240
|
+
}
|
|
1241
|
+
function joinKey(ns, rel) {
|
|
1242
|
+
return ns ? `${ns}.${rel}` : rel;
|
|
1243
|
+
}
|
|
1244
|
+
function uniqueBindingNames(bindings) {
|
|
1245
|
+
return [...new Set(bindings.map((b) => b.name))];
|
|
1246
|
+
}
|
|
1247
|
+
function nextIntlRefMatches(content) {
|
|
1248
|
+
const bindings = nextIntlBindings(content);
|
|
1249
|
+
const out = [];
|
|
1250
|
+
for (const name of uniqueBindingNames(bindings)) {
|
|
1251
|
+
const re = new RegExp(
|
|
1252
|
+
`\\b${escapeRe2(name)}${NI_METHOD}\\s*\\(\\s*(?:'([^'\\n]+)'|"([^"\\n]+)"|\`([^\`$\\n]+)\`)`,
|
|
1253
|
+
"g"
|
|
1254
|
+
);
|
|
1255
|
+
let m;
|
|
1256
|
+
while ((m = re.exec(content)) !== null) {
|
|
1257
|
+
const ns = nsForBindingAt(bindings, name, m.index);
|
|
1258
|
+
if (ns === null) continue;
|
|
1259
|
+
out.push({ key: joinKey(ns, m[1] ?? m[2] ?? m[3]), index: m.index });
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return out;
|
|
1263
|
+
}
|
|
1264
|
+
function nextIntlPrefixMatches(content) {
|
|
1265
|
+
const bindings = nextIntlBindings(content);
|
|
1266
|
+
const out = [];
|
|
1267
|
+
for (const name of uniqueBindingNames(bindings)) {
|
|
1268
|
+
const ev = escapeRe2(name);
|
|
1269
|
+
const headConcat = new RegExp(`\\b${ev}${NI_METHOD}\\s*\\(\\s*(?:'([^'\\n]*)'|"([^"\\n]*)")\\s*\\+`, "g");
|
|
1270
|
+
const headTemplate = new RegExp(`\\b${ev}${NI_METHOD}\\s*\\(\\s*\`([^\`$\\n]*)\\$\\{`, "g");
|
|
1271
|
+
const dynamicArg = new RegExp(`\\b${ev}${NI_METHOD}\\s*\\(\\s*[A-Za-z_$][\\w$.]*\\s*[),]`, "g");
|
|
1272
|
+
let m;
|
|
1273
|
+
for (const re of [headConcat, headTemplate]) {
|
|
1274
|
+
while ((m = re.exec(content)) !== null) {
|
|
1275
|
+
const ns = nsForBindingAt(bindings, name, m.index);
|
|
1276
|
+
if (ns === null) continue;
|
|
1277
|
+
out.push({ prefix: joinKey(ns, m[1] ?? m[2] ?? ""), index: m.index });
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
while ((m = dynamicArg.exec(content)) !== null) {
|
|
1281
|
+
const ns = nsForBindingAt(bindings, name, m.index);
|
|
1282
|
+
if (!ns) continue;
|
|
1283
|
+
out.push({ prefix: `${ns}.`, index: m.index });
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return out;
|
|
1287
|
+
}
|
|
1034
1288
|
function lineStartOffsets(content) {
|
|
1035
1289
|
const starts = [0];
|
|
1036
1290
|
let idx = content.indexOf("\n");
|
|
@@ -1051,49 +1305,56 @@ function offsetToLineCol(starts, offset) {
|
|
|
1051
1305
|
return { line: lo + 1, col: offset - starts[lo] + 1 };
|
|
1052
1306
|
}
|
|
1053
1307
|
function extractRefs(content, scanner, opts) {
|
|
1054
|
-
const
|
|
1055
|
-
const
|
|
1056
|
-
if (patterns.length === 0) return [];
|
|
1308
|
+
const useNextIntl = scanner === "next-intl" || scanner === "js-i18n" && isNextIntlFile(content);
|
|
1309
|
+
const effScanner = useNextIntl ? "next-intl" : scanner;
|
|
1057
1310
|
const starts = lineStartOffsets(content);
|
|
1058
1311
|
const result = [];
|
|
1059
1312
|
const seen = /* @__PURE__ */ new Set();
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
const { line, col } = offsetToLineCol(starts, m.index);
|
|
1067
|
-
const dedup = `${line}:${col}:${key}`;
|
|
1068
|
-
if (!seen.has(dedup)) {
|
|
1069
|
-
seen.add(dedup);
|
|
1070
|
-
result.push({ key, line, col, scanner });
|
|
1071
|
-
}
|
|
1313
|
+
const push = (key, index) => {
|
|
1314
|
+
const { line, col } = offsetToLineCol(starts, index);
|
|
1315
|
+
const dedup = `${line}:${col}:${key}`;
|
|
1316
|
+
if (!seen.has(dedup)) {
|
|
1317
|
+
seen.add(dedup);
|
|
1318
|
+
result.push({ key, line, col, scanner: effScanner });
|
|
1072
1319
|
}
|
|
1320
|
+
};
|
|
1321
|
+
if (useNextIntl) {
|
|
1322
|
+
for (const r of nextIntlRefMatches(content)) push(r.key, r.index);
|
|
1323
|
+
} else {
|
|
1324
|
+
const base = scanner === "flutter" ? flutterPatterns(content, opts) : PATTERNS[scanner] ?? [];
|
|
1325
|
+
for (const pattern of base) eachMatch(content, pattern, push);
|
|
1073
1326
|
}
|
|
1327
|
+
for (const pattern of customPatterns(opts)) eachMatch(content, pattern, push);
|
|
1074
1328
|
result.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
1075
1329
|
return result;
|
|
1076
1330
|
}
|
|
1331
|
+
function eachMatch(content, pattern, fn) {
|
|
1332
|
+
const re = new RegExp(pattern.source, "g");
|
|
1333
|
+
let m;
|
|
1334
|
+
while ((m = re.exec(content)) !== null) {
|
|
1335
|
+
if (m.index === re.lastIndex) re.lastIndex++;
|
|
1336
|
+
fn(m[1], m.index);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1077
1339
|
function extractPrefixes(content, scanner) {
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1340
|
+
const useNextIntl = scanner === "next-intl" || scanner === "js-i18n" && isNextIntlFile(content);
|
|
1341
|
+
const effScanner = useNextIntl ? "next-intl" : scanner;
|
|
1080
1342
|
const starts = lineStartOffsets(content);
|
|
1081
1343
|
const result = [];
|
|
1082
1344
|
const seen = /* @__PURE__ */ new Set();
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
const { line, col } = offsetToLineCol(starts, m.index);
|
|
1091
|
-
const dedup = `${line}:${col}:${prefix}`;
|
|
1092
|
-
if (!seen.has(dedup)) {
|
|
1093
|
-
seen.add(dedup);
|
|
1094
|
-
result.push({ prefix, line, col, scanner });
|
|
1095
|
-
}
|
|
1345
|
+
const push = (prefix, index) => {
|
|
1346
|
+
if (!prefix) return;
|
|
1347
|
+
const { line, col } = offsetToLineCol(starts, index);
|
|
1348
|
+
const dedup = `${line}:${col}:${prefix}`;
|
|
1349
|
+
if (!seen.has(dedup)) {
|
|
1350
|
+
seen.add(dedup);
|
|
1351
|
+
result.push({ prefix, line, col, scanner: effScanner });
|
|
1096
1352
|
}
|
|
1353
|
+
};
|
|
1354
|
+
if (useNextIntl) {
|
|
1355
|
+
for (const p of nextIntlPrefixMatches(content)) push(p.prefix, p.index);
|
|
1356
|
+
} else {
|
|
1357
|
+
for (const pattern of PREFIX_PATTERNS[scanner] ?? []) eachMatch(content, pattern, push);
|
|
1097
1358
|
}
|
|
1098
1359
|
result.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
1099
1360
|
return result;
|
|
@@ -1329,7 +1590,7 @@ var MAX_CONTEXT_LENGTH = 500;
|
|
|
1329
1590
|
var SNIPPET_WINDOW = 15;
|
|
1330
1591
|
var MAX_SNIPPETS = 3;
|
|
1331
1592
|
var EXCLUDED_DIRS = ["node_modules/", "vendor/", "dist/", ".git/", ".glotfile/"];
|
|
1332
|
-
function
|
|
1593
|
+
function globToRegExp3(glob) {
|
|
1333
1594
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1334
1595
|
return new RegExp(`^${escaped}$`);
|
|
1335
1596
|
}
|
|
@@ -1359,6 +1620,21 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
1359
1620
|
}
|
|
1360
1621
|
return snippets;
|
|
1361
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
|
+
}
|
|
1362
1638
|
function buildUsageIndex(cache2) {
|
|
1363
1639
|
const index = /* @__PURE__ */ new Map();
|
|
1364
1640
|
for (const [file, entry] of Object.entries(cache2.files)) {
|
|
@@ -1372,7 +1648,7 @@ function buildUsageIndex(cache2) {
|
|
|
1372
1648
|
}
|
|
1373
1649
|
function selectContextTargets(state, opts, cache2, lastRunAt) {
|
|
1374
1650
|
const cutoff = opts.all ? void 0 : opts.since ?? lastRunAt;
|
|
1375
|
-
const keyRe = opts.keyGlob ?
|
|
1651
|
+
const keyRe = opts.keyGlob ? globToRegExp3(opts.keyGlob) : null;
|
|
1376
1652
|
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
1377
1653
|
const usageIndex = buildUsageIndex(cache2);
|
|
1378
1654
|
let candidates = [];
|
|
@@ -1594,41 +1870,6 @@ function computeStats(state) {
|
|
|
1594
1870
|
};
|
|
1595
1871
|
}
|
|
1596
1872
|
|
|
1597
|
-
// src/server/glossary.ts
|
|
1598
|
-
function contains(haystack, needle, caseSensitive) {
|
|
1599
|
-
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
1600
|
-
}
|
|
1601
|
-
function relevantGlossary(source, targetLocale, glossary) {
|
|
1602
|
-
const hints = [];
|
|
1603
|
-
for (const entry of glossary) {
|
|
1604
|
-
if (!contains(source, entry.term, entry.caseSensitive)) continue;
|
|
1605
|
-
hints.push({
|
|
1606
|
-
term: entry.term,
|
|
1607
|
-
doNotTranslate: entry.doNotTranslate,
|
|
1608
|
-
forced: entry.translations?.[targetLocale],
|
|
1609
|
-
notes: entry.notes
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
return hints;
|
|
1613
|
-
}
|
|
1614
|
-
function glossaryViolations(source, value, targetLocale, glossary) {
|
|
1615
|
-
const out = [];
|
|
1616
|
-
for (const entry of glossary) {
|
|
1617
|
-
if (!contains(source, entry.term, entry.caseSensitive)) continue;
|
|
1618
|
-
if (entry.doNotTranslate) {
|
|
1619
|
-
if (!contains(value, entry.term, entry.caseSensitive)) {
|
|
1620
|
-
out.push({ term: entry.term, expected: entry.term, kind: "do-not-translate" });
|
|
1621
|
-
}
|
|
1622
|
-
continue;
|
|
1623
|
-
}
|
|
1624
|
-
const forced = entry.translations?.[targetLocale];
|
|
1625
|
-
if (forced && !contains(value, forced, entry.caseSensitive)) {
|
|
1626
|
-
out.push({ term: entry.term, expected: forced, kind: "forced" });
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
return out;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
1873
|
// src/server/spell.ts
|
|
1633
1874
|
var instances = /* @__PURE__ */ new Map();
|
|
1634
1875
|
var loading = /* @__PURE__ */ new Set();
|
|
@@ -2060,7 +2301,7 @@ async function runLint(state, options = {}) {
|
|
|
2060
2301
|
spellers,
|
|
2061
2302
|
allowWords
|
|
2062
2303
|
};
|
|
2063
|
-
const ignoreRes = (config.ignore ?? []).map(
|
|
2304
|
+
const ignoreRes = (config.ignore ?? []).map(globToRegExp2);
|
|
2064
2305
|
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
2065
2306
|
const findings = [];
|
|
2066
2307
|
let suppressed = 0;
|
|
@@ -2696,6 +2937,60 @@ var vueI18nJson = {
|
|
|
2696
2937
|
}
|
|
2697
2938
|
};
|
|
2698
2939
|
|
|
2940
|
+
// src/server/adapters/next-intl-json.ts
|
|
2941
|
+
var DEFAULT_LOCALE_CASE8 = "lower-hyphen";
|
|
2942
|
+
var nextIntlJson = {
|
|
2943
|
+
name: "next-intl-json",
|
|
2944
|
+
capabilities: {
|
|
2945
|
+
plural: "native",
|
|
2946
|
+
select: "native",
|
|
2947
|
+
nesting: "both",
|
|
2948
|
+
metadata: false,
|
|
2949
|
+
placeholderStyle: "icu",
|
|
2950
|
+
fileGrouping: "per-locale"
|
|
2951
|
+
},
|
|
2952
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE8,
|
|
2953
|
+
export(state, output) {
|
|
2954
|
+
const files = [];
|
|
2955
|
+
const warnings = [];
|
|
2956
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE8));
|
|
2957
|
+
const { indent, finalNewline } = resolveFormat(state, output);
|
|
2958
|
+
const fmt = { indent, sortKeys: true, finalNewline };
|
|
2959
|
+
const emptyAs = resolveEmptyAs(output, "omit");
|
|
2960
|
+
const flatOutput = output.style === "flat";
|
|
2961
|
+
for (const locale of state.config.locales) {
|
|
2962
|
+
const flat = {};
|
|
2963
|
+
for (const [key, entry] of Object.entries(state.keys)) {
|
|
2964
|
+
if (entry.plural) {
|
|
2965
|
+
const forms = resolveForms(entry, locale, state.config.sourceLocale, emptyAs);
|
|
2966
|
+
if (!forms) continue;
|
|
2967
|
+
flat[key] = formsToIcu(entry.plural.arg, forms);
|
|
2968
|
+
} else {
|
|
2969
|
+
const raw = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
|
|
2970
|
+
if (raw === null) continue;
|
|
2971
|
+
flat[key] = raw;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
let payload = flat;
|
|
2975
|
+
if (!flatOutput) {
|
|
2976
|
+
const { tree, collisions } = nestKeys(flat);
|
|
2977
|
+
for (const key of collisions) {
|
|
2978
|
+
warnings.push({
|
|
2979
|
+
code: "key-collision",
|
|
2980
|
+
key,
|
|
2981
|
+
locale,
|
|
2982
|
+
message: "key is both a leaf and a parent; dropped from nested output"
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
payload = tree;
|
|
2986
|
+
}
|
|
2987
|
+
files.push({ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8)), contents: serializeJson(payload, fmt) });
|
|
2988
|
+
}
|
|
2989
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
2990
|
+
return { files, warnings };
|
|
2991
|
+
}
|
|
2992
|
+
};
|
|
2993
|
+
|
|
2699
2994
|
// src/server/adapters/angular-xliff.ts
|
|
2700
2995
|
function xmlEscape2(s) {
|
|
2701
2996
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -2751,7 +3046,7 @@ function renderEmbeddedIcu(value) {
|
|
|
2751
3046
|
function renderScalar(value, ids, placeholders) {
|
|
2752
3047
|
return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids, placeholders);
|
|
2753
3048
|
}
|
|
2754
|
-
var
|
|
3049
|
+
var DEFAULT_LOCALE_CASE9 = "bcp47-hyphen";
|
|
2755
3050
|
var angularXliff = {
|
|
2756
3051
|
name: "angular-xliff",
|
|
2757
3052
|
capabilities: {
|
|
@@ -2762,18 +3057,18 @@ var angularXliff = {
|
|
|
2762
3057
|
placeholderStyle: "icu",
|
|
2763
3058
|
fileGrouping: "per-locale"
|
|
2764
3059
|
},
|
|
2765
|
-
defaultLocaleCase:
|
|
3060
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE9,
|
|
2766
3061
|
export(state, output) {
|
|
2767
3062
|
const files = [];
|
|
2768
3063
|
const warnings = [];
|
|
2769
|
-
warnings.push(...localeCollisionWarnings(output, state.config.locales,
|
|
3064
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE9));
|
|
2770
3065
|
const sourceLocale = state.config.sourceLocale;
|
|
2771
|
-
const sourceToken = resolveLocaleToken(output, sourceLocale,
|
|
3066
|
+
const sourceToken = resolveLocaleToken(output, sourceLocale, DEFAULT_LOCALE_CASE9);
|
|
2772
3067
|
const emptyAs = resolveEmptyAs(output, "source");
|
|
2773
3068
|
const keys = Object.keys(state.keys).sort();
|
|
2774
3069
|
for (const locale of state.config.locales) {
|
|
2775
3070
|
if (output.skipSourceLocale && locale === sourceLocale) continue;
|
|
2776
|
-
const token = resolveLocaleToken(output, locale,
|
|
3071
|
+
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE9);
|
|
2777
3072
|
const units = [];
|
|
2778
3073
|
for (const key of keys) {
|
|
2779
3074
|
const entry = state.keys[key];
|
|
@@ -2837,7 +3132,7 @@ function yamlMap(node, indent, level) {
|
|
|
2837
3132
|
}
|
|
2838
3133
|
return lines;
|
|
2839
3134
|
}
|
|
2840
|
-
var
|
|
3135
|
+
var DEFAULT_LOCALE_CASE10 = "bcp47-hyphen";
|
|
2841
3136
|
var railsYaml = {
|
|
2842
3137
|
name: "rails-yaml",
|
|
2843
3138
|
capabilities: {
|
|
@@ -2848,10 +3143,10 @@ var railsYaml = {
|
|
|
2848
3143
|
placeholderStyle: "named",
|
|
2849
3144
|
fileGrouping: "per-locale"
|
|
2850
3145
|
},
|
|
2851
|
-
defaultLocaleCase:
|
|
3146
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE10,
|
|
2852
3147
|
export(state, output) {
|
|
2853
3148
|
const warnings = [];
|
|
2854
|
-
warnings.push(...localeCollisionWarnings(output, state.config.locales,
|
|
3149
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE10));
|
|
2855
3150
|
const { indent, finalNewline } = resolveFormat(state, output);
|
|
2856
3151
|
const emptyAs = resolveEmptyAs(output, "omit");
|
|
2857
3152
|
const files = [];
|
|
@@ -2883,7 +3178,7 @@ var railsYaml = {
|
|
|
2883
3178
|
for (const c of collisions) {
|
|
2884
3179
|
warnings.push({ code: "key-collision", key: c, locale, message: "key is both a leaf and a parent; dropped from nested output" });
|
|
2885
3180
|
}
|
|
2886
|
-
const token = resolveLocaleToken(output, locale,
|
|
3181
|
+
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE10);
|
|
2887
3182
|
const body = [`${yamlKey(token)}:`, ...yamlMap(nested, indent, 1)].join("\n");
|
|
2888
3183
|
files.push({ path: resolvePath(output.path, token), contents: finalNewline ? body + "\n" : body });
|
|
2889
3184
|
}
|
|
@@ -2926,6 +3221,7 @@ function getRegistry() {
|
|
|
2926
3221
|
[appleStringsdict.name]: appleStringsdict,
|
|
2927
3222
|
[appleStrings.name]: appleStrings,
|
|
2928
3223
|
[vueI18nJson.name]: vueI18nJson,
|
|
3224
|
+
[nextIntlJson.name]: nextIntlJson,
|
|
2929
3225
|
[angularXliff.name]: angularXliff,
|
|
2930
3226
|
[railsYaml.name]: railsYaml
|
|
2931
3227
|
};
|
|
@@ -2955,7 +3251,7 @@ function checkOutputs(state, root) {
|
|
|
2955
3251
|
}
|
|
2956
3252
|
|
|
2957
3253
|
// src/server/api.ts
|
|
2958
|
-
import { readFileSync as
|
|
3254
|
+
import { readFileSync as readFileSync25, existsSync as existsSync13, readdirSync as readdirSync16, statSync as statSync10, rmSync as rmSync6 } from "fs";
|
|
2959
3255
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
2960
3256
|
|
|
2961
3257
|
// src/server/ai/anthropic.ts
|
|
@@ -3201,6 +3497,48 @@ async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, o
|
|
|
3201
3497
|
return results;
|
|
3202
3498
|
}
|
|
3203
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
|
+
|
|
3204
3542
|
// src/server/ai/pricing.ts
|
|
3205
3543
|
function addUsage(into, add) {
|
|
3206
3544
|
into.inputTokens += add.inputTokens;
|
|
@@ -3217,7 +3555,9 @@ function usageCostUsd(usage, ai, multiplier = 1) {
|
|
|
3217
3555
|
return pricing ? estimateUsageCostUsd(usage, pricing, multiplier) : void 0;
|
|
3218
3556
|
}
|
|
3219
3557
|
function estimateUsageCostUsd(usage, pricing, multiplier = 1) {
|
|
3220
|
-
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;
|
|
3221
3561
|
return (inputCost + usage.outputTokens * pricing.outputPerMTok) / 1e6 * multiplier;
|
|
3222
3562
|
}
|
|
3223
3563
|
var PRICE_TABLE = [
|
|
@@ -3249,12 +3589,27 @@ function bareModelId(model) {
|
|
|
3249
3589
|
if (anth !== -1) id = id.slice(anth + "anthropic.".length);
|
|
3250
3590
|
return id;
|
|
3251
3591
|
}
|
|
3252
|
-
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()) {
|
|
3253
3604
|
if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
|
|
3254
3605
|
return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
|
|
3255
3606
|
}
|
|
3256
3607
|
if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
|
|
3257
3608
|
const id = bareModelId(ai.model);
|
|
3609
|
+
if (cache2) {
|
|
3610
|
+
const cached = lookupCachePrice(cache2, id);
|
|
3611
|
+
if (cached) return cached;
|
|
3612
|
+
}
|
|
3258
3613
|
let best;
|
|
3259
3614
|
for (const row of PRICE_TABLE) {
|
|
3260
3615
|
if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
|
|
@@ -3802,7 +4157,7 @@ function makeProvider(ai) {
|
|
|
3802
4157
|
}
|
|
3803
4158
|
|
|
3804
4159
|
// src/server/ai/run.ts
|
|
3805
|
-
import { readFileSync as
|
|
4160
|
+
import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
|
|
3806
4161
|
import { resolve as resolve5, extname as extname2 } from "path";
|
|
3807
4162
|
|
|
3808
4163
|
// src/server/cell-state.ts
|
|
@@ -3821,7 +4176,7 @@ function cellState(entry, locale, sourceLocale) {
|
|
|
3821
4176
|
// src/server/ai/run.ts
|
|
3822
4177
|
function selectRequests(state, opts) {
|
|
3823
4178
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
3824
|
-
const keyRe = opts.keyGlob ?
|
|
4179
|
+
const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
|
|
3825
4180
|
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
3826
4181
|
const stateSet = opts.states ? new Set(opts.states) : null;
|
|
3827
4182
|
const skip = (st) => stateSet ? !stateSet.has(st) : !!opts.onlyMissing && st !== "missing";
|
|
@@ -3899,7 +4254,7 @@ function attachScreenshots(reqs, state, projectRoot) {
|
|
|
3899
4254
|
if (!existsSync7(abs)) {
|
|
3900
4255
|
cache2.set(screenshot, null);
|
|
3901
4256
|
} else {
|
|
3902
|
-
const buf =
|
|
4257
|
+
const buf = readFileSync8(abs);
|
|
3903
4258
|
cache2.set(screenshot, buf.length > MAX_IMAGE_BYTES ? null : { mediaType, base64: buf.toString("base64") });
|
|
3904
4259
|
}
|
|
3905
4260
|
}
|
|
@@ -4013,27 +4368,71 @@ function applyResults(state, reqs, results, clock = systemClock, force = false)
|
|
|
4013
4368
|
if (applyMachineTranslationForms(state, req.key, req.targetLocale, res.forms, clock, force)) written++;
|
|
4014
4369
|
continue;
|
|
4015
4370
|
}
|
|
4016
|
-
if (res.translation === void 0) {
|
|
4017
|
-
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
4018
|
-
continue;
|
|
4371
|
+
if (res.translation === void 0) {
|
|
4372
|
+
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
4373
|
+
continue;
|
|
4374
|
+
}
|
|
4375
|
+
if (res.error) errors.push({ key: req.key, locale: req.targetLocale, error: res.error });
|
|
4376
|
+
if (applyMachineTranslation(state, req.key, req.targetLocale, res.translation, clock, force)) written++;
|
|
4377
|
+
}
|
|
4378
|
+
return { written, errors };
|
|
4379
|
+
}
|
|
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.";
|
|
4019
4416
|
}
|
|
4020
|
-
if (res.error) errors.push({ key: req.key, locale: req.targetLocale, error: res.error });
|
|
4021
|
-
if (applyMachineTranslation(state, req.key, req.targetLocale, res.translation, clock, force)) written++;
|
|
4022
4417
|
}
|
|
4023
|
-
|
|
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;
|
|
4024
4423
|
}
|
|
4025
4424
|
|
|
4026
4425
|
// src/server/ai/pending-batch.ts
|
|
4027
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as
|
|
4028
|
-
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";
|
|
4029
4428
|
function pendingBatchPath(projectRoot) {
|
|
4030
|
-
return
|
|
4429
|
+
return join5(projectRoot, ".glotfile", "batch.json");
|
|
4031
4430
|
}
|
|
4032
4431
|
function loadPendingBatch(projectRoot) {
|
|
4033
4432
|
const path = pendingBatchPath(projectRoot);
|
|
4034
4433
|
if (!existsSync8(path)) return void 0;
|
|
4035
4434
|
try {
|
|
4036
|
-
const parsed = JSON.parse(
|
|
4435
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
4037
4436
|
if (parsed?.version !== 1) return void 0;
|
|
4038
4437
|
return parsed;
|
|
4039
4438
|
} catch {
|
|
@@ -4041,9 +4440,9 @@ function loadPendingBatch(projectRoot) {
|
|
|
4041
4440
|
}
|
|
4042
4441
|
}
|
|
4043
4442
|
function savePendingBatch(projectRoot, pending) {
|
|
4044
|
-
const dir =
|
|
4443
|
+
const dir = join5(projectRoot, ".glotfile");
|
|
4045
4444
|
mkdirSync4(dir, { recursive: true });
|
|
4046
|
-
const gitignore =
|
|
4445
|
+
const gitignore = join5(dir, ".gitignore");
|
|
4047
4446
|
if (!existsSync8(gitignore)) writeFileSync3(gitignore, "*\n");
|
|
4048
4447
|
writeFileSync3(pendingBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
4049
4448
|
}
|
|
@@ -4052,7 +4451,7 @@ function clearPendingBatch(projectRoot) {
|
|
|
4052
4451
|
}
|
|
4053
4452
|
|
|
4054
4453
|
// src/server/log.ts
|
|
4055
|
-
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";
|
|
4056
4455
|
import { resolve as resolve6 } from "path";
|
|
4057
4456
|
function logPath(projectRoot) {
|
|
4058
4457
|
return resolve6(projectRoot, ".glotfile", "log.jsonl");
|
|
@@ -4067,7 +4466,7 @@ function appendLog(projectRoot, entry) {
|
|
|
4067
4466
|
}
|
|
4068
4467
|
function trimLog(path, maxBytes = MAX_LOG_BYTES, targetBytes = TRIM_LOG_TO_BYTES) {
|
|
4069
4468
|
if (!existsSync9(path) || statSync2(path).size <= maxBytes) return;
|
|
4070
|
-
const lines =
|
|
4469
|
+
const lines = readFileSync10(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
4071
4470
|
const kept = [];
|
|
4072
4471
|
let bytes = 0;
|
|
4073
4472
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -4245,16 +4644,16 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
|
|
|
4245
4644
|
}
|
|
4246
4645
|
|
|
4247
4646
|
// src/server/ai/pending-context-batch.ts
|
|
4248
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as
|
|
4249
|
-
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";
|
|
4250
4649
|
function pendingContextBatchPath(projectRoot) {
|
|
4251
|
-
return
|
|
4650
|
+
return join6(projectRoot, ".glotfile", "context-batch.json");
|
|
4252
4651
|
}
|
|
4253
4652
|
function loadPendingContextBatch(projectRoot) {
|
|
4254
4653
|
const path = pendingContextBatchPath(projectRoot);
|
|
4255
4654
|
if (!existsSync10(path)) return void 0;
|
|
4256
4655
|
try {
|
|
4257
|
-
const parsed = JSON.parse(
|
|
4656
|
+
const parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
4258
4657
|
if (parsed?.version !== 1) return void 0;
|
|
4259
4658
|
return parsed;
|
|
4260
4659
|
} catch {
|
|
@@ -4262,9 +4661,9 @@ function loadPendingContextBatch(projectRoot) {
|
|
|
4262
4661
|
}
|
|
4263
4662
|
}
|
|
4264
4663
|
function savePendingContextBatch(projectRoot, pending) {
|
|
4265
|
-
const dir =
|
|
4664
|
+
const dir = join6(projectRoot, ".glotfile");
|
|
4266
4665
|
mkdirSync5(dir, { recursive: true });
|
|
4267
|
-
const gitignore =
|
|
4666
|
+
const gitignore = join6(dir, ".gitignore");
|
|
4268
4667
|
if (!existsSync10(gitignore)) writeFileSync4(gitignore, "*\n");
|
|
4269
4668
|
writeFileSync4(pendingContextBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
4270
4669
|
}
|
|
@@ -4423,13 +4822,83 @@ function estimateTranslation(state, ai, opts) {
|
|
|
4423
4822
|
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4424
4823
|
};
|
|
4425
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
|
+
}
|
|
4426
4895
|
|
|
4427
4896
|
// src/server/import/run.ts
|
|
4428
4897
|
import { relative as relative3 } from "path";
|
|
4429
4898
|
|
|
4430
4899
|
// src/server/import/detect.ts
|
|
4431
|
-
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as
|
|
4432
|
-
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";
|
|
4433
4902
|
var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4434
4903
|
var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
4435
4904
|
function safeIsDir(p) {
|
|
@@ -4440,7 +4909,7 @@ function safeIsDir(p) {
|
|
|
4440
4909
|
}
|
|
4441
4910
|
}
|
|
4442
4911
|
function listDirs(dir) {
|
|
4443
|
-
return readdirSync3(dir).filter((e) => safeIsDir(
|
|
4912
|
+
return readdirSync3(dir).filter((e) => safeIsDir(join7(dir, e)));
|
|
4444
4913
|
}
|
|
4445
4914
|
function fileCount(dir) {
|
|
4446
4915
|
try {
|
|
@@ -4454,23 +4923,23 @@ function pickSource(locales, sizeOf) {
|
|
|
4454
4923
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4455
4924
|
}
|
|
4456
4925
|
function detectLaravel(root) {
|
|
4457
|
-
const localeRoot = [
|
|
4926
|
+
const localeRoot = [join7(root, "resources", "lang"), join7(root, "lang")].find(safeIsDir);
|
|
4458
4927
|
if (!localeRoot) return null;
|
|
4459
4928
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4460
4929
|
if (locales.length === 0) return null;
|
|
4461
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
4930
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join7(localeRoot, loc)));
|
|
4462
4931
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4463
4932
|
}
|
|
4464
4933
|
function detectVue(root, forced = false) {
|
|
4465
4934
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4466
|
-
const localeRoot =
|
|
4935
|
+
const localeRoot = join7(root, rel);
|
|
4467
4936
|
if (!safeIsDir(localeRoot)) continue;
|
|
4468
4937
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4469
4938
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4470
4939
|
if (enough) {
|
|
4471
4940
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4472
4941
|
try {
|
|
4473
|
-
return statSync3(
|
|
4942
|
+
return statSync3(join7(localeRoot, `${loc}.json`)).size;
|
|
4474
4943
|
} catch {
|
|
4475
4944
|
return 0;
|
|
4476
4945
|
}
|
|
@@ -4480,9 +4949,50 @@ function detectVue(root, forced = false) {
|
|
|
4480
4949
|
}
|
|
4481
4950
|
return null;
|
|
4482
4951
|
}
|
|
4952
|
+
var NEXT_INTL_CONFIG_CANDIDATES = ["src/i18n/request.ts", "i18n/request.ts", "src/i18n/request.js", "i18n/request.js"];
|
|
4953
|
+
var NEXT_INTL_ROUTING_CANDIDATES = ["src/i18n/routing.ts", "i18n/routing.ts", "src/i18n/routing.js", "i18n/routing.js"];
|
|
4954
|
+
var NEXT_INTL_DIR_CANDIDATES = ["messages", "src/messages", "locales", "src/locales", "src/i18n/messages"];
|
|
4955
|
+
function hasNextIntlSignal(root) {
|
|
4956
|
+
if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync11(join7(root, rel)))) return true;
|
|
4957
|
+
try {
|
|
4958
|
+
const pkg = JSON.parse(readFileSync12(join7(root, "package.json"), "utf8"));
|
|
4959
|
+
if (pkg.dependencies?.["next-intl"] || pkg.devDependencies?.["next-intl"]) return true;
|
|
4960
|
+
} catch {
|
|
4961
|
+
}
|
|
4962
|
+
return false;
|
|
4963
|
+
}
|
|
4964
|
+
function nextIntlDefaultLocale(root) {
|
|
4965
|
+
for (const rel of NEXT_INTL_ROUTING_CANDIDATES) {
|
|
4966
|
+
try {
|
|
4967
|
+
const m = readFileSync12(join7(root, rel), "utf8").match(/defaultLocale\s*:\s*['"]([^'"]+)['"]/);
|
|
4968
|
+
if (m) return m[1];
|
|
4969
|
+
} catch {
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
return void 0;
|
|
4973
|
+
}
|
|
4974
|
+
function detectNextIntl(root, forced = false) {
|
|
4975
|
+
if (!forced && !hasNextIntlSignal(root)) return null;
|
|
4976
|
+
for (const rel of NEXT_INTL_DIR_CANDIDATES) {
|
|
4977
|
+
const localeRoot = join7(root, rel);
|
|
4978
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
4979
|
+
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4980
|
+
if (locales.length === 0) continue;
|
|
4981
|
+
const def = nextIntlDefaultLocale(root);
|
|
4982
|
+
const sourceLocale = def && locales.includes(def) ? def : pickSource(locales, (loc) => {
|
|
4983
|
+
try {
|
|
4984
|
+
return statSync3(join7(localeRoot, `${loc}.json`)).size;
|
|
4985
|
+
} catch {
|
|
4986
|
+
return 0;
|
|
4987
|
+
}
|
|
4988
|
+
});
|
|
4989
|
+
return { format: "next-intl-json", localeRoot, locales, sourceLocale };
|
|
4990
|
+
}
|
|
4991
|
+
return null;
|
|
4992
|
+
}
|
|
4483
4993
|
function detectArb(root) {
|
|
4484
4994
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4485
|
-
const localeRoot =
|
|
4995
|
+
const localeRoot = join7(root, rel);
|
|
4486
4996
|
if (!safeIsDir(localeRoot)) continue;
|
|
4487
4997
|
const locales = readdirSync3(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4488
4998
|
if (locales.length >= 1) {
|
|
@@ -4492,10 +5002,10 @@ function detectArb(root) {
|
|
|
4492
5002
|
return null;
|
|
4493
5003
|
}
|
|
4494
5004
|
function lprojLocales(dir) {
|
|
4495
|
-
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")));
|
|
4496
5006
|
}
|
|
4497
5007
|
function detectApple(root) {
|
|
4498
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5008
|
+
const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
|
|
4499
5009
|
let best = null;
|
|
4500
5010
|
for (const dir of candidates) {
|
|
4501
5011
|
const locales = lprojLocales(dir);
|
|
@@ -4507,7 +5017,7 @@ function detectApple(root) {
|
|
|
4507
5017
|
locales,
|
|
4508
5018
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4509
5019
|
try {
|
|
4510
|
-
return statSync3(
|
|
5020
|
+
return statSync3(join7(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4511
5021
|
} catch {
|
|
4512
5022
|
return 0;
|
|
4513
5023
|
}
|
|
@@ -4520,7 +5030,7 @@ function detectApple(root) {
|
|
|
4520
5030
|
var ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
|
|
4521
5031
|
function detectAngularXliff(root) {
|
|
4522
5032
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4523
|
-
const localeRoot = rel === "." ? root :
|
|
5033
|
+
const localeRoot = rel === "." ? root : join7(root, rel);
|
|
4524
5034
|
if (!safeIsDir(localeRoot)) continue;
|
|
4525
5035
|
const files = readdirSync3(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4526
5036
|
if (files.length === 0) continue;
|
|
@@ -4528,7 +5038,7 @@ function detectAngularXliff(root) {
|
|
|
4528
5038
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4529
5039
|
let sourceLocale;
|
|
4530
5040
|
try {
|
|
4531
|
-
sourceLocale =
|
|
5041
|
+
sourceLocale = readFileSync12(join7(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4532
5042
|
} catch {
|
|
4533
5043
|
}
|
|
4534
5044
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -4539,14 +5049,14 @@ function detectAngularXliff(root) {
|
|
|
4539
5049
|
return null;
|
|
4540
5050
|
}
|
|
4541
5051
|
function detectRails(root) {
|
|
4542
|
-
const localeRoot =
|
|
5052
|
+
const localeRoot = join7(root, "config", "locales");
|
|
4543
5053
|
if (!safeIsDir(localeRoot)) return null;
|
|
4544
5054
|
const locales = [];
|
|
4545
5055
|
for (const file of readdirSync3(localeRoot).sort()) {
|
|
4546
5056
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
4547
5057
|
let text;
|
|
4548
5058
|
try {
|
|
4549
|
-
text =
|
|
5059
|
+
text = readFileSync12(join7(localeRoot, file), "utf8");
|
|
4550
5060
|
} catch {
|
|
4551
5061
|
continue;
|
|
4552
5062
|
}
|
|
@@ -4561,15 +5071,15 @@ function detectRails(root) {
|
|
|
4561
5071
|
var I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
|
|
4562
5072
|
function detectI18next(root) {
|
|
4563
5073
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4564
|
-
const localeRoot =
|
|
5074
|
+
const localeRoot = join7(root, rel);
|
|
4565
5075
|
if (!safeIsDir(localeRoot)) continue;
|
|
4566
5076
|
const locales = listDirs(localeRoot).filter(
|
|
4567
|
-
(d) => LOCALE_RE.test(d) && readdirSync3(
|
|
5077
|
+
(d) => LOCALE_RE.test(d) && readdirSync3(join7(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4568
5078
|
);
|
|
4569
5079
|
if (locales.length === 0) continue;
|
|
4570
5080
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4571
5081
|
try {
|
|
4572
|
-
return readdirSync3(
|
|
5082
|
+
return readdirSync3(join7(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join7(localeRoot, loc, f)).size, 0);
|
|
4573
5083
|
} catch {
|
|
4574
5084
|
return 0;
|
|
4575
5085
|
}
|
|
@@ -4586,8 +5096,8 @@ function gettextLocales(dir) {
|
|
|
4586
5096
|
if (!locales.includes(flat)) locales.push(flat);
|
|
4587
5097
|
continue;
|
|
4588
5098
|
}
|
|
4589
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
4590
|
-
const sub =
|
|
5099
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join7(dir, entry))) continue;
|
|
5100
|
+
const sub = join7(dir, entry);
|
|
4591
5101
|
const hasPo = (d) => {
|
|
4592
5102
|
try {
|
|
4593
5103
|
return readdirSync3(d).some((f) => f.endsWith(".po"));
|
|
@@ -4595,7 +5105,7 @@ function gettextLocales(dir) {
|
|
|
4595
5105
|
return false;
|
|
4596
5106
|
}
|
|
4597
5107
|
};
|
|
4598
|
-
if (hasPo(
|
|
5108
|
+
if (hasPo(join7(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4599
5109
|
if (!locales.includes(entry)) locales.push(entry);
|
|
4600
5110
|
}
|
|
4601
5111
|
}
|
|
@@ -4604,7 +5114,7 @@ function gettextLocales(dir) {
|
|
|
4604
5114
|
var GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
|
|
4605
5115
|
function detectGettext(root) {
|
|
4606
5116
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4607
|
-
const localeRoot =
|
|
5117
|
+
const localeRoot = join7(root, rel);
|
|
4608
5118
|
if (!safeIsDir(localeRoot)) continue;
|
|
4609
5119
|
const locales = gettextLocales(localeRoot);
|
|
4610
5120
|
if (locales.length === 0) continue;
|
|
@@ -4613,10 +5123,10 @@ function detectGettext(root) {
|
|
|
4613
5123
|
return null;
|
|
4614
5124
|
}
|
|
4615
5125
|
function detectAppleStringsdict(root) {
|
|
4616
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5126
|
+
const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
|
|
4617
5127
|
let best = null;
|
|
4618
5128
|
for (const dir of candidates) {
|
|
4619
|
-
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")));
|
|
4620
5130
|
if (locales.length === 0) continue;
|
|
4621
5131
|
if (!best || locales.length > best.locales.length) {
|
|
4622
5132
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4626,6 +5136,7 @@ function detectAppleStringsdict(root) {
|
|
|
4626
5136
|
}
|
|
4627
5137
|
var DETECTORS = [
|
|
4628
5138
|
detectLaravel,
|
|
5139
|
+
detectNextIntl,
|
|
4629
5140
|
detectVue,
|
|
4630
5141
|
detectArb,
|
|
4631
5142
|
detectApple,
|
|
@@ -4637,6 +5148,7 @@ var DETECTORS = [
|
|
|
4637
5148
|
];
|
|
4638
5149
|
var BY_FORMAT = {
|
|
4639
5150
|
"laravel-php": detectLaravel,
|
|
5151
|
+
"next-intl-json": (root) => detectNextIntl(root, true),
|
|
4640
5152
|
"vue-i18n-json": (root) => detectVue(root, true),
|
|
4641
5153
|
"flutter-arb": detectArb,
|
|
4642
5154
|
"apple-strings": detectApple,
|
|
@@ -4661,8 +5173,8 @@ function detect(root, formatOverride) {
|
|
|
4661
5173
|
}
|
|
4662
5174
|
|
|
4663
5175
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4664
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
4665
|
-
import { join as
|
|
5176
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync13 } from "fs";
|
|
5177
|
+
import { join as join8 } from "path";
|
|
4666
5178
|
|
|
4667
5179
|
// src/server/import/flatten.ts
|
|
4668
5180
|
function flattenObject(value, prefix, warnings) {
|
|
@@ -4703,7 +5215,7 @@ var vueI18nJson2 = {
|
|
|
4703
5215
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4704
5216
|
let data;
|
|
4705
5217
|
try {
|
|
4706
|
-
data = JSON.parse(
|
|
5218
|
+
data = JSON.parse(readFileSync13(join8(localeRoot, file), "utf8"));
|
|
4707
5219
|
} catch (e) {
|
|
4708
5220
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4709
5221
|
continue;
|
|
@@ -4717,9 +5229,40 @@ var vueI18nJson2 = {
|
|
|
4717
5229
|
}
|
|
4718
5230
|
};
|
|
4719
5231
|
|
|
5232
|
+
// src/server/import/parsers/next-intl-json.ts
|
|
5233
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
|
|
5234
|
+
import { join as join9 } from "path";
|
|
5235
|
+
var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5236
|
+
var nextIntlJson2 = {
|
|
5237
|
+
name: "next-intl-json",
|
|
5238
|
+
parse(localeRoot, opts) {
|
|
5239
|
+
const warnings = [];
|
|
5240
|
+
const keys = {};
|
|
5241
|
+
const locales = [];
|
|
5242
|
+
for (const file of readdirSync5(localeRoot).sort()) {
|
|
5243
|
+
if (!file.endsWith(".json")) continue;
|
|
5244
|
+
const locale = file.slice(0, -".json".length);
|
|
5245
|
+
if (!LOCALE_RE3.test(locale)) continue;
|
|
5246
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5247
|
+
let data;
|
|
5248
|
+
try {
|
|
5249
|
+
data = JSON.parse(readFileSync14(join9(localeRoot, file), "utf8"));
|
|
5250
|
+
} catch (e) {
|
|
5251
|
+
warnings.push(`next-intl-json: failed to parse ${file}: ${e.message}`);
|
|
5252
|
+
continue;
|
|
5253
|
+
}
|
|
5254
|
+
if (!locales.includes(locale)) locales.push(locale);
|
|
5255
|
+
for (const [key, value] of Object.entries(flattenObject(data, "", warnings))) {
|
|
5256
|
+
(keys[key] ??= { values: {} }).values[locale] = value;
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
return { locales, keys, warnings };
|
|
5260
|
+
}
|
|
5261
|
+
};
|
|
5262
|
+
|
|
4720
5263
|
// src/server/import/parsers/laravel-php.ts
|
|
4721
|
-
import { readdirSync as
|
|
4722
|
-
import { join as
|
|
5264
|
+
import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
5265
|
+
import { join as join10, relative as relative2 } from "path";
|
|
4723
5266
|
import { execFileSync } from "child_process";
|
|
4724
5267
|
|
|
4725
5268
|
// src/server/import/placeholders.ts
|
|
@@ -4735,13 +5278,13 @@ function railsToCanonical(value) {
|
|
|
4735
5278
|
|
|
4736
5279
|
// src/server/import/parsers/laravel-php.ts
|
|
4737
5280
|
function listDirs2(dir) {
|
|
4738
|
-
return
|
|
5281
|
+
return readdirSync6(dir).filter((e) => statSync4(join10(dir, e)).isDirectory());
|
|
4739
5282
|
}
|
|
4740
5283
|
function listPhpFiles(dir) {
|
|
4741
5284
|
const out = [];
|
|
4742
5285
|
const walk = (d) => {
|
|
4743
|
-
for (const e of
|
|
4744
|
-
const full =
|
|
5286
|
+
for (const e of readdirSync6(d)) {
|
|
5287
|
+
const full = join10(d, e);
|
|
4745
5288
|
if (statSync4(full).isDirectory()) walk(full);
|
|
4746
5289
|
else if (e.endsWith(".php")) out.push(full);
|
|
4747
5290
|
}
|
|
@@ -4778,7 +5321,7 @@ var laravelPhp2 = {
|
|
|
4778
5321
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
4779
5322
|
if (locale === "vendor") continue;
|
|
4780
5323
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4781
|
-
const localeDir =
|
|
5324
|
+
const localeDir = join10(localeRoot, locale);
|
|
4782
5325
|
locales.push(locale);
|
|
4783
5326
|
for (const file of listPhpFiles(localeDir)) {
|
|
4784
5327
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -4801,15 +5344,15 @@ var laravelPhp2 = {
|
|
|
4801
5344
|
};
|
|
4802
5345
|
|
|
4803
5346
|
// src/server/import/parsers/flutter-arb.ts
|
|
4804
|
-
import { readdirSync as
|
|
4805
|
-
import { join as
|
|
4806
|
-
var
|
|
5347
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync15 } from "fs";
|
|
5348
|
+
import { join as join11 } from "path";
|
|
5349
|
+
var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4807
5350
|
function localeFromArbName(file) {
|
|
4808
5351
|
const m = file.match(/^(.+)\.arb$/);
|
|
4809
5352
|
if (!m) return null;
|
|
4810
5353
|
let locale = m[1];
|
|
4811
5354
|
if (locale.startsWith("app_")) locale = locale.slice(4);
|
|
4812
|
-
return
|
|
5355
|
+
return LOCALE_RE4.test(locale) ? locale : null;
|
|
4813
5356
|
}
|
|
4814
5357
|
function placeholderMeta(raw) {
|
|
4815
5358
|
if (!raw || typeof raw !== "object") return void 0;
|
|
@@ -4831,14 +5374,14 @@ var flutterArb2 = {
|
|
|
4831
5374
|
const warnings = [];
|
|
4832
5375
|
const keys = {};
|
|
4833
5376
|
const locales = [];
|
|
4834
|
-
for (const file of
|
|
5377
|
+
for (const file of readdirSync7(localeRoot).sort()) {
|
|
4835
5378
|
if (!file.endsWith(".arb")) continue;
|
|
4836
5379
|
const locale = localeFromArbName(file);
|
|
4837
5380
|
if (!locale) continue;
|
|
4838
5381
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4839
5382
|
let data;
|
|
4840
5383
|
try {
|
|
4841
|
-
data = JSON.parse(
|
|
5384
|
+
data = JSON.parse(readFileSync15(join11(localeRoot, file), "utf8"));
|
|
4842
5385
|
} catch (e) {
|
|
4843
5386
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
4844
5387
|
continue;
|
|
@@ -4863,14 +5406,14 @@ var flutterArb2 = {
|
|
|
4863
5406
|
};
|
|
4864
5407
|
|
|
4865
5408
|
// src/server/import/parsers/apple-strings.ts
|
|
4866
|
-
import { readdirSync as
|
|
4867
|
-
import { join as
|
|
4868
|
-
var
|
|
5409
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync16, statSync as statSync5 } from "fs";
|
|
5410
|
+
import { join as join12 } from "path";
|
|
5411
|
+
var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4869
5412
|
var TABLE = "Localizable.strings";
|
|
4870
5413
|
function localeFromLproj(dir) {
|
|
4871
5414
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
4872
5415
|
if (!m) return null;
|
|
4873
|
-
return
|
|
5416
|
+
return LOCALE_RE5.test(m[1]) ? m[1] : null;
|
|
4874
5417
|
}
|
|
4875
5418
|
function printfToCanonical(s) {
|
|
4876
5419
|
return s.replace(/%%/g, "%");
|
|
@@ -4976,20 +5519,20 @@ var appleStrings2 = {
|
|
|
4976
5519
|
const warnings = [];
|
|
4977
5520
|
const keys = {};
|
|
4978
5521
|
const locales = [];
|
|
4979
|
-
for (const dir of
|
|
5522
|
+
for (const dir of readdirSync8(localeRoot).sort()) {
|
|
4980
5523
|
const locale = localeFromLproj(dir);
|
|
4981
5524
|
if (!locale) continue;
|
|
4982
5525
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4983
|
-
const file =
|
|
5526
|
+
const file = join12(localeRoot, dir, TABLE);
|
|
4984
5527
|
let text;
|
|
4985
5528
|
try {
|
|
4986
5529
|
if (!statSync5(file).isFile()) continue;
|
|
4987
|
-
text =
|
|
5530
|
+
text = readFileSync16(file, "utf8");
|
|
4988
5531
|
} catch {
|
|
4989
5532
|
continue;
|
|
4990
5533
|
}
|
|
4991
5534
|
locales.push(locale);
|
|
4992
|
-
const others =
|
|
5535
|
+
const others = readdirSync8(join12(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
4993
5536
|
if (others.length) {
|
|
4994
5537
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
4995
5538
|
}
|
|
@@ -5002,9 +5545,9 @@ var appleStrings2 = {
|
|
|
5002
5545
|
};
|
|
5003
5546
|
|
|
5004
5547
|
// src/server/import/parsers/angular-xliff.ts
|
|
5005
|
-
import { readdirSync as
|
|
5006
|
-
import { join as
|
|
5007
|
-
var
|
|
5548
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync17 } from "fs";
|
|
5549
|
+
import { join as join13 } from "path";
|
|
5550
|
+
var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5008
5551
|
var FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
|
|
5009
5552
|
function decodeEntities(s) {
|
|
5010
5553
|
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
@@ -5064,13 +5607,13 @@ var angularXliff2 = {
|
|
|
5064
5607
|
const seen = (loc) => {
|
|
5065
5608
|
if (!locales.includes(loc)) locales.push(loc);
|
|
5066
5609
|
};
|
|
5067
|
-
const files =
|
|
5610
|
+
const files = readdirSync9(localeRoot).filter((f) => FILE_RE.test(f)).sort((a, b) => (a === "messages.xlf" ? -1 : 0) - (b === "messages.xlf" ? -1 : 0) || a.localeCompare(b));
|
|
5068
5611
|
for (const file of files) {
|
|
5069
5612
|
const fnameLocale = file.match(FILE_RE)[1];
|
|
5070
|
-
if (fnameLocale !== void 0 && !
|
|
5613
|
+
if (fnameLocale !== void 0 && !LOCALE_RE6.test(fnameLocale)) continue;
|
|
5071
5614
|
let xml;
|
|
5072
5615
|
try {
|
|
5073
|
-
xml =
|
|
5616
|
+
xml = readFileSync17(join13(localeRoot, file), "utf8");
|
|
5074
5617
|
} catch (e) {
|
|
5075
5618
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
5076
5619
|
continue;
|
|
@@ -5115,9 +5658,9 @@ var angularXliff2 = {
|
|
|
5115
5658
|
};
|
|
5116
5659
|
|
|
5117
5660
|
// src/server/import/parsers/gettext-po.ts
|
|
5118
|
-
import { readdirSync as
|
|
5119
|
-
import { join as
|
|
5120
|
-
var
|
|
5661
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync18 } from "fs";
|
|
5662
|
+
import { join as join14 } from "path";
|
|
5663
|
+
var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5121
5664
|
var DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
|
|
5122
5665
|
var CONT_RE = /^[ \t]*"(.*)"\s*$/;
|
|
5123
5666
|
function unescapePo(s) {
|
|
@@ -5204,21 +5747,21 @@ function parseEntries(text) {
|
|
|
5204
5747
|
}
|
|
5205
5748
|
function discoverPoFiles(root) {
|
|
5206
5749
|
const found = [];
|
|
5207
|
-
const entries =
|
|
5750
|
+
const entries = readdirSync10(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
5208
5751
|
for (const e of entries) {
|
|
5209
5752
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
5210
5753
|
const base = e.name.slice(0, -3);
|
|
5211
|
-
found.push({ path:
|
|
5212
|
-
} else if (e.isDirectory() &&
|
|
5213
|
-
for (const sub of [
|
|
5754
|
+
found.push({ path: join14(root, e.name), rel: e.name, locale: LOCALE_RE7.test(base) ? base : null });
|
|
5755
|
+
} else if (e.isDirectory() && LOCALE_RE7.test(e.name)) {
|
|
5756
|
+
for (const sub of [join14(e.name, "LC_MESSAGES"), e.name]) {
|
|
5214
5757
|
let names;
|
|
5215
5758
|
try {
|
|
5216
|
-
names =
|
|
5759
|
+
names = readdirSync10(join14(root, sub)).sort();
|
|
5217
5760
|
} catch {
|
|
5218
5761
|
continue;
|
|
5219
5762
|
}
|
|
5220
5763
|
for (const f of names) {
|
|
5221
|
-
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 });
|
|
5222
5765
|
}
|
|
5223
5766
|
}
|
|
5224
5767
|
}
|
|
@@ -5234,7 +5777,7 @@ var gettextPo2 = {
|
|
|
5234
5777
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
5235
5778
|
let entries;
|
|
5236
5779
|
try {
|
|
5237
|
-
entries = parseEntries(
|
|
5780
|
+
entries = parseEntries(readFileSync18(file.path, "utf8"));
|
|
5238
5781
|
} catch (e) {
|
|
5239
5782
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
5240
5783
|
continue;
|
|
@@ -5279,9 +5822,9 @@ var gettextPo2 = {
|
|
|
5279
5822
|
};
|
|
5280
5823
|
|
|
5281
5824
|
// src/server/import/parsers/i18next-json.ts
|
|
5282
|
-
import { readdirSync as
|
|
5283
|
-
import { join as
|
|
5284
|
-
var
|
|
5825
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync19, statSync as statSync6 } from "fs";
|
|
5826
|
+
import { join as join15 } from "path";
|
|
5827
|
+
var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5285
5828
|
var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
5286
5829
|
var PLURAL_ARG = "count";
|
|
5287
5830
|
var DEFAULT_NAMESPACE = "translation";
|
|
@@ -5299,7 +5842,7 @@ function fromI18next(value) {
|
|
|
5299
5842
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
5300
5843
|
let data;
|
|
5301
5844
|
try {
|
|
5302
|
-
data = JSON.parse(
|
|
5845
|
+
data = JSON.parse(readFileSync19(path, "utf8"));
|
|
5303
5846
|
} catch (e) {
|
|
5304
5847
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
5305
5848
|
return false;
|
|
@@ -5340,22 +5883,22 @@ var i18nextJson2 = {
|
|
|
5340
5883
|
const warnings = [];
|
|
5341
5884
|
const keys = {};
|
|
5342
5885
|
const locales = [];
|
|
5343
|
-
for (const entry of
|
|
5344
|
-
const full =
|
|
5886
|
+
for (const entry of readdirSync11(localeRoot).sort()) {
|
|
5887
|
+
const full = join15(localeRoot, entry);
|
|
5345
5888
|
if (safeIsDir2(full)) {
|
|
5346
|
-
if (!
|
|
5889
|
+
if (!LOCALE_RE8.test(entry)) continue;
|
|
5347
5890
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
5348
5891
|
let any = false;
|
|
5349
|
-
for (const file of
|
|
5892
|
+
for (const file of readdirSync11(full).sort()) {
|
|
5350
5893
|
if (!file.endsWith(".json")) continue;
|
|
5351
5894
|
const ns = file.slice(0, -".json".length);
|
|
5352
5895
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
5353
|
-
if (ingestFile(
|
|
5896
|
+
if (ingestFile(join15(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
5354
5897
|
}
|
|
5355
5898
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
5356
5899
|
} else if (entry.endsWith(".json")) {
|
|
5357
5900
|
const locale = entry.slice(0, -".json".length);
|
|
5358
|
-
if (!
|
|
5901
|
+
if (!LOCALE_RE8.test(locale)) continue;
|
|
5359
5902
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5360
5903
|
if (ingestFile(full, entry, "", locale, keys, warnings) && !locales.includes(locale)) {
|
|
5361
5904
|
locales.push(locale);
|
|
@@ -5367,9 +5910,9 @@ var i18nextJson2 = {
|
|
|
5367
5910
|
};
|
|
5368
5911
|
|
|
5369
5912
|
// src/server/import/parsers/rails-yaml.ts
|
|
5370
|
-
import { readdirSync as
|
|
5371
|
-
import { join as
|
|
5372
|
-
var
|
|
5913
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync20 } from "fs";
|
|
5914
|
+
import { join as join16 } from "path";
|
|
5915
|
+
var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
|
|
5373
5916
|
var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
5374
5917
|
function makeNode() {
|
|
5375
5918
|
return /* @__PURE__ */ Object.create(null);
|
|
@@ -5582,18 +6125,18 @@ var railsYaml2 = {
|
|
|
5582
6125
|
else flatten(v, key, locale, file);
|
|
5583
6126
|
}
|
|
5584
6127
|
};
|
|
5585
|
-
for (const file of
|
|
6128
|
+
for (const file of readdirSync12(localeRoot).sort()) {
|
|
5586
6129
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5587
6130
|
let text;
|
|
5588
6131
|
try {
|
|
5589
|
-
text =
|
|
6132
|
+
text = readFileSync20(join16(localeRoot, file), "utf8");
|
|
5590
6133
|
} catch (e) {
|
|
5591
6134
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5592
6135
|
continue;
|
|
5593
6136
|
}
|
|
5594
6137
|
const { roots } = parseYamlSubset(text, file, warnings);
|
|
5595
6138
|
for (const token of Object.keys(roots).sort()) {
|
|
5596
|
-
if (!
|
|
6139
|
+
if (!LOCALE_RE9.test(token)) {
|
|
5597
6140
|
warnings.push(`rails-yaml: ${file}: top-level key "${token}" is not a locale; subtree skipped`);
|
|
5598
6141
|
continue;
|
|
5599
6142
|
}
|
|
@@ -5607,14 +6150,14 @@ var railsYaml2 = {
|
|
|
5607
6150
|
};
|
|
5608
6151
|
|
|
5609
6152
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5610
|
-
import { readdirSync as
|
|
5611
|
-
import { join as
|
|
5612
|
-
var
|
|
6153
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync21, statSync as statSync7 } from "fs";
|
|
6154
|
+
import { join as join17 } from "path";
|
|
6155
|
+
var LOCALE_RE10 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5613
6156
|
var TABLE2 = "Localizable.stringsdict";
|
|
5614
6157
|
function localeFromLproj2(dir) {
|
|
5615
6158
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
5616
6159
|
if (!m) return null;
|
|
5617
|
-
return
|
|
6160
|
+
return LOCALE_RE10.test(m[1]) ? m[1] : null;
|
|
5618
6161
|
}
|
|
5619
6162
|
function decodeEntities2(s) {
|
|
5620
6163
|
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
@@ -5758,20 +6301,20 @@ var appleStringsdict2 = {
|
|
|
5758
6301
|
const warnings = [];
|
|
5759
6302
|
const keys = {};
|
|
5760
6303
|
const locales = [];
|
|
5761
|
-
for (const dir of
|
|
6304
|
+
for (const dir of readdirSync13(localeRoot).sort()) {
|
|
5762
6305
|
const locale = localeFromLproj2(dir);
|
|
5763
6306
|
if (!locale) continue;
|
|
5764
6307
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5765
|
-
const file =
|
|
6308
|
+
const file = join17(localeRoot, dir, TABLE2);
|
|
5766
6309
|
let text;
|
|
5767
6310
|
try {
|
|
5768
6311
|
if (!statSync7(file).isFile()) continue;
|
|
5769
|
-
text =
|
|
6312
|
+
text = readFileSync21(file, "utf8");
|
|
5770
6313
|
} catch {
|
|
5771
6314
|
continue;
|
|
5772
6315
|
}
|
|
5773
6316
|
locales.push(locale);
|
|
5774
|
-
const others =
|
|
6317
|
+
const others = readdirSync13(join17(localeRoot, dir)).filter(
|
|
5775
6318
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5776
6319
|
);
|
|
5777
6320
|
if (others.length) {
|
|
@@ -5799,6 +6342,7 @@ var appleStringsdict2 = {
|
|
|
5799
6342
|
// src/server/import/parsers/index.ts
|
|
5800
6343
|
var REGISTRY = {
|
|
5801
6344
|
[vueI18nJson2.name]: vueI18nJson2,
|
|
6345
|
+
[nextIntlJson2.name]: nextIntlJson2,
|
|
5802
6346
|
[laravelPhp2.name]: laravelPhp2,
|
|
5803
6347
|
[flutterArb2.name]: flutterArb2,
|
|
5804
6348
|
[appleStrings2.name]: appleStrings2,
|
|
@@ -5818,6 +6362,9 @@ function getParser(name) {
|
|
|
5818
6362
|
var OUTPUT_BY_FORMAT = {
|
|
5819
6363
|
"laravel-php": { adapter: "laravel-php", path: "lang/{locale}/{namespace}.php" },
|
|
5820
6364
|
"vue-i18n-json": { adapter: "vue-i18n-json", path: "src/locale/{locale}.json" },
|
|
6365
|
+
// rootRelative: write back to wherever the messages dir was found (messages/,
|
|
6366
|
+
// src/messages/, …) rather than assuming the conventional root-level location.
|
|
6367
|
+
"next-intl-json": { adapter: "next-intl-json", path: "{locale}.json", rootRelative: true },
|
|
5821
6368
|
"flutter-arb": { adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" },
|
|
5822
6369
|
"apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true },
|
|
5823
6370
|
// skipSourceLocale: ng extract-i18n owns messages.xlf (the source file); glotfile
|
|
@@ -5891,6 +6438,7 @@ function assemble2(parsed, opts) {
|
|
|
5891
6438
|
spelling: { customWords: [] }
|
|
5892
6439
|
},
|
|
5893
6440
|
glossary: [],
|
|
6441
|
+
glossarySuggestions: [],
|
|
5894
6442
|
keys,
|
|
5895
6443
|
warnings
|
|
5896
6444
|
};
|
|
@@ -6087,7 +6635,7 @@ function refreshLocationUsage(projectRoot, format) {
|
|
|
6087
6635
|
}
|
|
6088
6636
|
|
|
6089
6637
|
// src/server/export-run.ts
|
|
6090
|
-
import { existsSync as existsSync12, readFileSync as
|
|
6638
|
+
import { existsSync as existsSync12, readFileSync as readFileSync22, readdirSync as readdirSync14, rmdirSync, statSync as statSync8, unlinkSync } from "fs";
|
|
6091
6639
|
import { dirname as dirname2, resolve as resolve7, sep } from "path";
|
|
6092
6640
|
function effectiveLocales(config) {
|
|
6093
6641
|
const limit = config.exportLocales;
|
|
@@ -6100,11 +6648,11 @@ function narrowForExport(state) {
|
|
|
6100
6648
|
return { ...state, config: { ...state.config, locales } };
|
|
6101
6649
|
}
|
|
6102
6650
|
var LOCALE_TOKEN = /^[a-z]{2,3}([_-][a-z0-9]+)*$/i;
|
|
6103
|
-
function
|
|
6651
|
+
function escapeRegExp2(s) {
|
|
6104
6652
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6105
6653
|
}
|
|
6106
6654
|
function segmentRegExp(segment) {
|
|
6107
|
-
const pattern =
|
|
6655
|
+
const pattern = escapeRegExp2(segment).replaceAll("\\{locale\\}", "(?<locale>[A-Za-z0-9_-]+)").replaceAll("\\{namespace\\}", "[^/]*");
|
|
6108
6656
|
return new RegExp(`^${pattern}$`);
|
|
6109
6657
|
}
|
|
6110
6658
|
function removeEmptyDirs(dir, stopAt) {
|
|
@@ -6143,7 +6691,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
|
6143
6691
|
const re = segmentRegExp(segment);
|
|
6144
6692
|
let entries;
|
|
6145
6693
|
try {
|
|
6146
|
-
entries =
|
|
6694
|
+
entries = readdirSync14(dir, { withFileTypes: true });
|
|
6147
6695
|
} catch {
|
|
6148
6696
|
return;
|
|
6149
6697
|
}
|
|
@@ -6186,7 +6734,7 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
6186
6734
|
writtenPaths.add(abs);
|
|
6187
6735
|
let current = null;
|
|
6188
6736
|
try {
|
|
6189
|
-
current =
|
|
6737
|
+
current = readFileSync22(abs, "utf8");
|
|
6190
6738
|
} catch {
|
|
6191
6739
|
}
|
|
6192
6740
|
if (current === f.contents) {
|
|
@@ -6203,17 +6751,17 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
6203
6751
|
}
|
|
6204
6752
|
|
|
6205
6753
|
// src/server/ui-prefs.ts
|
|
6206
|
-
import { readFileSync as
|
|
6207
|
-
import { homedir } from "os";
|
|
6208
|
-
import { join as
|
|
6754
|
+
import { readFileSync as readFileSync23 } from "fs";
|
|
6755
|
+
import { homedir as homedir2 } from "os";
|
|
6756
|
+
import { join as join18 } from "path";
|
|
6209
6757
|
var THEMES = ["system", "light", "dark"];
|
|
6210
6758
|
var isThemeMode = (v) => THEMES.includes(v);
|
|
6211
6759
|
var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
6212
|
-
var defaultUiPrefsPath = () =>
|
|
6760
|
+
var defaultUiPrefsPath = () => join18(homedir2(), ".glotfile", "ui.json");
|
|
6213
6761
|
var DEFAULTS = { theme: "system" };
|
|
6214
6762
|
function readJson(path) {
|
|
6215
6763
|
try {
|
|
6216
|
-
const parsed = JSON.parse(
|
|
6764
|
+
const parsed = JSON.parse(readFileSync23(path, "utf8"));
|
|
6217
6765
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
6218
6766
|
} catch {
|
|
6219
6767
|
return {};
|
|
@@ -6232,7 +6780,7 @@ function saveUiPrefs(path, prefs) {
|
|
|
6232
6780
|
}
|
|
6233
6781
|
|
|
6234
6782
|
// src/server/local-settings.ts
|
|
6235
|
-
import { readFileSync as
|
|
6783
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
6236
6784
|
import { resolve as resolve8 } from "path";
|
|
6237
6785
|
var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
|
|
6238
6786
|
var isEditorId = (v) => EDITOR_IDS.includes(v);
|
|
@@ -6247,7 +6795,7 @@ var DEFAULT_EDITOR = "vscode";
|
|
|
6247
6795
|
var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
|
|
6248
6796
|
function readJson2(path) {
|
|
6249
6797
|
try {
|
|
6250
|
-
const parsed = JSON.parse(
|
|
6798
|
+
const parsed = JSON.parse(readFileSync24(path, "utf8"));
|
|
6251
6799
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
6252
6800
|
} catch {
|
|
6253
6801
|
return {};
|
|
@@ -6336,8 +6884,8 @@ function createEventHub() {
|
|
|
6336
6884
|
}
|
|
6337
6885
|
|
|
6338
6886
|
// src/server/watch.ts
|
|
6339
|
-
import { statSync as statSync9, readdirSync as
|
|
6340
|
-
import { join as
|
|
6887
|
+
import { statSync as statSync9, readdirSync as readdirSync15 } from "fs";
|
|
6888
|
+
import { join as join19 } from "path";
|
|
6341
6889
|
import { createHash as createHash2 } from "crypto";
|
|
6342
6890
|
function hashState(state) {
|
|
6343
6891
|
return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
|
|
@@ -6353,15 +6901,15 @@ function signature(statePath) {
|
|
|
6353
6901
|
const parts = [];
|
|
6354
6902
|
for (const rel of ["config.json", "keys.json"]) {
|
|
6355
6903
|
try {
|
|
6356
|
-
const s = statSync9(
|
|
6904
|
+
const s = statSync9(join19(dir, rel));
|
|
6357
6905
|
parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
|
|
6358
6906
|
} catch {
|
|
6359
6907
|
}
|
|
6360
6908
|
}
|
|
6361
6909
|
try {
|
|
6362
|
-
for (const name of
|
|
6910
|
+
for (const name of readdirSync15(join19(dir, "locales")).sort()) {
|
|
6363
6911
|
if (!name.endsWith(".json")) continue;
|
|
6364
|
-
const s = statSync9(
|
|
6912
|
+
const s = statSync9(join19(dir, "locales", name));
|
|
6365
6913
|
parts.push(`${name}:${s.size}:${s.mtimeMs}`);
|
|
6366
6914
|
}
|
|
6367
6915
|
} catch {
|
|
@@ -6436,28 +6984,13 @@ function projectName(root) {
|
|
|
6436
6984
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
6437
6985
|
if (existsSync13(nameFile)) {
|
|
6438
6986
|
try {
|
|
6439
|
-
const name =
|
|
6987
|
+
const name = readFileSync25(nameFile, "utf8").trim();
|
|
6440
6988
|
if (name) return name;
|
|
6441
6989
|
} catch {
|
|
6442
6990
|
}
|
|
6443
6991
|
}
|
|
6444
6992
|
return basename(root);
|
|
6445
6993
|
}
|
|
6446
|
-
function attachUsageSnippets(targets, cache2, projectRoot) {
|
|
6447
|
-
const fileCache = /* @__PURE__ */ new Map();
|
|
6448
|
-
for (const target of targets) {
|
|
6449
|
-
const allRefs = Object.entries(cache2.files).flatMap(
|
|
6450
|
-
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
6451
|
-
key: r.key,
|
|
6452
|
-
file,
|
|
6453
|
-
line: r.line,
|
|
6454
|
-
col: r.col,
|
|
6455
|
-
scanner: r.scanner
|
|
6456
|
-
}))
|
|
6457
|
-
);
|
|
6458
|
-
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
6459
|
-
}
|
|
6460
|
-
}
|
|
6461
6994
|
function createApi(deps) {
|
|
6462
6995
|
const app = new Hono();
|
|
6463
6996
|
const load = () => loadState(deps.statePath);
|
|
@@ -6589,6 +7122,61 @@ function createApi(deps) {
|
|
|
6589
7122
|
}
|
|
6590
7123
|
return c.json({ ok: true });
|
|
6591
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
|
+
});
|
|
6592
7180
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
6593
7181
|
app.get("/files", (c) => {
|
|
6594
7182
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -6602,7 +7190,7 @@ function createApi(deps) {
|
|
|
6602
7190
|
if (depth > 4) return;
|
|
6603
7191
|
let entries = [];
|
|
6604
7192
|
try {
|
|
6605
|
-
entries =
|
|
7193
|
+
entries = readdirSync16(dir);
|
|
6606
7194
|
} catch {
|
|
6607
7195
|
return;
|
|
6608
7196
|
}
|
|
@@ -6953,6 +7541,90 @@ function createApi(deps) {
|
|
|
6953
7541
|
logChange({ kind: "glossary", summary: `Deleted glossary term "${term}"`, before });
|
|
6954
7542
|
return c.json({ ok: true });
|
|
6955
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
|
+
});
|
|
6956
7628
|
app.post("/keys/:key/screenshot", async (c) => {
|
|
6957
7629
|
const key = c.req.param("key");
|
|
6958
7630
|
const body = await c.req.parseBody();
|
|
@@ -7138,7 +7810,7 @@ function createApi(deps) {
|
|
|
7138
7810
|
try {
|
|
7139
7811
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7140
7812
|
} catch (e) {
|
|
7141
|
-
await stream.writeSSE({ event: "error", data: JSON.stringify({ error:
|
|
7813
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: explainProviderError(aiCfg.provider, e) }) });
|
|
7142
7814
|
return;
|
|
7143
7815
|
}
|
|
7144
7816
|
const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
@@ -7155,58 +7827,65 @@ function createApi(deps) {
|
|
|
7155
7827
|
event: "start",
|
|
7156
7828
|
data: JSON.stringify({ total: reqs.length, locales: [...localeTotals].map(([locale, total]) => ({ locale, total })) })
|
|
7157
7829
|
});
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
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) }) });
|
|
7208
7886
|
}
|
|
7209
|
-
|
|
7887
|
+
return;
|
|
7888
|
+
}
|
|
7210
7889
|
if (!signal?.aborted) {
|
|
7211
7890
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
7212
7891
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -7233,23 +7912,28 @@ function createApi(deps) {
|
|
|
7233
7912
|
try {
|
|
7234
7913
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7235
7914
|
} catch (e) {
|
|
7236
|
-
return c.json({ error:
|
|
7915
|
+
return c.json({ error: explainProviderError(aiCfg.provider, e) }, 400);
|
|
7237
7916
|
}
|
|
7238
7917
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
7239
7918
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
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
|
+
}
|
|
7253
7937
|
const latest = load();
|
|
7254
7938
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
7255
7939
|
const usage = provider.takeUsage?.();
|
|
@@ -7520,6 +8204,22 @@ function createApi(deps) {
|
|
|
7520
8204
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
7521
8205
|
});
|
|
7522
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
|
+
});
|
|
7523
8223
|
app.get("/context/batch/status", async (c) => {
|
|
7524
8224
|
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7525
8225
|
let supported = false;
|
|
@@ -7622,7 +8322,7 @@ function createApi(deps) {
|
|
|
7622
8322
|
|
|
7623
8323
|
// src/server/server.ts
|
|
7624
8324
|
var here = dirname4(fileURLToPath(import.meta.url));
|
|
7625
|
-
var DEFAULT_UI_DIR =
|
|
8325
|
+
var DEFAULT_UI_DIR = join20(here, "..", "ui");
|
|
7626
8326
|
var MIME = {
|
|
7627
8327
|
".html": "text/html; charset=utf-8",
|
|
7628
8328
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7689,7 +8389,7 @@ function buildApp(opts) {
|
|
|
7689
8389
|
const file = await readFileResponse(target);
|
|
7690
8390
|
if (file) return file;
|
|
7691
8391
|
}
|
|
7692
|
-
const index = await readFileResponse(
|
|
8392
|
+
const index = await readFileResponse(join20(root, "index.html"));
|
|
7693
8393
|
if (index) return index;
|
|
7694
8394
|
return c.notFound();
|
|
7695
8395
|
});
|