glotfile 1.1.0 → 1.1.2

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 join20, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
5
+ import { dirname as dirname4, join as join21, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
6
6
  import { readFile, stat } from "fs/promises";
7
7
  import { createServer } from "net";
8
8
  import open from "open";
@@ -265,9 +265,6 @@ function validate(raw) {
265
265
  if (entry.contextSource !== void 0 && entry.contextSource !== "ai") {
266
266
  fail(`key "${key}" contextSource must be "ai" if present`);
267
267
  }
268
- if (entry.contextAt !== void 0 && typeof entry.contextAt !== "string") {
269
- fail(`key "${key}" contextAt must be a string if present`);
270
- }
271
268
  }
272
269
  if (raw.glossary !== void 0 && !Array.isArray(raw.glossary)) fail("glossary must be an array");
273
270
  if (raw.glossarySuggestions !== void 0 && !Array.isArray(raw.glossarySuggestions)) fail("glossarySuggestions must be an array");
@@ -713,7 +710,6 @@ function setMetadata(state, key, partial) {
713
710
  delete safe.values;
714
711
  if ("context" in safe) {
715
712
  delete entry.contextSource;
716
- delete entry.contextAt;
717
713
  }
718
714
  Object.assign(entry, safe);
719
715
  if ("context" in safe && !entry.context) delete entry.context;
@@ -1730,7 +1726,7 @@ var CONTEXT_BATCH_SCHEMA = {
1730
1726
  required: ["items"],
1731
1727
  additionalProperties: false
1732
1728
  };
1733
- function applyContext(state, reqs, results, clock = systemClock, force = false) {
1729
+ function applyContext(state, reqs, results, force = false) {
1734
1730
  const byId = new Map(reqs.map((r) => [r.id, r]));
1735
1731
  let written = 0;
1736
1732
  const errors = [];
@@ -1754,7 +1750,6 @@ function applyContext(state, reqs, results, clock = systemClock, force = false)
1754
1750
  if (!entry || entry.context && !force) continue;
1755
1751
  entry.context = context;
1756
1752
  entry.contextSource = "ai";
1757
- entry.contextAt = clock();
1758
1753
  written++;
1759
1754
  }
1760
1755
  return { written, errors };
@@ -3251,7 +3246,7 @@ function checkOutputs(state, root) {
3251
3246
  }
3252
3247
 
3253
3248
  // src/server/api.ts
3254
- import { readFileSync as readFileSync25, existsSync as existsSync13, readdirSync as readdirSync16, statSync as statSync10, rmSync as rmSync6 } from "fs";
3249
+ import { readFileSync as readFileSync26, existsSync as existsSync14, readdirSync as readdirSync16, statSync as statSync10, rmSync as rmSync7 } from "fs";
3255
3250
  import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
3256
3251
 
3257
3252
  // src/server/ai/anthropic.ts
@@ -3987,7 +3982,7 @@ var BedrockProvider = class {
3987
3982
  if (res.stopReason === "max_tokens") {
3988
3983
  throw new MalformedReplyError(text || JSON.stringify(tool?.input ?? {}));
3989
3984
  }
3990
- if (tool?.input?.items) return tool.input.items;
3985
+ if (Array.isArray(tool?.input?.items)) return tool.input.items;
3991
3986
  return parseReplyItems(text);
3992
3987
  }
3993
3988
  };
@@ -4751,7 +4746,7 @@ async function applyContextBatchResults(load, persist, provider, pending, projec
4751
4746
  if (retryUsage) addUsage(usage, retryUsage);
4752
4747
  }
4753
4748
  const fresh = load();
4754
- const { written, errors: applyErrors } = applyContext(fresh, applied, items, void 0, pending.force);
4749
+ const { written, errors: applyErrors } = applyContext(fresh, applied, items, pending.force);
4755
4750
  errors.push(...applyErrors);
4756
4751
  persist(fresh);
4757
4752
  clearPendingContextBatch(projectRoot);
@@ -4770,6 +4765,127 @@ async function applyContextBatchResults(load, persist, provider, pending, projec
4770
4765
  return { written, errors, retried: retryChunks.length };
4771
4766
  }
4772
4767
 
4768
+ // src/server/ai/pending-glossary-batch.ts
4769
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync5, rmSync as rmSync6 } from "fs";
4770
+ import { join as join7 } from "path";
4771
+ function pendingGlossaryBatchPath(projectRoot) {
4772
+ return join7(projectRoot, ".glotfile", "glossary-suggest-batch.json");
4773
+ }
4774
+ function loadPendingGlossaryBatch(projectRoot) {
4775
+ const path = pendingGlossaryBatchPath(projectRoot);
4776
+ if (!existsSync11(path)) return void 0;
4777
+ try {
4778
+ const parsed = JSON.parse(readFileSync12(path, "utf8"));
4779
+ if (parsed?.version !== 1) return void 0;
4780
+ return parsed;
4781
+ } catch {
4782
+ return void 0;
4783
+ }
4784
+ }
4785
+ function savePendingGlossaryBatch(projectRoot, pending) {
4786
+ const dir = join7(projectRoot, ".glotfile");
4787
+ mkdirSync6(dir, { recursive: true });
4788
+ const gitignore = join7(dir, ".gitignore");
4789
+ if (!existsSync11(gitignore)) writeFileSync5(gitignore, "*\n");
4790
+ writeFileSync5(pendingGlossaryBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
4791
+ }
4792
+ function clearPendingGlossaryBatch(projectRoot) {
4793
+ rmSync6(pendingGlossaryBatchPath(projectRoot), { force: true });
4794
+ }
4795
+
4796
+ // src/server/ai/glossary-batch-run.ts
4797
+ function completionRequestFor2(chunk2, knownTerms) {
4798
+ return {
4799
+ system: buildGlossarySuggestSystemPrompt(),
4800
+ content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunk2, knownTerms) }],
4801
+ schema: GLOSSARY_SUGGEST_SCHEMA
4802
+ };
4803
+ }
4804
+ async function submitGlossarySuggestBatch(provider, sources, knownTerms, batchSize, model, projectRoot) {
4805
+ if (loadPendingGlossaryBatch(projectRoot)) {
4806
+ throw new Error("A glossary suggestion batch is already pending. Apply or cancel it first.");
4807
+ }
4808
+ const chunks = [];
4809
+ const size = Math.max(1, batchSize);
4810
+ for (let i = 0; i < sources.length; i += size) chunks.push(sources.slice(i, i + size));
4811
+ const jobs = chunks.map((chunk2, i) => ({ customId: `gloss_${i}`, chunk: chunk2 }));
4812
+ const batchId = await provider.submitCompletionBatch(
4813
+ jobs.map((j) => ({ customId: j.customId, request: completionRequestFor2(j.chunk, knownTerms) }))
4814
+ );
4815
+ const pending = {
4816
+ version: 1,
4817
+ // Only Anthropic implements completion batches today.
4818
+ provider: "anthropic",
4819
+ model,
4820
+ batchId,
4821
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4822
+ total: sources.length,
4823
+ knownTerms,
4824
+ jobs: jobs.map((j) => ({
4825
+ customId: j.customId,
4826
+ requests: j.chunk
4827
+ }))
4828
+ };
4829
+ savePendingGlossaryBatch(projectRoot, pending);
4830
+ return pending;
4831
+ }
4832
+ async function applyGlossarySuggestBatchResults(load, persist, provider, pending, projectRoot, ai) {
4833
+ provider.takeUsage?.();
4834
+ const outcomes = await provider.completionBatchResults(pending.batchId);
4835
+ const batchUsage = provider.takeUsage?.();
4836
+ const allTerms = [];
4837
+ const errors = [];
4838
+ const jobFailures = [];
4839
+ const retryChunks = [];
4840
+ for (const job of pending.jobs) {
4841
+ const outcome = outcomes.get(job.customId);
4842
+ if (outcome?.type === "json") {
4843
+ const batch = outcome.value;
4844
+ allTerms.push(...batch.terms ?? []);
4845
+ continue;
4846
+ }
4847
+ if (!outcome) jobFailures.push({ customId: job.customId, locale: "", type: "missing" });
4848
+ else if (outcome.type === "malformed") jobFailures.push({ customId: job.customId, locale: "", type: "malformed", raw: outcome.raw });
4849
+ else jobFailures.push({ customId: job.customId, locale: "", type: "failed", error: outcome.error });
4850
+ retryChunks.push(job.requests);
4851
+ }
4852
+ for (const chunk2 of retryChunks) {
4853
+ try {
4854
+ const raw = await provider.complete(completionRequestFor2(chunk2, pending.knownTerms));
4855
+ const batch = raw;
4856
+ allTerms.push(...batch.terms ?? []);
4857
+ } catch (e) {
4858
+ errors.push({ error: e.message });
4859
+ }
4860
+ }
4861
+ const retryUsage = provider.takeUsage?.();
4862
+ const pricing = resolvePricing({ ...ai, model: pending.model });
4863
+ let estimatedCostUsd;
4864
+ if (pricing && (batchUsage || retryUsage)) {
4865
+ estimatedCostUsd = (batchUsage ? estimateUsageCostUsd(batchUsage, pricing, BATCH_PRICE_MULTIPLIER) : 0) + (retryUsage ? estimateUsageCostUsd(retryUsage, pricing) : 0);
4866
+ }
4867
+ let usage;
4868
+ if (batchUsage || retryUsage) {
4869
+ usage = batchUsage ?? { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
4870
+ if (retryUsage) addUsage(usage, retryUsage);
4871
+ }
4872
+ const fresh = load();
4873
+ const added = mergeGlossarySuggestions(fresh, dedupeTerms(allTerms));
4874
+ persist(fresh);
4875
+ clearPendingGlossaryBatch(projectRoot);
4876
+ const costSuffix = estimatedCostUsd !== void 0 ? ` (~$${estimatedCostUsd.toFixed(2)})` : "";
4877
+ appendLog(projectRoot, {
4878
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4879
+ kind: "glossary",
4880
+ summary: `Applied glossary suggestion batch ${pending.batchId}: ${added.length} new term(s), ${errors.length} error(s), ${retryChunks.length} job(s) retried${costSuffix}`,
4881
+ model: pending.model,
4882
+ jobFailures: jobFailures.length ? jobFailures : void 0,
4883
+ usage,
4884
+ estimatedCostUsd
4885
+ });
4886
+ return { added: added.length, errors, retried: retryChunks.length };
4887
+ }
4888
+
4773
4889
  // src/server/ai/estimate.ts
4774
4890
  var CJK_RE = /[ -鿿가-힯豈-﫿]/g;
4775
4891
  function estimateTokens(text) {
@@ -4844,6 +4960,28 @@ function estimateContext(targets, ai) {
4844
4960
  estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
4845
4961
  };
4846
4962
  }
4963
+ var TERM_REPLY_TOKENS = 24;
4964
+ var TERM_YIELD = 0.15;
4965
+ function estimateGlossarySuggest(sources, knownTerms, ai) {
4966
+ const batchSize = Math.max(1, ai.contextBatchSize ?? ai.batchSize ?? 10);
4967
+ const batches = chunk(sources, batchSize);
4968
+ const system = buildGlossarySuggestSystemPrompt();
4969
+ let inputTokens = 0;
4970
+ let outputTokens = 0;
4971
+ for (const batch of batches) {
4972
+ inputTokens += estimateTokens(system) + estimateTokens(buildGlossarySuggestBatchPrompt(batch, knownTerms));
4973
+ outputTokens += Math.ceil(batch.length * TERM_YIELD) * TERM_REPLY_TOKENS;
4974
+ }
4975
+ const pricing = resolvePricing(ai);
4976
+ return {
4977
+ sources: sources.length,
4978
+ batches: batches.length,
4979
+ inputTokens,
4980
+ outputTokens,
4981
+ pricing,
4982
+ estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
4983
+ };
4984
+ }
4847
4985
 
4848
4986
  // src/server/ai/price-fetch.ts
4849
4987
  var MODELS_DEV_URL = "https://models.dev/api.json";
@@ -4897,8 +5035,8 @@ async function refreshPrices(opts = {}) {
4897
5035
  import { relative as relative3 } from "path";
4898
5036
 
4899
5037
  // src/server/import/detect.ts
4900
- import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
4901
- import { join as join7 } from "path";
5038
+ import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
5039
+ import { join as join8 } from "path";
4902
5040
  var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4903
5041
  var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
4904
5042
  function safeIsDir(p) {
@@ -4909,7 +5047,7 @@ function safeIsDir(p) {
4909
5047
  }
4910
5048
  }
4911
5049
  function listDirs(dir) {
4912
- return readdirSync3(dir).filter((e) => safeIsDir(join7(dir, e)));
5050
+ return readdirSync3(dir).filter((e) => safeIsDir(join8(dir, e)));
4913
5051
  }
4914
5052
  function fileCount(dir) {
4915
5053
  try {
@@ -4923,23 +5061,23 @@ function pickSource(locales, sizeOf) {
4923
5061
  return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
4924
5062
  }
4925
5063
  function detectLaravel(root) {
4926
- const localeRoot = [join7(root, "resources", "lang"), join7(root, "lang")].find(safeIsDir);
5064
+ const localeRoot = [join8(root, "resources", "lang"), join8(root, "lang")].find(safeIsDir);
4927
5065
  if (!localeRoot) return null;
4928
5066
  const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
4929
5067
  if (locales.length === 0) return null;
4930
- const sourceLocale = pickSource(locales, (loc) => fileCount(join7(localeRoot, loc)));
5068
+ const sourceLocale = pickSource(locales, (loc) => fileCount(join8(localeRoot, loc)));
4931
5069
  return { format: "laravel-php", localeRoot, locales, sourceLocale };
4932
5070
  }
4933
5071
  function detectVue(root, forced = false) {
4934
5072
  for (const rel of VUE_DIR_CANDIDATES) {
4935
- const localeRoot = join7(root, rel);
5073
+ const localeRoot = join8(root, rel);
4936
5074
  if (!safeIsDir(localeRoot)) continue;
4937
5075
  const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
4938
5076
  const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
4939
5077
  if (enough) {
4940
5078
  const sourceLocale = pickSource(locales, (loc) => {
4941
5079
  try {
4942
- return statSync3(join7(localeRoot, `${loc}.json`)).size;
5080
+ return statSync3(join8(localeRoot, `${loc}.json`)).size;
4943
5081
  } catch {
4944
5082
  return 0;
4945
5083
  }
@@ -4953,9 +5091,9 @@ var NEXT_INTL_CONFIG_CANDIDATES = ["src/i18n/request.ts", "i18n/request.ts", "sr
4953
5091
  var NEXT_INTL_ROUTING_CANDIDATES = ["src/i18n/routing.ts", "i18n/routing.ts", "src/i18n/routing.js", "i18n/routing.js"];
4954
5092
  var NEXT_INTL_DIR_CANDIDATES = ["messages", "src/messages", "locales", "src/locales", "src/i18n/messages"];
4955
5093
  function hasNextIntlSignal(root) {
4956
- if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync11(join7(root, rel)))) return true;
5094
+ if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync12(join8(root, rel)))) return true;
4957
5095
  try {
4958
- const pkg = JSON.parse(readFileSync12(join7(root, "package.json"), "utf8"));
5096
+ const pkg = JSON.parse(readFileSync13(join8(root, "package.json"), "utf8"));
4959
5097
  if (pkg.dependencies?.["next-intl"] || pkg.devDependencies?.["next-intl"]) return true;
4960
5098
  } catch {
4961
5099
  }
@@ -4964,7 +5102,7 @@ function hasNextIntlSignal(root) {
4964
5102
  function nextIntlDefaultLocale(root) {
4965
5103
  for (const rel of NEXT_INTL_ROUTING_CANDIDATES) {
4966
5104
  try {
4967
- const m = readFileSync12(join7(root, rel), "utf8").match(/defaultLocale\s*:\s*['"]([^'"]+)['"]/);
5105
+ const m = readFileSync13(join8(root, rel), "utf8").match(/defaultLocale\s*:\s*['"]([^'"]+)['"]/);
4968
5106
  if (m) return m[1];
4969
5107
  } catch {
4970
5108
  }
@@ -4974,14 +5112,14 @@ function nextIntlDefaultLocale(root) {
4974
5112
  function detectNextIntl(root, forced = false) {
4975
5113
  if (!forced && !hasNextIntlSignal(root)) return null;
4976
5114
  for (const rel of NEXT_INTL_DIR_CANDIDATES) {
4977
- const localeRoot = join7(root, rel);
5115
+ const localeRoot = join8(root, rel);
4978
5116
  if (!safeIsDir(localeRoot)) continue;
4979
5117
  const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
4980
5118
  if (locales.length === 0) continue;
4981
5119
  const def = nextIntlDefaultLocale(root);
4982
5120
  const sourceLocale = def && locales.includes(def) ? def : pickSource(locales, (loc) => {
4983
5121
  try {
4984
- return statSync3(join7(localeRoot, `${loc}.json`)).size;
5122
+ return statSync3(join8(localeRoot, `${loc}.json`)).size;
4985
5123
  } catch {
4986
5124
  return 0;
4987
5125
  }
@@ -4992,7 +5130,7 @@ function detectNextIntl(root, forced = false) {
4992
5130
  }
4993
5131
  function detectArb(root) {
4994
5132
  for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
4995
- const localeRoot = join7(root, rel);
5133
+ const localeRoot = join8(root, rel);
4996
5134
  if (!safeIsDir(localeRoot)) continue;
4997
5135
  const locales = readdirSync3(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
4998
5136
  if (locales.length >= 1) {
@@ -5002,10 +5140,10 @@ function detectArb(root) {
5002
5140
  return null;
5003
5141
  }
5004
5142
  function lprojLocales(dir) {
5005
- return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join7(dir, `${l}.lproj`, "Localizable.strings")));
5143
+ return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync12(join8(dir, `${l}.lproj`, "Localizable.strings")));
5006
5144
  }
5007
5145
  function detectApple(root) {
5008
- const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
5146
+ const candidates = [root, ...listDirs(root).map((d) => join8(root, d))];
5009
5147
  let best = null;
5010
5148
  for (const dir of candidates) {
5011
5149
  const locales = lprojLocales(dir);
@@ -5017,7 +5155,7 @@ function detectApple(root) {
5017
5155
  locales,
5018
5156
  sourceLocale: pickSource(locales, (loc) => {
5019
5157
  try {
5020
- return statSync3(join7(dir, `${loc}.lproj`, "Localizable.strings")).size;
5158
+ return statSync3(join8(dir, `${loc}.lproj`, "Localizable.strings")).size;
5021
5159
  } catch {
5022
5160
  return 0;
5023
5161
  }
@@ -5030,7 +5168,7 @@ function detectApple(root) {
5030
5168
  var ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
5031
5169
  function detectAngularXliff(root) {
5032
5170
  for (const rel of ANGULAR_DIR_CANDIDATES) {
5033
- const localeRoot = rel === "." ? root : join7(root, rel);
5171
+ const localeRoot = rel === "." ? root : join8(root, rel);
5034
5172
  if (!safeIsDir(localeRoot)) continue;
5035
5173
  const files = readdirSync3(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
5036
5174
  if (files.length === 0) continue;
@@ -5038,7 +5176,7 @@ function detectAngularXliff(root) {
5038
5176
  const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
5039
5177
  let sourceLocale;
5040
5178
  try {
5041
- sourceLocale = readFileSync12(join7(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
5179
+ sourceLocale = readFileSync13(join8(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
5042
5180
  } catch {
5043
5181
  }
5044
5182
  if (!sourceLocale && locales.length === 0) continue;
@@ -5049,14 +5187,14 @@ function detectAngularXliff(root) {
5049
5187
  return null;
5050
5188
  }
5051
5189
  function detectRails(root) {
5052
- const localeRoot = join7(root, "config", "locales");
5190
+ const localeRoot = join8(root, "config", "locales");
5053
5191
  if (!safeIsDir(localeRoot)) return null;
5054
5192
  const locales = [];
5055
5193
  for (const file of readdirSync3(localeRoot).sort()) {
5056
5194
  if (!/\.ya?ml$/.test(file)) continue;
5057
5195
  let text;
5058
5196
  try {
5059
- text = readFileSync12(join7(localeRoot, file), "utf8");
5197
+ text = readFileSync13(join8(localeRoot, file), "utf8");
5060
5198
  } catch {
5061
5199
  continue;
5062
5200
  }
@@ -5071,15 +5209,15 @@ function detectRails(root) {
5071
5209
  var I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
5072
5210
  function detectI18next(root) {
5073
5211
  for (const rel of I18NEXT_DIR_CANDIDATES) {
5074
- const localeRoot = join7(root, rel);
5212
+ const localeRoot = join8(root, rel);
5075
5213
  if (!safeIsDir(localeRoot)) continue;
5076
5214
  const locales = listDirs(localeRoot).filter(
5077
- (d) => LOCALE_RE.test(d) && readdirSync3(join7(localeRoot, d)).some((f) => f.endsWith(".json"))
5215
+ (d) => LOCALE_RE.test(d) && readdirSync3(join8(localeRoot, d)).some((f) => f.endsWith(".json"))
5078
5216
  );
5079
5217
  if (locales.length === 0) continue;
5080
5218
  const sourceLocale = pickSource(locales, (loc) => {
5081
5219
  try {
5082
- return readdirSync3(join7(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join7(localeRoot, loc, f)).size, 0);
5220
+ return readdirSync3(join8(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join8(localeRoot, loc, f)).size, 0);
5083
5221
  } catch {
5084
5222
  return 0;
5085
5223
  }
@@ -5096,8 +5234,8 @@ function gettextLocales(dir) {
5096
5234
  if (!locales.includes(flat)) locales.push(flat);
5097
5235
  continue;
5098
5236
  }
5099
- if (!LOCALE_RE.test(entry) || !safeIsDir(join7(dir, entry))) continue;
5100
- const sub = join7(dir, entry);
5237
+ if (!LOCALE_RE.test(entry) || !safeIsDir(join8(dir, entry))) continue;
5238
+ const sub = join8(dir, entry);
5101
5239
  const hasPo = (d) => {
5102
5240
  try {
5103
5241
  return readdirSync3(d).some((f) => f.endsWith(".po"));
@@ -5105,7 +5243,7 @@ function gettextLocales(dir) {
5105
5243
  return false;
5106
5244
  }
5107
5245
  };
5108
- if (hasPo(join7(sub, "LC_MESSAGES")) || hasPo(sub)) {
5246
+ if (hasPo(join8(sub, "LC_MESSAGES")) || hasPo(sub)) {
5109
5247
  if (!locales.includes(entry)) locales.push(entry);
5110
5248
  }
5111
5249
  }
@@ -5114,7 +5252,7 @@ function gettextLocales(dir) {
5114
5252
  var GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
5115
5253
  function detectGettext(root) {
5116
5254
  for (const rel of GETTEXT_DIR_CANDIDATES) {
5117
- const localeRoot = join7(root, rel);
5255
+ const localeRoot = join8(root, rel);
5118
5256
  if (!safeIsDir(localeRoot)) continue;
5119
5257
  const locales = gettextLocales(localeRoot);
5120
5258
  if (locales.length === 0) continue;
@@ -5123,10 +5261,10 @@ function detectGettext(root) {
5123
5261
  return null;
5124
5262
  }
5125
5263
  function detectAppleStringsdict(root) {
5126
- const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
5264
+ const candidates = [root, ...listDirs(root).map((d) => join8(root, d))];
5127
5265
  let best = null;
5128
5266
  for (const dir of candidates) {
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")));
5267
+ const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync12(join8(dir, `${l}.lproj`, "Localizable.stringsdict")));
5130
5268
  if (locales.length === 0) continue;
5131
5269
  if (!best || locales.length > best.locales.length) {
5132
5270
  best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
@@ -5159,7 +5297,7 @@ var BY_FORMAT = {
5159
5297
  "apple-stringsdict": detectAppleStringsdict
5160
5298
  };
5161
5299
  function detect(root, formatOverride) {
5162
- if (!existsSync11(root)) return null;
5300
+ if (!existsSync12(root)) return null;
5163
5301
  if (formatOverride) {
5164
5302
  const fn = BY_FORMAT[formatOverride];
5165
5303
  if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
@@ -5173,8 +5311,8 @@ function detect(root, formatOverride) {
5173
5311
  }
5174
5312
 
5175
5313
  // src/server/import/parsers/vue-i18n-json.ts
5176
- import { readdirSync as readdirSync4, readFileSync as readFileSync13 } from "fs";
5177
- import { join as join8 } from "path";
5314
+ import { readdirSync as readdirSync4, readFileSync as readFileSync14 } from "fs";
5315
+ import { join as join9 } from "path";
5178
5316
 
5179
5317
  // src/server/import/flatten.ts
5180
5318
  function flattenObject(value, prefix, warnings) {
@@ -5215,7 +5353,7 @@ var vueI18nJson2 = {
5215
5353
  if (opts?.locales && !opts.locales.includes(locale)) continue;
5216
5354
  let data;
5217
5355
  try {
5218
- data = JSON.parse(readFileSync13(join8(localeRoot, file), "utf8"));
5356
+ data = JSON.parse(readFileSync14(join9(localeRoot, file), "utf8"));
5219
5357
  } catch (e) {
5220
5358
  warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
5221
5359
  continue;
@@ -5230,8 +5368,8 @@ var vueI18nJson2 = {
5230
5368
  };
5231
5369
 
5232
5370
  // 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";
5371
+ import { readdirSync as readdirSync5, readFileSync as readFileSync15 } from "fs";
5372
+ import { join as join10 } from "path";
5235
5373
  var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
5236
5374
  var nextIntlJson2 = {
5237
5375
  name: "next-intl-json",
@@ -5246,7 +5384,7 @@ var nextIntlJson2 = {
5246
5384
  if (opts?.locales && !opts.locales.includes(locale)) continue;
5247
5385
  let data;
5248
5386
  try {
5249
- data = JSON.parse(readFileSync14(join9(localeRoot, file), "utf8"));
5387
+ data = JSON.parse(readFileSync15(join10(localeRoot, file), "utf8"));
5250
5388
  } catch (e) {
5251
5389
  warnings.push(`next-intl-json: failed to parse ${file}: ${e.message}`);
5252
5390
  continue;
@@ -5262,7 +5400,7 @@ var nextIntlJson2 = {
5262
5400
 
5263
5401
  // src/server/import/parsers/laravel-php.ts
5264
5402
  import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
5265
- import { join as join10, relative as relative2 } from "path";
5403
+ import { join as join11, relative as relative2 } from "path";
5266
5404
  import { execFileSync } from "child_process";
5267
5405
 
5268
5406
  // src/server/import/placeholders.ts
@@ -5278,13 +5416,13 @@ function railsToCanonical(value) {
5278
5416
 
5279
5417
  // src/server/import/parsers/laravel-php.ts
5280
5418
  function listDirs2(dir) {
5281
- return readdirSync6(dir).filter((e) => statSync4(join10(dir, e)).isDirectory());
5419
+ return readdirSync6(dir).filter((e) => statSync4(join11(dir, e)).isDirectory());
5282
5420
  }
5283
5421
  function listPhpFiles(dir) {
5284
5422
  const out = [];
5285
5423
  const walk = (d) => {
5286
5424
  for (const e of readdirSync6(d)) {
5287
- const full = join10(d, e);
5425
+ const full = join11(d, e);
5288
5426
  if (statSync4(full).isDirectory()) walk(full);
5289
5427
  else if (e.endsWith(".php")) out.push(full);
5290
5428
  }
@@ -5321,7 +5459,7 @@ var laravelPhp2 = {
5321
5459
  for (const locale of listDirs2(localeRoot).sort()) {
5322
5460
  if (locale === "vendor") continue;
5323
5461
  if (opts?.locales && !opts.locales.includes(locale)) continue;
5324
- const localeDir = join10(localeRoot, locale);
5462
+ const localeDir = join11(localeRoot, locale);
5325
5463
  locales.push(locale);
5326
5464
  for (const file of listPhpFiles(localeDir)) {
5327
5465
  const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
@@ -5344,8 +5482,8 @@ var laravelPhp2 = {
5344
5482
  };
5345
5483
 
5346
5484
  // src/server/import/parsers/flutter-arb.ts
5347
- import { readdirSync as readdirSync7, readFileSync as readFileSync15 } from "fs";
5348
- import { join as join11 } from "path";
5485
+ import { readdirSync as readdirSync7, readFileSync as readFileSync16 } from "fs";
5486
+ import { join as join12 } from "path";
5349
5487
  var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
5350
5488
  function localeFromArbName(file) {
5351
5489
  const m = file.match(/^(.+)\.arb$/);
@@ -5381,7 +5519,7 @@ var flutterArb2 = {
5381
5519
  if (opts?.locales && !opts.locales.includes(locale)) continue;
5382
5520
  let data;
5383
5521
  try {
5384
- data = JSON.parse(readFileSync15(join11(localeRoot, file), "utf8"));
5522
+ data = JSON.parse(readFileSync16(join12(localeRoot, file), "utf8"));
5385
5523
  } catch (e) {
5386
5524
  warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
5387
5525
  continue;
@@ -5406,8 +5544,8 @@ var flutterArb2 = {
5406
5544
  };
5407
5545
 
5408
5546
  // src/server/import/parsers/apple-strings.ts
5409
- import { readdirSync as readdirSync8, readFileSync as readFileSync16, statSync as statSync5 } from "fs";
5410
- import { join as join12 } from "path";
5547
+ import { readdirSync as readdirSync8, readFileSync as readFileSync17, statSync as statSync5 } from "fs";
5548
+ import { join as join13 } from "path";
5411
5549
  var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
5412
5550
  var TABLE = "Localizable.strings";
5413
5551
  function localeFromLproj(dir) {
@@ -5523,16 +5661,16 @@ var appleStrings2 = {
5523
5661
  const locale = localeFromLproj(dir);
5524
5662
  if (!locale) continue;
5525
5663
  if (opts?.locales && !opts.locales.includes(locale)) continue;
5526
- const file = join12(localeRoot, dir, TABLE);
5664
+ const file = join13(localeRoot, dir, TABLE);
5527
5665
  let text;
5528
5666
  try {
5529
5667
  if (!statSync5(file).isFile()) continue;
5530
- text = readFileSync16(file, "utf8");
5668
+ text = readFileSync17(file, "utf8");
5531
5669
  } catch {
5532
5670
  continue;
5533
5671
  }
5534
5672
  locales.push(locale);
5535
- const others = readdirSync8(join12(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
5673
+ const others = readdirSync8(join13(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
5536
5674
  if (others.length) {
5537
5675
  warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
5538
5676
  }
@@ -5545,8 +5683,8 @@ var appleStrings2 = {
5545
5683
  };
5546
5684
 
5547
5685
  // src/server/import/parsers/angular-xliff.ts
5548
- import { readdirSync as readdirSync9, readFileSync as readFileSync17 } from "fs";
5549
- import { join as join13 } from "path";
5686
+ import { readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
5687
+ import { join as join14 } from "path";
5550
5688
  var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
5551
5689
  var FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
5552
5690
  function decodeEntities(s) {
@@ -5613,7 +5751,7 @@ var angularXliff2 = {
5613
5751
  if (fnameLocale !== void 0 && !LOCALE_RE6.test(fnameLocale)) continue;
5614
5752
  let xml;
5615
5753
  try {
5616
- xml = readFileSync17(join13(localeRoot, file), "utf8");
5754
+ xml = readFileSync18(join14(localeRoot, file), "utf8");
5617
5755
  } catch (e) {
5618
5756
  warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
5619
5757
  continue;
@@ -5658,8 +5796,8 @@ var angularXliff2 = {
5658
5796
  };
5659
5797
 
5660
5798
  // src/server/import/parsers/gettext-po.ts
5661
- import { readdirSync as readdirSync10, readFileSync as readFileSync18 } from "fs";
5662
- import { join as join14 } from "path";
5799
+ import { readdirSync as readdirSync10, readFileSync as readFileSync19 } from "fs";
5800
+ import { join as join15 } from "path";
5663
5801
  var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
5664
5802
  var DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
5665
5803
  var CONT_RE = /^[ \t]*"(.*)"\s*$/;
@@ -5751,17 +5889,17 @@ function discoverPoFiles(root) {
5751
5889
  for (const e of entries) {
5752
5890
  if (e.isFile() && e.name.endsWith(".po")) {
5753
5891
  const base = e.name.slice(0, -3);
5754
- found.push({ path: join14(root, e.name), rel: e.name, locale: LOCALE_RE7.test(base) ? base : null });
5892
+ found.push({ path: join15(root, e.name), rel: e.name, locale: LOCALE_RE7.test(base) ? base : null });
5755
5893
  } else if (e.isDirectory() && LOCALE_RE7.test(e.name)) {
5756
- for (const sub of [join14(e.name, "LC_MESSAGES"), e.name]) {
5894
+ for (const sub of [join15(e.name, "LC_MESSAGES"), e.name]) {
5757
5895
  let names;
5758
5896
  try {
5759
- names = readdirSync10(join14(root, sub)).sort();
5897
+ names = readdirSync10(join15(root, sub)).sort();
5760
5898
  } catch {
5761
5899
  continue;
5762
5900
  }
5763
5901
  for (const f of names) {
5764
- if (f.endsWith(".po")) found.push({ path: join14(root, sub, f), rel: join14(sub, f), locale: e.name });
5902
+ if (f.endsWith(".po")) found.push({ path: join15(root, sub, f), rel: join15(sub, f), locale: e.name });
5765
5903
  }
5766
5904
  }
5767
5905
  }
@@ -5777,7 +5915,7 @@ var gettextPo2 = {
5777
5915
  for (const file of discoverPoFiles(localeRoot)) {
5778
5916
  let entries;
5779
5917
  try {
5780
- entries = parseEntries(readFileSync18(file.path, "utf8"));
5918
+ entries = parseEntries(readFileSync19(file.path, "utf8"));
5781
5919
  } catch (e) {
5782
5920
  warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
5783
5921
  continue;
@@ -5822,8 +5960,8 @@ var gettextPo2 = {
5822
5960
  };
5823
5961
 
5824
5962
  // src/server/import/parsers/i18next-json.ts
5825
- import { readdirSync as readdirSync11, readFileSync as readFileSync19, statSync as statSync6 } from "fs";
5826
- import { join as join15 } from "path";
5963
+ import { readdirSync as readdirSync11, readFileSync as readFileSync20, statSync as statSync6 } from "fs";
5964
+ import { join as join16 } from "path";
5827
5965
  var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
5828
5966
  var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
5829
5967
  var PLURAL_ARG = "count";
@@ -5842,7 +5980,7 @@ function fromI18next(value) {
5842
5980
  function ingestFile(path, label, prefix, locale, keys, warnings) {
5843
5981
  let data;
5844
5982
  try {
5845
- data = JSON.parse(readFileSync19(path, "utf8"));
5983
+ data = JSON.parse(readFileSync20(path, "utf8"));
5846
5984
  } catch (e) {
5847
5985
  warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
5848
5986
  return false;
@@ -5884,7 +6022,7 @@ var i18nextJson2 = {
5884
6022
  const keys = {};
5885
6023
  const locales = [];
5886
6024
  for (const entry of readdirSync11(localeRoot).sort()) {
5887
- const full = join15(localeRoot, entry);
6025
+ const full = join16(localeRoot, entry);
5888
6026
  if (safeIsDir2(full)) {
5889
6027
  if (!LOCALE_RE8.test(entry)) continue;
5890
6028
  if (opts?.locales && !opts.locales.includes(entry)) continue;
@@ -5893,7 +6031,7 @@ var i18nextJson2 = {
5893
6031
  if (!file.endsWith(".json")) continue;
5894
6032
  const ns = file.slice(0, -".json".length);
5895
6033
  const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
5896
- if (ingestFile(join15(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
6034
+ if (ingestFile(join16(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
5897
6035
  }
5898
6036
  if (any && !locales.includes(entry)) locales.push(entry);
5899
6037
  } else if (entry.endsWith(".json")) {
@@ -5910,8 +6048,8 @@ var i18nextJson2 = {
5910
6048
  };
5911
6049
 
5912
6050
  // src/server/import/parsers/rails-yaml.ts
5913
- import { readdirSync as readdirSync12, readFileSync as readFileSync20 } from "fs";
5914
- import { join as join16 } from "path";
6051
+ import { readdirSync as readdirSync12, readFileSync as readFileSync21 } from "fs";
6052
+ import { join as join17 } from "path";
5915
6053
  var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
5916
6054
  var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
5917
6055
  function makeNode() {
@@ -6129,7 +6267,7 @@ var railsYaml2 = {
6129
6267
  if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
6130
6268
  let text;
6131
6269
  try {
6132
- text = readFileSync20(join16(localeRoot, file), "utf8");
6270
+ text = readFileSync21(join17(localeRoot, file), "utf8");
6133
6271
  } catch (e) {
6134
6272
  warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
6135
6273
  continue;
@@ -6150,8 +6288,8 @@ var railsYaml2 = {
6150
6288
  };
6151
6289
 
6152
6290
  // src/server/import/parsers/apple-stringsdict.ts
6153
- import { readdirSync as readdirSync13, readFileSync as readFileSync21, statSync as statSync7 } from "fs";
6154
- import { join as join17 } from "path";
6291
+ import { readdirSync as readdirSync13, readFileSync as readFileSync22, statSync as statSync7 } from "fs";
6292
+ import { join as join18 } from "path";
6155
6293
  var LOCALE_RE10 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
6156
6294
  var TABLE2 = "Localizable.stringsdict";
6157
6295
  function localeFromLproj2(dir) {
@@ -6305,16 +6443,16 @@ var appleStringsdict2 = {
6305
6443
  const locale = localeFromLproj2(dir);
6306
6444
  if (!locale) continue;
6307
6445
  if (opts?.locales && !opts.locales.includes(locale)) continue;
6308
- const file = join17(localeRoot, dir, TABLE2);
6446
+ const file = join18(localeRoot, dir, TABLE2);
6309
6447
  let text;
6310
6448
  try {
6311
6449
  if (!statSync7(file).isFile()) continue;
6312
- text = readFileSync21(file, "utf8");
6450
+ text = readFileSync22(file, "utf8");
6313
6451
  } catch {
6314
6452
  continue;
6315
6453
  }
6316
6454
  locales.push(locale);
6317
- const others = readdirSync13(join17(localeRoot, dir)).filter(
6455
+ const others = readdirSync13(join18(localeRoot, dir)).filter(
6318
6456
  (f) => f.endsWith(".stringsdict") && f !== TABLE2
6319
6457
  );
6320
6458
  if (others.length) {
@@ -6635,7 +6773,7 @@ function refreshLocationUsage(projectRoot, format) {
6635
6773
  }
6636
6774
 
6637
6775
  // src/server/export-run.ts
6638
- import { existsSync as existsSync12, readFileSync as readFileSync22, readdirSync as readdirSync14, rmdirSync, statSync as statSync8, unlinkSync } from "fs";
6776
+ import { existsSync as existsSync13, readFileSync as readFileSync23, readdirSync as readdirSync14, rmdirSync, statSync as statSync8, unlinkSync } from "fs";
6639
6777
  import { dirname as dirname2, resolve as resolve7, sep } from "path";
6640
6778
  function effectiveLocales(config) {
6641
6779
  const limit = config.exportLocales;
@@ -6678,7 +6816,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
6678
6816
  if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
6679
6817
  const next = resolve7(dir, segment);
6680
6818
  if (isLast) {
6681
- if (stale(locale) && existsSync12(next) && statSync8(next).isFile()) {
6819
+ if (stale(locale) && existsSync13(next) && statSync8(next).isFile()) {
6682
6820
  unlinkSync(next);
6683
6821
  deleted++;
6684
6822
  removeEmptyDirs(dir, root);
@@ -6734,7 +6872,7 @@ function exportToDisk(state, projectRoot, opts) {
6734
6872
  writtenPaths.add(abs);
6735
6873
  let current = null;
6736
6874
  try {
6737
- current = readFileSync22(abs, "utf8");
6875
+ current = readFileSync23(abs, "utf8");
6738
6876
  } catch {
6739
6877
  }
6740
6878
  if (current === f.contents) {
@@ -6751,17 +6889,17 @@ function exportToDisk(state, projectRoot, opts) {
6751
6889
  }
6752
6890
 
6753
6891
  // src/server/ui-prefs.ts
6754
- import { readFileSync as readFileSync23 } from "fs";
6892
+ import { readFileSync as readFileSync24 } from "fs";
6755
6893
  import { homedir as homedir2 } from "os";
6756
- import { join as join18 } from "path";
6894
+ import { join as join19 } from "path";
6757
6895
  var THEMES = ["system", "light", "dark"];
6758
6896
  var isThemeMode = (v) => THEMES.includes(v);
6759
6897
  var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
6760
- var defaultUiPrefsPath = () => join18(homedir2(), ".glotfile", "ui.json");
6898
+ var defaultUiPrefsPath = () => join19(homedir2(), ".glotfile", "ui.json");
6761
6899
  var DEFAULTS = { theme: "system" };
6762
6900
  function readJson(path) {
6763
6901
  try {
6764
- const parsed = JSON.parse(readFileSync23(path, "utf8"));
6902
+ const parsed = JSON.parse(readFileSync24(path, "utf8"));
6765
6903
  return parsed && typeof parsed === "object" ? parsed : {};
6766
6904
  } catch {
6767
6905
  return {};
@@ -6780,7 +6918,7 @@ function saveUiPrefs(path, prefs) {
6780
6918
  }
6781
6919
 
6782
6920
  // src/server/local-settings.ts
6783
- import { readFileSync as readFileSync24 } from "fs";
6921
+ import { readFileSync as readFileSync25 } from "fs";
6784
6922
  import { resolve as resolve8 } from "path";
6785
6923
  var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
6786
6924
  var isEditorId = (v) => EDITOR_IDS.includes(v);
@@ -6795,7 +6933,7 @@ var DEFAULT_EDITOR = "vscode";
6795
6933
  var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
6796
6934
  function readJson2(path) {
6797
6935
  try {
6798
- const parsed = JSON.parse(readFileSync24(path, "utf8"));
6936
+ const parsed = JSON.parse(readFileSync25(path, "utf8"));
6799
6937
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
6800
6938
  } catch {
6801
6939
  return {};
@@ -6885,7 +7023,7 @@ function createEventHub() {
6885
7023
 
6886
7024
  // src/server/watch.ts
6887
7025
  import { statSync as statSync9, readdirSync as readdirSync15 } from "fs";
6888
- import { join as join19 } from "path";
7026
+ import { join as join20 } from "path";
6889
7027
  import { createHash as createHash2 } from "crypto";
6890
7028
  function hashState(state) {
6891
7029
  return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
@@ -6901,15 +7039,15 @@ function signature(statePath) {
6901
7039
  const parts = [];
6902
7040
  for (const rel of ["config.json", "keys.json"]) {
6903
7041
  try {
6904
- const s = statSync9(join19(dir, rel));
7042
+ const s = statSync9(join20(dir, rel));
6905
7043
  parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
6906
7044
  } catch {
6907
7045
  }
6908
7046
  }
6909
7047
  try {
6910
- for (const name of readdirSync15(join19(dir, "locales")).sort()) {
7048
+ for (const name of readdirSync15(join20(dir, "locales")).sort()) {
6911
7049
  if (!name.endsWith(".json")) continue;
6912
- const s = statSync9(join19(dir, "locales", name));
7050
+ const s = statSync9(join20(dir, "locales", name));
6913
7051
  parts.push(`${name}:${s.size}:${s.mtimeMs}`);
6914
7052
  }
6915
7053
  } catch {
@@ -6982,9 +7120,9 @@ var sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
6982
7120
  var screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
6983
7121
  function projectName(root) {
6984
7122
  const nameFile = resolve9(root, ".idea", ".name");
6985
- if (existsSync13(nameFile)) {
7123
+ if (existsSync14(nameFile)) {
6986
7124
  try {
6987
- const name = readFileSync25(nameFile, "utf8").trim();
7125
+ const name = readFileSync26(nameFile, "utf8").trim();
6988
7126
  if (name) return name;
6989
7127
  } catch {
6990
7128
  }
@@ -7198,7 +7336,7 @@ function createApi(deps) {
7198
7336
  if (name.startsWith(".") || name === "node_modules") continue;
7199
7337
  const abs = resolve9(dir, name);
7200
7338
  let filePath = null;
7201
- if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync13(resolve9(abs, "config.json"))) {
7339
+ if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync14(resolve9(abs, "config.json"))) {
7202
7340
  filePath = resolve9(dir, `${name}.json`);
7203
7341
  } else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
7204
7342
  filePath = abs;
@@ -7232,7 +7370,7 @@ function createApi(deps) {
7232
7370
  const resolved = resolve9(projectRoot, path);
7233
7371
  const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
7234
7372
  if (!inside) return c.json({ error: "file is outside the project" }, 400);
7235
- if (!existsSync13(resolved)) return c.json({ error: "file not found" }, 400);
7373
+ if (!existsSync14(resolved)) return c.json({ error: "file not found" }, 400);
7236
7374
  loadState(resolved);
7237
7375
  deps.statePath = resolved;
7238
7376
  watcher.retarget(resolved);
@@ -7294,9 +7432,9 @@ function createApi(deps) {
7294
7432
  const abs = resolve9(root, screenshot);
7295
7433
  const rel = relative4(root, abs);
7296
7434
  const seg0 = rel.split(sep2)[0] ?? "";
7297
- if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync13(abs)) {
7435
+ if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync14(abs)) {
7298
7436
  try {
7299
- rmSync6(abs);
7437
+ rmSync7(abs);
7300
7438
  } catch {
7301
7439
  }
7302
7440
  }
@@ -7377,7 +7515,6 @@ function createApi(deps) {
7377
7515
  if (clearContext === true) {
7378
7516
  delete entry.context;
7379
7517
  delete entry.contextSource;
7380
- delete entry.contextAt;
7381
7518
  }
7382
7519
  updated++;
7383
7520
  }
@@ -7625,6 +7762,93 @@ function createApi(deps) {
7625
7762
  await stream.writeSSE({ event: "done", data: JSON.stringify({ added: added.length, terms: added }) });
7626
7763
  });
7627
7764
  });
7765
+ app.post("/glossary/suggest/estimate", async (c) => {
7766
+ const body = await c.req.json().catch(() => ({}));
7767
+ const s = load();
7768
+ const sources = selectGlossarySources(s, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
7769
+ const aiCfg = loadLocalSettings(projectRoot).ai;
7770
+ return c.json(estimateGlossarySuggest(sources, knownTermList(s), aiCfg));
7771
+ });
7772
+ app.get("/glossary/suggest/batch/status", async (c) => {
7773
+ const aiCfg = loadLocalSettings(projectRoot).ai;
7774
+ let supported = false;
7775
+ let provider;
7776
+ try {
7777
+ provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
7778
+ supported = supportsBatchComplete(provider);
7779
+ } catch {
7780
+ }
7781
+ const pending = loadPendingGlossaryBatch(projectRoot);
7782
+ if (!pending) return c.json({ supported, pending: null });
7783
+ const base = { batchId: pending.batchId, createdAt: pending.createdAt, model: pending.model, total: pending.total };
7784
+ if (!provider || !supportsBatchComplete(provider)) {
7785
+ return c.json({ supported, pending: { ...base, status: "unknown", counts: null } });
7786
+ }
7787
+ try {
7788
+ const status = await provider.translationBatchStatus(pending.batchId);
7789
+ return c.json({ supported, pending: { ...base, status: status.status, counts: status.counts } });
7790
+ } catch (e) {
7791
+ return c.json({ supported, pending: { ...base, status: "unknown", counts: null, error: e.message } });
7792
+ }
7793
+ });
7794
+ app.post("/glossary/suggest/batch", (c) => withTranslateLock(async () => {
7795
+ const body = await c.req.json().catch(() => ({}));
7796
+ const s = load();
7797
+ const sources = selectGlossarySources(s, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
7798
+ if (!sources.length) return c.json({ error: "No source strings to scan." }, 400);
7799
+ const aiCfg = loadLocalSettings(projectRoot).ai;
7800
+ let provider;
7801
+ try {
7802
+ provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
7803
+ } catch (e) {
7804
+ return c.json({ error: e.message }, 400);
7805
+ }
7806
+ if (!supportsBatchComplete(provider)) {
7807
+ return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
7808
+ }
7809
+ const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
7810
+ let pending;
7811
+ try {
7812
+ pending = await submitGlossarySuggestBatch(provider, sources, knownTermList(s), batchSize, aiCfg.model, projectRoot);
7813
+ } catch (e) {
7814
+ return c.json({ error: e.message }, 409);
7815
+ }
7816
+ appendLog(projectRoot, {
7817
+ at: (/* @__PURE__ */ new Date()).toISOString(),
7818
+ kind: "glossary",
7819
+ summary: `Submitted glossary suggestion batch ${pending.batchId} (${pending.total} sources)`,
7820
+ model: aiCfg.model
7821
+ });
7822
+ return c.json({ batchId: pending.batchId, total: pending.total });
7823
+ }));
7824
+ app.post("/glossary/suggest/batch/apply", (c) => withTranslateLock(async () => {
7825
+ const pending = loadPendingGlossaryBatch(projectRoot);
7826
+ if (!pending) return c.json({ error: "No pending glossary suggestion batch." }, 404);
7827
+ const aiCfg = loadLocalSettings(projectRoot).ai;
7828
+ let provider;
7829
+ try {
7830
+ provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
7831
+ } catch (e) {
7832
+ return c.json({ error: e.message }, 400);
7833
+ }
7834
+ if (!supportsBatchComplete(provider)) {
7835
+ return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
7836
+ }
7837
+ const outcome = await applyGlossarySuggestBatchResults(load, persist, provider, pending, projectRoot, aiCfg);
7838
+ return c.json(outcome);
7839
+ }));
7840
+ app.post("/glossary/suggest/batch/cancel", async (c) => {
7841
+ const pending = loadPendingGlossaryBatch(projectRoot);
7842
+ if (!pending) return c.json({ error: "No pending glossary suggestion batch." }, 404);
7843
+ const aiCfg = loadLocalSettings(projectRoot).ai;
7844
+ try {
7845
+ const provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
7846
+ if (supportsBatchComplete(provider)) await provider.cancelTranslationBatch(pending.batchId);
7847
+ } catch {
7848
+ }
7849
+ clearPendingGlossaryBatch(projectRoot);
7850
+ return c.json({ canceled: pending.batchId });
7851
+ });
7628
7852
  app.post("/keys/:key/screenshot", async (c) => {
7629
7853
  const key = c.req.param("key");
7630
7854
  const body = await c.req.parseBody();
@@ -8178,7 +8402,7 @@ function createApi(deps) {
8178
8402
  if (signal?.aborted) break;
8179
8403
  const batch = raw;
8180
8404
  const fresh = load();
8181
- const { written, errors } = applyContext(fresh, chunk2, batch.items ?? [], void 0, body.force === true);
8405
+ const { written, errors } = applyContext(fresh, chunk2, batch.items ?? [], body.force === true);
8182
8406
  const usage = provider.takeUsage?.();
8183
8407
  appendLog(projectRoot, {
8184
8408
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8322,7 +8546,7 @@ function createApi(deps) {
8322
8546
 
8323
8547
  // src/server/server.ts
8324
8548
  var here = dirname4(fileURLToPath(import.meta.url));
8325
- var DEFAULT_UI_DIR = join20(here, "..", "ui");
8549
+ var DEFAULT_UI_DIR = join21(here, "..", "ui");
8326
8550
  var MIME = {
8327
8551
  ".html": "text/html; charset=utf-8",
8328
8552
  ".js": "text/javascript; charset=utf-8",
@@ -8389,7 +8613,7 @@ function buildApp(opts) {
8389
8613
  const file = await readFileResponse(target);
8390
8614
  if (file) return file;
8391
8615
  }
8392
- const index = await readFileResponse(join20(root, "index.html"));
8616
+ const index = await readFileResponse(join21(root, "index.html"));
8393
8617
  if (index) return index;
8394
8618
  return c.notFound();
8395
8619
  });