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.
@@ -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 join18, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
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
- const state = { glossary: [], ...raw };
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 globToRegExp(glob) {
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(globToRegExp);
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 = 7;
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 base = scanner === "flutter" ? flutterPatterns(content, opts) : PATTERNS[scanner] ?? [];
1055
- const patterns = [...base, ...customPatterns(opts)];
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
- for (const pattern of patterns) {
1061
- const re = new RegExp(pattern.source, "g");
1062
- let m;
1063
- while ((m = re.exec(content)) !== null) {
1064
- if (m.index === re.lastIndex) re.lastIndex++;
1065
- const key = m[1];
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 patterns = PREFIX_PATTERNS[scanner];
1079
- if (!patterns) return [];
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
- for (const pattern of patterns) {
1084
- const re = new RegExp(pattern.source, "g");
1085
- let m;
1086
- while ((m = re.exec(content)) !== null) {
1087
- if (m.index === re.lastIndex) re.lastIndex++;
1088
- const prefix = m[1];
1089
- if (!prefix) continue;
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 globToRegExp2(glob) {
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 ? globToRegExp2(opts.keyGlob) : null;
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(globToRegExp);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -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 DEFAULT_LOCALE_CASE8 = "bcp47-hyphen";
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: DEFAULT_LOCALE_CASE8,
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, DEFAULT_LOCALE_CASE8));
3064
+ warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE9));
2770
3065
  const sourceLocale = state.config.sourceLocale;
2771
- const sourceToken = resolveLocaleToken(output, sourceLocale, DEFAULT_LOCALE_CASE8);
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, DEFAULT_LOCALE_CASE8);
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 DEFAULT_LOCALE_CASE9 = "bcp47-hyphen";
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: DEFAULT_LOCALE_CASE9,
3146
+ defaultLocaleCase: DEFAULT_LOCALE_CASE10,
2852
3147
  export(state, output) {
2853
3148
  const warnings = [];
2854
- warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE9));
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, DEFAULT_LOCALE_CASE9);
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 readFileSync23, existsSync as existsSync13, readdirSync as readdirSync15, statSync as statSync10, rmSync as rmSync6 } from "fs";
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 inputCost = (usage.inputTokens + usage.cacheCreationInputTokens * CACHE_WRITE_MULTIPLIER + usage.cacheReadInputTokens * CACHE_READ_MULTIPLIER) * pricing.inputPerMTok;
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 resolvePricing(ai) {
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 readFileSync7, existsSync as existsSync7 } from "fs";
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 ? globToRegExp(opts.keyGlob) : null;
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 = readFileSync7(abs);
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
- return { written, errors };
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 readFileSync8, writeFileSync as writeFileSync3, rmSync as rmSync4 } from "fs";
4028
- import { join as join4 } from "path";
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 join4(projectRoot, ".glotfile", "batch.json");
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(readFileSync8(path, "utf8"));
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 = join4(projectRoot, ".glotfile");
4443
+ const dir = join5(projectRoot, ".glotfile");
4045
4444
  mkdirSync4(dir, { recursive: true });
4046
- const gitignore = join4(dir, ".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 readFileSync9 } from "fs";
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 = readFileSync9(path, "utf8").split("\n").filter((l) => l.trim() !== "");
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 readFileSync10, writeFileSync as writeFileSync4, rmSync as rmSync5 } from "fs";
4249
- import { join as join5 } from "path";
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 join5(projectRoot, ".glotfile", "context-batch.json");
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(readFileSync10(path, "utf8"));
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 = join5(projectRoot, ".glotfile");
4664
+ const dir = join6(projectRoot, ".glotfile");
4266
4665
  mkdirSync5(dir, { recursive: true });
4267
- const gitignore = join5(dir, ".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 readFileSync11, statSync as statSync3 } from "fs";
4432
- import { join as join6 } from "path";
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(join6(dir, e)));
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 = [join6(root, "resources", "lang"), join6(root, "lang")].find(safeIsDir);
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(join6(localeRoot, loc)));
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 = join6(root, rel);
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(join6(localeRoot, `${loc}.json`)).size;
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 = join6(root, rel);
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(join6(dir, `${l}.lproj`, "Localizable.strings")));
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) => join6(root, 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(join6(dir, `${loc}.lproj`, "Localizable.strings")).size;
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 : join6(root, rel);
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 = readFileSync11(join6(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
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 = join6(root, "config", "locales");
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 = readFileSync11(join6(localeRoot, file), "utf8");
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 = join6(root, rel);
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(join6(localeRoot, d)).some((f) => f.endsWith(".json"))
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(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join6(localeRoot, loc, f)).size, 0);
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(join6(dir, entry))) continue;
4590
- const sub = join6(dir, entry);
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(join6(sub, "LC_MESSAGES")) || hasPo(sub)) {
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 = join6(root, rel);
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) => join6(root, 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(join6(dir, `${l}.lproj`, "Localizable.stringsdict")));
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 readFileSync12 } from "fs";
4665
- import { join as join7 } from "path";
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(readFileSync12(join7(localeRoot, file), "utf8"));
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 readdirSync5, statSync as statSync4 } from "fs";
4722
- import { join as join8, relative as relative2 } from "path";
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 readdirSync5(dir).filter((e) => statSync4(join8(dir, e)).isDirectory());
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 readdirSync5(d)) {
4744
- const full = join8(d, e);
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 = join8(localeRoot, locale);
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 readdirSync6, readFileSync as readFileSync13 } from "fs";
4805
- import { join as join9 } from "path";
4806
- var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
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 LOCALE_RE3.test(locale) ? locale : null;
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 readdirSync6(localeRoot).sort()) {
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(readFileSync13(join9(localeRoot, file), "utf8"));
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 readdirSync7, readFileSync as readFileSync14, statSync as statSync5 } from "fs";
4867
- import { join as join10 } from "path";
4868
- var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
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 LOCALE_RE4.test(m[1]) ? m[1] : null;
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 readdirSync7(localeRoot).sort()) {
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 = join10(localeRoot, dir, TABLE);
5526
+ const file = join12(localeRoot, dir, TABLE);
4984
5527
  let text;
4985
5528
  try {
4986
5529
  if (!statSync5(file).isFile()) continue;
4987
- text = readFileSync14(file, "utf8");
5530
+ text = readFileSync16(file, "utf8");
4988
5531
  } catch {
4989
5532
  continue;
4990
5533
  }
4991
5534
  locales.push(locale);
4992
- const others = readdirSync7(join10(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
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 readdirSync8, readFileSync as readFileSync15 } from "fs";
5006
- import { join as join11 } from "path";
5007
- var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
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(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/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 = readdirSync8(localeRoot).filter((f) => FILE_RE.test(f)).sort((a, b) => (a === "messages.xlf" ? -1 : 0) - (b === "messages.xlf" ? -1 : 0) || a.localeCompare(b));
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 && !LOCALE_RE5.test(fnameLocale)) continue;
5613
+ if (fnameLocale !== void 0 && !LOCALE_RE6.test(fnameLocale)) continue;
5071
5614
  let xml;
5072
5615
  try {
5073
- xml = readFileSync15(join11(localeRoot, file), "utf8");
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 readdirSync9, readFileSync as readFileSync16 } from "fs";
5119
- import { join as join12 } from "path";
5120
- var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
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 = readdirSync9(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
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: join12(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
5212
- } else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
5213
- for (const sub of [join12(e.name, "LC_MESSAGES"), e.name]) {
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 = readdirSync9(join12(root, sub)).sort();
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: join12(root, sub, f), rel: join12(sub, f), locale: e.name });
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(readFileSync16(file.path, "utf8"));
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 readdirSync10, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
5283
- import { join as join13 } from "path";
5284
- var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
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(readFileSync17(path, "utf8"));
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 readdirSync10(localeRoot).sort()) {
5344
- const full = join13(localeRoot, entry);
5886
+ for (const entry of readdirSync11(localeRoot).sort()) {
5887
+ const full = join15(localeRoot, entry);
5345
5888
  if (safeIsDir2(full)) {
5346
- if (!LOCALE_RE7.test(entry)) continue;
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 readdirSync10(full).sort()) {
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(join13(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
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 (!LOCALE_RE7.test(locale)) continue;
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 readdirSync11, readFileSync as readFileSync18 } from "fs";
5371
- import { join as join14 } from "path";
5372
- var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
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 readdirSync11(localeRoot).sort()) {
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 = readFileSync18(join14(localeRoot, file), "utf8");
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 (!LOCALE_RE8.test(token)) {
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 readdirSync12, readFileSync as readFileSync19, statSync as statSync7 } from "fs";
5611
- import { join as join15 } from "path";
5612
- var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
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 LOCALE_RE9.test(m[1]) ? m[1] : null;
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(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
@@ -5758,20 +6301,20 @@ var appleStringsdict2 = {
5758
6301
  const warnings = [];
5759
6302
  const keys = {};
5760
6303
  const locales = [];
5761
- for (const dir of readdirSync12(localeRoot).sort()) {
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 = join15(localeRoot, dir, TABLE2);
6308
+ const file = join17(localeRoot, dir, TABLE2);
5766
6309
  let text;
5767
6310
  try {
5768
6311
  if (!statSync7(file).isFile()) continue;
5769
- text = readFileSync19(file, "utf8");
6312
+ text = readFileSync21(file, "utf8");
5770
6313
  } catch {
5771
6314
  continue;
5772
6315
  }
5773
6316
  locales.push(locale);
5774
- const others = readdirSync12(join15(localeRoot, dir)).filter(
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 readFileSync20, readdirSync as readdirSync13, rmdirSync, statSync as statSync8, unlinkSync } from "fs";
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 escapeRegExp(s) {
6651
+ function escapeRegExp2(s) {
6104
6652
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6105
6653
  }
6106
6654
  function segmentRegExp(segment) {
6107
- const pattern = escapeRegExp(segment).replaceAll("\\{locale\\}", "(?<locale>[A-Za-z0-9_-]+)").replaceAll("\\{namespace\\}", "[^/]*");
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 = readdirSync13(dir, { withFileTypes: true });
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 = readFileSync20(abs, "utf8");
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 readFileSync21 } from "fs";
6207
- import { homedir } from "os";
6208
- import { join as join16 } from "path";
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 = () => join16(homedir(), ".glotfile", "ui.json");
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(readFileSync21(path, "utf8"));
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 readFileSync22 } from "fs";
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(readFileSync22(path, "utf8"));
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 readdirSync14 } from "fs";
6340
- import { join as join17 } from "path";
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(join17(dir, rel));
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 readdirSync14(join17(dir, "locales")).sort()) {
6910
+ for (const name of readdirSync15(join19(dir, "locales")).sort()) {
6363
6911
  if (!name.endsWith(".json")) continue;
6364
- const s = statSync9(join17(dir, "locales", name));
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 = readFileSync23(nameFile, "utf8").trim();
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 = readdirSync15(dir);
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: e.message }) });
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
- await runLocaleParallel(reqs, provider, {
7159
- // Announce a language the moment a worker picks it up — this is the
7160
- // signal that "something is happening" during the long first LLM call.
7161
- onLocaleStart: (locale) => {
7162
- void stream.writeSSE({ event: "locale-start", data: JSON.stringify({ locale }) });
7163
- },
7164
- onBatchComplete: (done, total, batchResults, locale) => {
7165
- const fresh = load();
7166
- const { written, errors } = applyResults(fresh, reqs, batchResults);
7167
- persist(fresh);
7168
- totalWritten += written;
7169
- allErrors.push(...errors);
7170
- const usage = provider.takeUsage?.();
7171
- appendLog(projectRoot, {
7172
- at: (/* @__PURE__ */ new Date()).toISOString(),
7173
- kind: "translate",
7174
- summary: `Translated ${batchResults.length} item(s)`,
7175
- model: aiCfg.model,
7176
- system,
7177
- items: batchResults.map((r) => {
7178
- const req = reqById.get(r.id);
7179
- 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 };
7180
- }),
7181
- results: batchResults,
7182
- usage,
7183
- estimatedCostUsd: usageCostUsd(usage, aiCfg)
7184
- });
7185
- const ld = (localeDone.get(locale) ?? 0) + batchResults.length;
7186
- localeDone.set(locale, ld);
7187
- console.log(`[translate] ${done}/${total}`);
7188
- void stream.writeSSE({
7189
- event: "progress",
7190
- data: JSON.stringify({ done, total, written: totalWritten, errors, locale, localeDone: ld, localeTotal: localeTotals.get(locale) ?? 0 })
7191
- });
7192
- },
7193
- onLocaleDone: (locale) => {
7194
- void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
7195
- },
7196
- // Record the raw reply so an unparseable model response is diagnosable
7197
- // from the activity log instead of vanishing into per-item errors.
7198
- onMalformedReply: (raw, batchSize, locale) => {
7199
- console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
7200
- appendLog(projectRoot, {
7201
- at: (/* @__PURE__ */ new Date()).toISOString(),
7202
- kind: "translate",
7203
- summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
7204
- model: aiCfg.model,
7205
- locale,
7206
- raw
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
- }, aiCfg.concurrency, signal, aiCfg.batchSize);
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: e.message }, 400);
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
- const results = await runLocaleParallel(toTranslate, provider, {
7241
- onMalformedReply: (raw, batchSize, locale) => {
7242
- console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
7243
- appendLog(projectRoot, {
7244
- at: (/* @__PURE__ */ new Date()).toISOString(),
7245
- kind: "translate",
7246
- summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
7247
- model: aiCfg.model,
7248
- locale,
7249
- raw
7250
- });
7251
- }
7252
- }, aiCfg.concurrency, void 0, aiCfg.batchSize);
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 = join18(here, "..", "ui");
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(join18(root, "index.html"));
8392
+ const index = await readFileResponse(join20(root, "index.html"));
7693
8393
  if (index) return index;
7694
8394
  return c.notFound();
7695
8395
  });