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.
@@ -274,9 +274,6 @@ function validate(raw) {
274
274
  if (entry.contextSource !== void 0 && entry.contextSource !== "ai") {
275
275
  fail(`key "${key}" contextSource must be "ai" if present`);
276
276
  }
277
- if (entry.contextAt !== void 0 && typeof entry.contextAt !== "string") {
278
- fail(`key "${key}" contextAt must be a string if present`);
279
- }
280
277
  }
281
278
  if (raw.glossary !== void 0 && !Array.isArray(raw.glossary)) fail("glossary must be an array");
282
279
  if (raw.glossarySuggestions !== void 0 && !Array.isArray(raw.glossarySuggestions)) fail("glossarySuggestions must be an array");
@@ -772,7 +769,6 @@ function setMetadata(state, key, partial) {
772
769
  delete safe.values;
773
770
  if ("context" in safe) {
774
771
  delete entry.contextSource;
775
- delete entry.contextAt;
776
772
  }
777
773
  Object.assign(entry, safe);
778
774
  if ("context" in safe && !entry.context) delete entry.context;
@@ -3098,7 +3094,7 @@ var init_bedrock = __esm({
3098
3094
  if (res.stopReason === "max_tokens") {
3099
3095
  throw new MalformedReplyError(text || JSON.stringify(tool?.input ?? {}));
3100
3096
  }
3101
- if (tool?.input?.items) return tool.input.items;
3097
+ if (Array.isArray(tool?.input?.items)) return tool.input.items;
3102
3098
  return parseReplyItems(text);
3103
3099
  }
3104
3100
  };
@@ -4063,7 +4059,7 @@ ${s.lines}
4063
4059
  });
4064
4060
  return 'Write a context note for each key. Return JSON {"items":[{"id","context"}]}.\n' + JSON.stringify(items, null, 2);
4065
4061
  }
4066
- function applyContext(state, reqs, results, clock = systemClock, force = false) {
4062
+ function applyContext(state, reqs, results, force = false) {
4067
4063
  const byId = new Map(reqs.map((r) => [r.id, r]));
4068
4064
  let written = 0;
4069
4065
  const errors = [];
@@ -4087,7 +4083,6 @@ function applyContext(state, reqs, results, clock = systemClock, force = false)
4087
4083
  if (!entry || entry.context && !force) continue;
4088
4084
  entry.context = context;
4089
4085
  entry.contextSource = "ai";
4090
- entry.contextAt = clock();
4091
4086
  written++;
4092
4087
  }
4093
4088
  return { written, errors };
@@ -4096,7 +4091,6 @@ var MAX_CONTEXT_LENGTH, SNIPPET_WINDOW, MAX_SNIPPETS, EXCLUDED_DIRS, CONTEXT_BAT
4096
4091
  var init_context = __esm({
4097
4092
  "src/server/ai/context.ts"() {
4098
4093
  "use strict";
4099
- init_state();
4100
4094
  init_placeholders();
4101
4095
  MAX_CONTEXT_LENGTH = 500;
4102
4096
  SNIPPET_WINDOW = 15;
@@ -4238,7 +4232,7 @@ async function applyContextBatchResults(load, persist, provider, pending, projec
4238
4232
  if (retryUsage) addUsage(usage, retryUsage);
4239
4233
  }
4240
4234
  const fresh = load();
4241
- const { written, errors: applyErrors } = applyContext(fresh, applied, items, void 0, pending.force);
4235
+ const { written, errors: applyErrors } = applyContext(fresh, applied, items, pending.force);
4242
4236
  errors.push(...applyErrors);
4243
4237
  persist(fresh);
4244
4238
  clearPendingContextBatch(projectRoot);
@@ -4369,6 +4363,142 @@ var init_glossary_suggest = __esm({
4369
4363
  }
4370
4364
  });
4371
4365
 
4366
+ // src/server/ai/pending-glossary-batch.ts
4367
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync5, rmSync as rmSync6 } from "fs";
4368
+ import { join as join6 } from "path";
4369
+ function pendingGlossaryBatchPath(projectRoot) {
4370
+ return join6(projectRoot, ".glotfile", "glossary-suggest-batch.json");
4371
+ }
4372
+ function loadPendingGlossaryBatch(projectRoot) {
4373
+ const path = pendingGlossaryBatchPath(projectRoot);
4374
+ if (!existsSync10(path)) return void 0;
4375
+ try {
4376
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
4377
+ if (parsed?.version !== 1) return void 0;
4378
+ return parsed;
4379
+ } catch {
4380
+ return void 0;
4381
+ }
4382
+ }
4383
+ function savePendingGlossaryBatch(projectRoot, pending) {
4384
+ const dir = join6(projectRoot, ".glotfile");
4385
+ mkdirSync6(dir, { recursive: true });
4386
+ const gitignore = join6(dir, ".gitignore");
4387
+ if (!existsSync10(gitignore)) writeFileSync5(gitignore, "*\n");
4388
+ writeFileSync5(pendingGlossaryBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
4389
+ }
4390
+ function clearPendingGlossaryBatch(projectRoot) {
4391
+ rmSync6(pendingGlossaryBatchPath(projectRoot), { force: true });
4392
+ }
4393
+ var init_pending_glossary_batch = __esm({
4394
+ "src/server/ai/pending-glossary-batch.ts"() {
4395
+ "use strict";
4396
+ }
4397
+ });
4398
+
4399
+ // src/server/ai/glossary-batch-run.ts
4400
+ function completionRequestFor2(chunk2, knownTerms) {
4401
+ return {
4402
+ system: buildGlossarySuggestSystemPrompt(),
4403
+ content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunk2, knownTerms) }],
4404
+ schema: GLOSSARY_SUGGEST_SCHEMA
4405
+ };
4406
+ }
4407
+ async function submitGlossarySuggestBatch(provider, sources, knownTerms, batchSize, model, projectRoot) {
4408
+ if (loadPendingGlossaryBatch(projectRoot)) {
4409
+ throw new Error("A glossary suggestion batch is already pending. Apply or cancel it first.");
4410
+ }
4411
+ const chunks = [];
4412
+ const size = Math.max(1, batchSize);
4413
+ for (let i = 0; i < sources.length; i += size) chunks.push(sources.slice(i, i + size));
4414
+ const jobs = chunks.map((chunk2, i) => ({ customId: `gloss_${i}`, chunk: chunk2 }));
4415
+ const batchId = await provider.submitCompletionBatch(
4416
+ jobs.map((j) => ({ customId: j.customId, request: completionRequestFor2(j.chunk, knownTerms) }))
4417
+ );
4418
+ const pending = {
4419
+ version: 1,
4420
+ // Only Anthropic implements completion batches today.
4421
+ provider: "anthropic",
4422
+ model,
4423
+ batchId,
4424
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4425
+ total: sources.length,
4426
+ knownTerms,
4427
+ jobs: jobs.map((j) => ({
4428
+ customId: j.customId,
4429
+ requests: j.chunk
4430
+ }))
4431
+ };
4432
+ savePendingGlossaryBatch(projectRoot, pending);
4433
+ return pending;
4434
+ }
4435
+ async function applyGlossarySuggestBatchResults(load, persist, provider, pending, projectRoot, ai) {
4436
+ provider.takeUsage?.();
4437
+ const outcomes = await provider.completionBatchResults(pending.batchId);
4438
+ const batchUsage = provider.takeUsage?.();
4439
+ const allTerms = [];
4440
+ const errors = [];
4441
+ const jobFailures = [];
4442
+ const retryChunks = [];
4443
+ for (const job of pending.jobs) {
4444
+ const outcome = outcomes.get(job.customId);
4445
+ if (outcome?.type === "json") {
4446
+ const batch = outcome.value;
4447
+ allTerms.push(...batch.terms ?? []);
4448
+ continue;
4449
+ }
4450
+ if (!outcome) jobFailures.push({ customId: job.customId, locale: "", type: "missing" });
4451
+ else if (outcome.type === "malformed") jobFailures.push({ customId: job.customId, locale: "", type: "malformed", raw: outcome.raw });
4452
+ else jobFailures.push({ customId: job.customId, locale: "", type: "failed", error: outcome.error });
4453
+ retryChunks.push(job.requests);
4454
+ }
4455
+ for (const chunk2 of retryChunks) {
4456
+ try {
4457
+ const raw = await provider.complete(completionRequestFor2(chunk2, pending.knownTerms));
4458
+ const batch = raw;
4459
+ allTerms.push(...batch.terms ?? []);
4460
+ } catch (e) {
4461
+ errors.push({ error: e.message });
4462
+ }
4463
+ }
4464
+ const retryUsage = provider.takeUsage?.();
4465
+ const pricing = resolvePricing({ ...ai, model: pending.model });
4466
+ let estimatedCostUsd;
4467
+ if (pricing && (batchUsage || retryUsage)) {
4468
+ estimatedCostUsd = (batchUsage ? estimateUsageCostUsd(batchUsage, pricing, BATCH_PRICE_MULTIPLIER) : 0) + (retryUsage ? estimateUsageCostUsd(retryUsage, pricing) : 0);
4469
+ }
4470
+ let usage;
4471
+ if (batchUsage || retryUsage) {
4472
+ usage = batchUsage ?? { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
4473
+ if (retryUsage) addUsage(usage, retryUsage);
4474
+ }
4475
+ const fresh = load();
4476
+ const added = mergeGlossarySuggestions(fresh, dedupeTerms(allTerms));
4477
+ persist(fresh);
4478
+ clearPendingGlossaryBatch(projectRoot);
4479
+ const costSuffix = estimatedCostUsd !== void 0 ? ` (~$${estimatedCostUsd.toFixed(2)})` : "";
4480
+ appendLog(projectRoot, {
4481
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4482
+ kind: "glossary",
4483
+ summary: `Applied glossary suggestion batch ${pending.batchId}: ${added.length} new term(s), ${errors.length} error(s), ${retryChunks.length} job(s) retried${costSuffix}`,
4484
+ model: pending.model,
4485
+ jobFailures: jobFailures.length ? jobFailures : void 0,
4486
+ usage,
4487
+ estimatedCostUsd
4488
+ });
4489
+ return { added: added.length, errors, retried: retryChunks.length };
4490
+ }
4491
+ var init_glossary_batch_run = __esm({
4492
+ "src/server/ai/glossary-batch-run.ts"() {
4493
+ "use strict";
4494
+ init_glossary_suggest();
4495
+ init_pending_glossary_batch();
4496
+ init_state();
4497
+ init_log();
4498
+ init_pricing();
4499
+ }
4500
+ });
4501
+
4372
4502
  // src/server/ai/estimate.ts
4373
4503
  function estimateTokens(text) {
4374
4504
  const cjk = text.match(CJK_RE)?.length ?? 0;
@@ -4535,13 +4665,13 @@ var init_price_fetch = __esm({
4535
4665
  });
4536
4666
 
4537
4667
  // src/server/scan.ts
4538
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
4668
+ import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
4539
4669
  import { resolve as resolve7 } from "path";
4540
4670
  function loadUsageCache(projectRoot) {
4541
4671
  const path = resolve7(projectRoot, ".glotfile", "usage.json");
4542
- if (!existsSync10(path)) return null;
4672
+ if (!existsSync11(path)) return null;
4543
4673
  try {
4544
- return JSON.parse(readFileSync11(path, "utf8"));
4674
+ return JSON.parse(readFileSync12(path, "utf8"));
4545
4675
  } catch {
4546
4676
  return null;
4547
4677
  }
@@ -4606,8 +4736,8 @@ var init_scan = __esm({
4606
4736
  });
4607
4737
 
4608
4738
  // src/server/scanner.ts
4609
- import { readdirSync as readdirSync3, statSync as statSync3, readFileSync as readFileSync12 } from "fs";
4610
- import { join as join6, extname as extname2, relative } from "path";
4739
+ import { readdirSync as readdirSync3, statSync as statSync3, readFileSync as readFileSync13 } from "fs";
4740
+ import { join as join7, extname as extname2, relative } from "path";
4611
4741
  function scannerForExt(ext) {
4612
4742
  return EXT_SCANNER[ext] ?? null;
4613
4743
  }
@@ -4829,7 +4959,7 @@ function* walkFiles(dir, root, exclude) {
4829
4959
  }
4830
4960
  for (const name of entries) {
4831
4961
  if (ALWAYS_EXCLUDE.has(name)) continue;
4832
- const abs = join6(dir, name);
4962
+ const abs = join7(dir, name);
4833
4963
  const rel = relative(root, abs);
4834
4964
  let st;
4835
4965
  try {
@@ -4859,7 +4989,7 @@ function runScan(projectRoot, opts, existing) {
4859
4989
  const ext = extname2(relPath);
4860
4990
  const scanner = scannerForExt(ext);
4861
4991
  if (!scanner) continue;
4862
- const abs = join6(projectRoot, relPath);
4992
+ const abs = join7(projectRoot, relPath);
4863
4993
  let st;
4864
4994
  try {
4865
4995
  st = statSync3(abs);
@@ -4875,7 +5005,7 @@ function runScan(projectRoot, opts, existing) {
4875
5005
  }
4876
5006
  let content;
4877
5007
  try {
4878
- content = readFileSync12(abs, "utf8");
5008
+ content = readFileSync13(abs, "utf8");
4879
5009
  } catch {
4880
5010
  continue;
4881
5011
  }
@@ -5009,8 +5139,8 @@ var init_scanner = __esm({
5009
5139
  });
5010
5140
 
5011
5141
  // src/server/import/detect.ts
5012
- import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync4 } from "fs";
5013
- import { join as join7 } from "path";
5142
+ import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync14, statSync as statSync4 } from "fs";
5143
+ import { join as join8 } from "path";
5014
5144
  function safeIsDir(p) {
5015
5145
  try {
5016
5146
  return statSync4(p).isDirectory();
@@ -5019,7 +5149,7 @@ function safeIsDir(p) {
5019
5149
  }
5020
5150
  }
5021
5151
  function listDirs(dir) {
5022
- return readdirSync4(dir).filter((e) => safeIsDir(join7(dir, e)));
5152
+ return readdirSync4(dir).filter((e) => safeIsDir(join8(dir, e)));
5023
5153
  }
5024
5154
  function fileCount(dir) {
5025
5155
  try {
@@ -5033,23 +5163,23 @@ function pickSource(locales, sizeOf) {
5033
5163
  return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
5034
5164
  }
5035
5165
  function detectLaravel(root) {
5036
- const localeRoot = [join7(root, "resources", "lang"), join7(root, "lang")].find(safeIsDir);
5166
+ const localeRoot = [join8(root, "resources", "lang"), join8(root, "lang")].find(safeIsDir);
5037
5167
  if (!localeRoot) return null;
5038
5168
  const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
5039
5169
  if (locales.length === 0) return null;
5040
- const sourceLocale = pickSource(locales, (loc) => fileCount(join7(localeRoot, loc)));
5170
+ const sourceLocale = pickSource(locales, (loc) => fileCount(join8(localeRoot, loc)));
5041
5171
  return { format: "laravel-php", localeRoot, locales, sourceLocale };
5042
5172
  }
5043
5173
  function detectVue(root, forced = false) {
5044
5174
  for (const rel of VUE_DIR_CANDIDATES) {
5045
- const localeRoot = join7(root, rel);
5175
+ const localeRoot = join8(root, rel);
5046
5176
  if (!safeIsDir(localeRoot)) continue;
5047
5177
  const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
5048
5178
  const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
5049
5179
  if (enough) {
5050
5180
  const sourceLocale = pickSource(locales, (loc) => {
5051
5181
  try {
5052
- return statSync4(join7(localeRoot, `${loc}.json`)).size;
5182
+ return statSync4(join8(localeRoot, `${loc}.json`)).size;
5053
5183
  } catch {
5054
5184
  return 0;
5055
5185
  }
@@ -5060,9 +5190,9 @@ function detectVue(root, forced = false) {
5060
5190
  return null;
5061
5191
  }
5062
5192
  function hasNextIntlSignal(root) {
5063
- if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync11(join7(root, rel)))) return true;
5193
+ if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync12(join8(root, rel)))) return true;
5064
5194
  try {
5065
- const pkg = JSON.parse(readFileSync13(join7(root, "package.json"), "utf8"));
5195
+ const pkg = JSON.parse(readFileSync14(join8(root, "package.json"), "utf8"));
5066
5196
  if (pkg.dependencies?.["next-intl"] || pkg.devDependencies?.["next-intl"]) return true;
5067
5197
  } catch {
5068
5198
  }
@@ -5071,7 +5201,7 @@ function hasNextIntlSignal(root) {
5071
5201
  function nextIntlDefaultLocale(root) {
5072
5202
  for (const rel of NEXT_INTL_ROUTING_CANDIDATES) {
5073
5203
  try {
5074
- const m = readFileSync13(join7(root, rel), "utf8").match(/defaultLocale\s*:\s*['"]([^'"]+)['"]/);
5204
+ const m = readFileSync14(join8(root, rel), "utf8").match(/defaultLocale\s*:\s*['"]([^'"]+)['"]/);
5075
5205
  if (m) return m[1];
5076
5206
  } catch {
5077
5207
  }
@@ -5081,14 +5211,14 @@ function nextIntlDefaultLocale(root) {
5081
5211
  function detectNextIntl(root, forced = false) {
5082
5212
  if (!forced && !hasNextIntlSignal(root)) return null;
5083
5213
  for (const rel of NEXT_INTL_DIR_CANDIDATES) {
5084
- const localeRoot = join7(root, rel);
5214
+ const localeRoot = join8(root, rel);
5085
5215
  if (!safeIsDir(localeRoot)) continue;
5086
5216
  const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
5087
5217
  if (locales.length === 0) continue;
5088
5218
  const def = nextIntlDefaultLocale(root);
5089
5219
  const sourceLocale = def && locales.includes(def) ? def : pickSource(locales, (loc) => {
5090
5220
  try {
5091
- return statSync4(join7(localeRoot, `${loc}.json`)).size;
5221
+ return statSync4(join8(localeRoot, `${loc}.json`)).size;
5092
5222
  } catch {
5093
5223
  return 0;
5094
5224
  }
@@ -5099,7 +5229,7 @@ function detectNextIntl(root, forced = false) {
5099
5229
  }
5100
5230
  function detectArb(root) {
5101
5231
  for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
5102
- const localeRoot = join7(root, rel);
5232
+ const localeRoot = join8(root, rel);
5103
5233
  if (!safeIsDir(localeRoot)) continue;
5104
5234
  const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
5105
5235
  if (locales.length >= 1) {
@@ -5109,10 +5239,10 @@ function detectArb(root) {
5109
5239
  return null;
5110
5240
  }
5111
5241
  function lprojLocales(dir) {
5112
- return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join7(dir, `${l}.lproj`, "Localizable.strings")));
5242
+ return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync12(join8(dir, `${l}.lproj`, "Localizable.strings")));
5113
5243
  }
5114
5244
  function detectApple(root) {
5115
- const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
5245
+ const candidates = [root, ...listDirs(root).map((d) => join8(root, d))];
5116
5246
  let best = null;
5117
5247
  for (const dir of candidates) {
5118
5248
  const locales = lprojLocales(dir);
@@ -5124,7 +5254,7 @@ function detectApple(root) {
5124
5254
  locales,
5125
5255
  sourceLocale: pickSource(locales, (loc) => {
5126
5256
  try {
5127
- return statSync4(join7(dir, `${loc}.lproj`, "Localizable.strings")).size;
5257
+ return statSync4(join8(dir, `${loc}.lproj`, "Localizable.strings")).size;
5128
5258
  } catch {
5129
5259
  return 0;
5130
5260
  }
@@ -5136,7 +5266,7 @@ function detectApple(root) {
5136
5266
  }
5137
5267
  function detectAngularXliff(root) {
5138
5268
  for (const rel of ANGULAR_DIR_CANDIDATES) {
5139
- const localeRoot = rel === "." ? root : join7(root, rel);
5269
+ const localeRoot = rel === "." ? root : join8(root, rel);
5140
5270
  if (!safeIsDir(localeRoot)) continue;
5141
5271
  const files = readdirSync4(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
5142
5272
  if (files.length === 0) continue;
@@ -5144,7 +5274,7 @@ function detectAngularXliff(root) {
5144
5274
  const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
5145
5275
  let sourceLocale;
5146
5276
  try {
5147
- sourceLocale = readFileSync13(join7(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
5277
+ sourceLocale = readFileSync14(join8(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
5148
5278
  } catch {
5149
5279
  }
5150
5280
  if (!sourceLocale && locales.length === 0) continue;
@@ -5155,14 +5285,14 @@ function detectAngularXliff(root) {
5155
5285
  return null;
5156
5286
  }
5157
5287
  function detectRails(root) {
5158
- const localeRoot = join7(root, "config", "locales");
5288
+ const localeRoot = join8(root, "config", "locales");
5159
5289
  if (!safeIsDir(localeRoot)) return null;
5160
5290
  const locales = [];
5161
5291
  for (const file of readdirSync4(localeRoot).sort()) {
5162
5292
  if (!/\.ya?ml$/.test(file)) continue;
5163
5293
  let text;
5164
5294
  try {
5165
- text = readFileSync13(join7(localeRoot, file), "utf8");
5295
+ text = readFileSync14(join8(localeRoot, file), "utf8");
5166
5296
  } catch {
5167
5297
  continue;
5168
5298
  }
@@ -5176,15 +5306,15 @@ function detectRails(root) {
5176
5306
  }
5177
5307
  function detectI18next(root) {
5178
5308
  for (const rel of I18NEXT_DIR_CANDIDATES) {
5179
- const localeRoot = join7(root, rel);
5309
+ const localeRoot = join8(root, rel);
5180
5310
  if (!safeIsDir(localeRoot)) continue;
5181
5311
  const locales = listDirs(localeRoot).filter(
5182
- (d) => LOCALE_RE.test(d) && readdirSync4(join7(localeRoot, d)).some((f) => f.endsWith(".json"))
5312
+ (d) => LOCALE_RE.test(d) && readdirSync4(join8(localeRoot, d)).some((f) => f.endsWith(".json"))
5183
5313
  );
5184
5314
  if (locales.length === 0) continue;
5185
5315
  const sourceLocale = pickSource(locales, (loc) => {
5186
5316
  try {
5187
- return readdirSync4(join7(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync4(join7(localeRoot, loc, f)).size, 0);
5317
+ return readdirSync4(join8(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync4(join8(localeRoot, loc, f)).size, 0);
5188
5318
  } catch {
5189
5319
  return 0;
5190
5320
  }
@@ -5201,8 +5331,8 @@ function gettextLocales(dir) {
5201
5331
  if (!locales.includes(flat)) locales.push(flat);
5202
5332
  continue;
5203
5333
  }
5204
- if (!LOCALE_RE.test(entry) || !safeIsDir(join7(dir, entry))) continue;
5205
- const sub = join7(dir, entry);
5334
+ if (!LOCALE_RE.test(entry) || !safeIsDir(join8(dir, entry))) continue;
5335
+ const sub = join8(dir, entry);
5206
5336
  const hasPo = (d) => {
5207
5337
  try {
5208
5338
  return readdirSync4(d).some((f) => f.endsWith(".po"));
@@ -5210,7 +5340,7 @@ function gettextLocales(dir) {
5210
5340
  return false;
5211
5341
  }
5212
5342
  };
5213
- if (hasPo(join7(sub, "LC_MESSAGES")) || hasPo(sub)) {
5343
+ if (hasPo(join8(sub, "LC_MESSAGES")) || hasPo(sub)) {
5214
5344
  if (!locales.includes(entry)) locales.push(entry);
5215
5345
  }
5216
5346
  }
@@ -5218,7 +5348,7 @@ function gettextLocales(dir) {
5218
5348
  }
5219
5349
  function detectGettext(root) {
5220
5350
  for (const rel of GETTEXT_DIR_CANDIDATES) {
5221
- const localeRoot = join7(root, rel);
5351
+ const localeRoot = join8(root, rel);
5222
5352
  if (!safeIsDir(localeRoot)) continue;
5223
5353
  const locales = gettextLocales(localeRoot);
5224
5354
  if (locales.length === 0) continue;
@@ -5227,10 +5357,10 @@ function detectGettext(root) {
5227
5357
  return null;
5228
5358
  }
5229
5359
  function detectAppleStringsdict(root) {
5230
- const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
5360
+ const candidates = [root, ...listDirs(root).map((d) => join8(root, d))];
5231
5361
  let best = null;
5232
5362
  for (const dir of candidates) {
5233
- const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join7(dir, `${l}.lproj`, "Localizable.stringsdict")));
5363
+ const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync12(join8(dir, `${l}.lproj`, "Localizable.stringsdict")));
5234
5364
  if (locales.length === 0) continue;
5235
5365
  if (!best || locales.length > best.locales.length) {
5236
5366
  best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
@@ -5239,7 +5369,7 @@ function detectAppleStringsdict(root) {
5239
5369
  return best;
5240
5370
  }
5241
5371
  function detect(root, formatOverride) {
5242
- if (!existsSync11(root)) return null;
5372
+ if (!existsSync12(root)) return null;
5243
5373
  if (formatOverride) {
5244
5374
  const fn = BY_FORMAT[formatOverride];
5245
5375
  if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
@@ -5317,8 +5447,8 @@ var init_flatten = __esm({
5317
5447
  });
5318
5448
 
5319
5449
  // src/server/import/parsers/vue-i18n-json.ts
5320
- import { readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
5321
- import { join as join8 } from "path";
5450
+ import { readdirSync as readdirSync5, readFileSync as readFileSync15 } from "fs";
5451
+ import { join as join9 } from "path";
5322
5452
  function fromVueI18n(value) {
5323
5453
  return value.replace(/\{'([^']*)'\}/g, "'$1'");
5324
5454
  }
@@ -5341,7 +5471,7 @@ var init_vue_i18n_json2 = __esm({
5341
5471
  if (opts?.locales && !opts.locales.includes(locale)) continue;
5342
5472
  let data;
5343
5473
  try {
5344
- data = JSON.parse(readFileSync14(join8(localeRoot, file), "utf8"));
5474
+ data = JSON.parse(readFileSync15(join9(localeRoot, file), "utf8"));
5345
5475
  } catch (e) {
5346
5476
  warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
5347
5477
  continue;
@@ -5358,8 +5488,8 @@ var init_vue_i18n_json2 = __esm({
5358
5488
  });
5359
5489
 
5360
5490
  // src/server/import/parsers/next-intl-json.ts
5361
- import { readdirSync as readdirSync6, readFileSync as readFileSync15 } from "fs";
5362
- import { join as join9 } from "path";
5491
+ import { readdirSync as readdirSync6, readFileSync as readFileSync16 } from "fs";
5492
+ import { join as join10 } from "path";
5363
5493
  var LOCALE_RE3, nextIntlJson2;
5364
5494
  var init_next_intl_json2 = __esm({
5365
5495
  "src/server/import/parsers/next-intl-json.ts"() {
@@ -5379,7 +5509,7 @@ var init_next_intl_json2 = __esm({
5379
5509
  if (opts?.locales && !opts.locales.includes(locale)) continue;
5380
5510
  let data;
5381
5511
  try {
5382
- data = JSON.parse(readFileSync15(join9(localeRoot, file), "utf8"));
5512
+ data = JSON.parse(readFileSync16(join10(localeRoot, file), "utf8"));
5383
5513
  } catch (e) {
5384
5514
  warnings.push(`next-intl-json: failed to parse ${file}: ${e.message}`);
5385
5515
  continue;
@@ -5413,16 +5543,16 @@ var init_placeholders2 = __esm({
5413
5543
 
5414
5544
  // src/server/import/parsers/laravel-php.ts
5415
5545
  import { readdirSync as readdirSync7, statSync as statSync5 } from "fs";
5416
- import { join as join10, relative as relative2 } from "path";
5546
+ import { join as join11, relative as relative2 } from "path";
5417
5547
  import { execFileSync } from "child_process";
5418
5548
  function listDirs2(dir) {
5419
- return readdirSync7(dir).filter((e) => statSync5(join10(dir, e)).isDirectory());
5549
+ return readdirSync7(dir).filter((e) => statSync5(join11(dir, e)).isDirectory());
5420
5550
  }
5421
5551
  function listPhpFiles(dir) {
5422
5552
  const out = [];
5423
5553
  const walk = (d) => {
5424
5554
  for (const e of readdirSync7(d)) {
5425
- const full = join10(d, e);
5555
+ const full = join11(d, e);
5426
5556
  if (statSync5(full).isDirectory()) walk(full);
5427
5557
  else if (e.endsWith(".php")) out.push(full);
5428
5558
  }
@@ -5465,7 +5595,7 @@ var init_laravel_php2 = __esm({
5465
5595
  for (const locale of listDirs2(localeRoot).sort()) {
5466
5596
  if (locale === "vendor") continue;
5467
5597
  if (opts?.locales && !opts.locales.includes(locale)) continue;
5468
- const localeDir = join10(localeRoot, locale);
5598
+ const localeDir = join11(localeRoot, locale);
5469
5599
  locales.push(locale);
5470
5600
  for (const file of listPhpFiles(localeDir)) {
5471
5601
  const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
@@ -5490,8 +5620,8 @@ var init_laravel_php2 = __esm({
5490
5620
  });
5491
5621
 
5492
5622
  // src/server/import/parsers/flutter-arb.ts
5493
- import { readdirSync as readdirSync8, readFileSync as readFileSync16 } from "fs";
5494
- import { join as join11 } from "path";
5623
+ import { readdirSync as readdirSync8, readFileSync as readFileSync17 } from "fs";
5624
+ import { join as join12 } from "path";
5495
5625
  function localeFromArbName(file) {
5496
5626
  const m = file.match(/^(.+)\.arb$/);
5497
5627
  if (!m) return null;
@@ -5531,7 +5661,7 @@ var init_flutter_arb2 = __esm({
5531
5661
  if (opts?.locales && !opts.locales.includes(locale)) continue;
5532
5662
  let data;
5533
5663
  try {
5534
- data = JSON.parse(readFileSync16(join11(localeRoot, file), "utf8"));
5664
+ data = JSON.parse(readFileSync17(join12(localeRoot, file), "utf8"));
5535
5665
  } catch (e) {
5536
5666
  warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
5537
5667
  continue;
@@ -5558,8 +5688,8 @@ var init_flutter_arb2 = __esm({
5558
5688
  });
5559
5689
 
5560
5690
  // src/server/import/parsers/apple-strings.ts
5561
- import { readdirSync as readdirSync9, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
5562
- import { join as join12 } from "path";
5691
+ import { readdirSync as readdirSync9, readFileSync as readFileSync18, statSync as statSync6 } from "fs";
5692
+ import { join as join13 } from "path";
5563
5693
  function localeFromLproj(dir) {
5564
5694
  const m = dir.match(/^(.+)\.lproj$/);
5565
5695
  if (!m) return null;
@@ -5679,16 +5809,16 @@ var init_apple_strings2 = __esm({
5679
5809
  const locale = localeFromLproj(dir);
5680
5810
  if (!locale) continue;
5681
5811
  if (opts?.locales && !opts.locales.includes(locale)) continue;
5682
- const file = join12(localeRoot, dir, TABLE);
5812
+ const file = join13(localeRoot, dir, TABLE);
5683
5813
  let text;
5684
5814
  try {
5685
5815
  if (!statSync6(file).isFile()) continue;
5686
- text = readFileSync17(file, "utf8");
5816
+ text = readFileSync18(file, "utf8");
5687
5817
  } catch {
5688
5818
  continue;
5689
5819
  }
5690
5820
  locales.push(locale);
5691
- const others = readdirSync9(join12(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
5821
+ const others = readdirSync9(join13(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
5692
5822
  if (others.length) {
5693
5823
  warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
5694
5824
  }
@@ -5703,8 +5833,8 @@ var init_apple_strings2 = __esm({
5703
5833
  });
5704
5834
 
5705
5835
  // src/server/import/parsers/angular-xliff.ts
5706
- import { readdirSync as readdirSync10, readFileSync as readFileSync18 } from "fs";
5707
- import { join as join13 } from "path";
5836
+ import { readdirSync as readdirSync10, readFileSync as readFileSync19 } from "fs";
5837
+ import { join as join14 } from "path";
5708
5838
  function decodeEntities(s) {
5709
5839
  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, "&");
5710
5840
  }
@@ -5775,7 +5905,7 @@ var init_angular_xliff2 = __esm({
5775
5905
  if (fnameLocale !== void 0 && !LOCALE_RE6.test(fnameLocale)) continue;
5776
5906
  let xml;
5777
5907
  try {
5778
- xml = readFileSync18(join13(localeRoot, file), "utf8");
5908
+ xml = readFileSync19(join14(localeRoot, file), "utf8");
5779
5909
  } catch (e) {
5780
5910
  warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
5781
5911
  continue;
@@ -5822,8 +5952,8 @@ var init_angular_xliff2 = __esm({
5822
5952
  });
5823
5953
 
5824
5954
  // src/server/import/parsers/gettext-po.ts
5825
- import { readdirSync as readdirSync11, readFileSync as readFileSync19 } from "fs";
5826
- import { join as join14 } from "path";
5955
+ import { readdirSync as readdirSync11, readFileSync as readFileSync20 } from "fs";
5956
+ import { join as join15 } from "path";
5827
5957
  function unescapePo(s) {
5828
5958
  return s.replace(
5829
5959
  /\\([\\"ntr])/g,
@@ -5912,17 +6042,17 @@ function discoverPoFiles(root) {
5912
6042
  for (const e of entries) {
5913
6043
  if (e.isFile() && e.name.endsWith(".po")) {
5914
6044
  const base = e.name.slice(0, -3);
5915
- found.push({ path: join14(root, e.name), rel: e.name, locale: LOCALE_RE7.test(base) ? base : null });
6045
+ found.push({ path: join15(root, e.name), rel: e.name, locale: LOCALE_RE7.test(base) ? base : null });
5916
6046
  } else if (e.isDirectory() && LOCALE_RE7.test(e.name)) {
5917
- for (const sub of [join14(e.name, "LC_MESSAGES"), e.name]) {
6047
+ for (const sub of [join15(e.name, "LC_MESSAGES"), e.name]) {
5918
6048
  let names;
5919
6049
  try {
5920
- names = readdirSync11(join14(root, sub)).sort();
6050
+ names = readdirSync11(join15(root, sub)).sort();
5921
6051
  } catch {
5922
6052
  continue;
5923
6053
  }
5924
6054
  for (const f of names) {
5925
- if (f.endsWith(".po")) found.push({ path: join14(root, sub, f), rel: join14(sub, f), locale: e.name });
6055
+ if (f.endsWith(".po")) found.push({ path: join15(root, sub, f), rel: join15(sub, f), locale: e.name });
5926
6056
  }
5927
6057
  }
5928
6058
  }
@@ -5946,7 +6076,7 @@ var init_gettext_po2 = __esm({
5946
6076
  for (const file of discoverPoFiles(localeRoot)) {
5947
6077
  let entries;
5948
6078
  try {
5949
- entries = parseEntries(readFileSync19(file.path, "utf8"));
6079
+ entries = parseEntries(readFileSync20(file.path, "utf8"));
5950
6080
  } catch (e) {
5951
6081
  warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
5952
6082
  continue;
@@ -5993,8 +6123,8 @@ var init_gettext_po2 = __esm({
5993
6123
  });
5994
6124
 
5995
6125
  // src/server/import/parsers/i18next-json.ts
5996
- import { readdirSync as readdirSync12, readFileSync as readFileSync20, statSync as statSync7 } from "fs";
5997
- import { join as join15 } from "path";
6126
+ import { readdirSync as readdirSync12, readFileSync as readFileSync21, statSync as statSync7 } from "fs";
6127
+ import { join as join16 } from "path";
5998
6128
  function safeIsDir2(p) {
5999
6129
  try {
6000
6130
  return statSync7(p).isDirectory();
@@ -6009,7 +6139,7 @@ function fromI18next(value) {
6009
6139
  function ingestFile(path, label, prefix, locale, keys, warnings) {
6010
6140
  let data;
6011
6141
  try {
6012
- data = JSON.parse(readFileSync20(path, "utf8"));
6142
+ data = JSON.parse(readFileSync21(path, "utf8"));
6013
6143
  } catch (e) {
6014
6144
  warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
6015
6145
  return false;
@@ -6062,7 +6192,7 @@ var init_i18next_json2 = __esm({
6062
6192
  const keys = {};
6063
6193
  const locales = [];
6064
6194
  for (const entry of readdirSync12(localeRoot).sort()) {
6065
- const full = join15(localeRoot, entry);
6195
+ const full = join16(localeRoot, entry);
6066
6196
  if (safeIsDir2(full)) {
6067
6197
  if (!LOCALE_RE8.test(entry)) continue;
6068
6198
  if (opts?.locales && !opts.locales.includes(entry)) continue;
@@ -6071,7 +6201,7 @@ var init_i18next_json2 = __esm({
6071
6201
  if (!file.endsWith(".json")) continue;
6072
6202
  const ns = file.slice(0, -".json".length);
6073
6203
  const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
6074
- if (ingestFile(join15(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
6204
+ if (ingestFile(join16(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
6075
6205
  }
6076
6206
  if (any && !locales.includes(entry)) locales.push(entry);
6077
6207
  } else if (entry.endsWith(".json")) {
@@ -6090,8 +6220,8 @@ var init_i18next_json2 = __esm({
6090
6220
  });
6091
6221
 
6092
6222
  // src/server/import/parsers/rails-yaml.ts
6093
- import { readdirSync as readdirSync13, readFileSync as readFileSync21 } from "fs";
6094
- import { join as join16 } from "path";
6223
+ import { readdirSync as readdirSync13, readFileSync as readFileSync22 } from "fs";
6224
+ import { join as join17 } from "path";
6095
6225
  function makeNode() {
6096
6226
  return /* @__PURE__ */ Object.create(null);
6097
6227
  }
@@ -6315,7 +6445,7 @@ var init_rails_yaml2 = __esm({
6315
6445
  if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
6316
6446
  let text;
6317
6447
  try {
6318
- text = readFileSync21(join16(localeRoot, file), "utf8");
6448
+ text = readFileSync22(join17(localeRoot, file), "utf8");
6319
6449
  } catch (e) {
6320
6450
  warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
6321
6451
  continue;
@@ -6338,8 +6468,8 @@ var init_rails_yaml2 = __esm({
6338
6468
  });
6339
6469
 
6340
6470
  // src/server/import/parsers/apple-stringsdict.ts
6341
- import { readdirSync as readdirSync14, readFileSync as readFileSync22, statSync as statSync8 } from "fs";
6342
- import { join as join17 } from "path";
6471
+ import { readdirSync as readdirSync14, readFileSync as readFileSync23, statSync as statSync8 } from "fs";
6472
+ import { join as join18 } from "path";
6343
6473
  function localeFromLproj2(dir) {
6344
6474
  const m = dir.match(/^(.+)\.lproj$/);
6345
6475
  if (!m) return null;
@@ -6499,16 +6629,16 @@ var init_apple_stringsdict2 = __esm({
6499
6629
  const locale = localeFromLproj2(dir);
6500
6630
  if (!locale) continue;
6501
6631
  if (opts?.locales && !opts.locales.includes(locale)) continue;
6502
- const file = join17(localeRoot, dir, TABLE2);
6632
+ const file = join18(localeRoot, dir, TABLE2);
6503
6633
  let text;
6504
6634
  try {
6505
6635
  if (!statSync8(file).isFile()) continue;
6506
- text = readFileSync22(file, "utf8");
6636
+ text = readFileSync23(file, "utf8");
6507
6637
  } catch {
6508
6638
  continue;
6509
6639
  }
6510
6640
  locales.push(locale);
6511
- const others = readdirSync14(join17(localeRoot, dir)).filter(
6641
+ const others = readdirSync14(join18(localeRoot, dir)).filter(
6512
6642
  (f) => f.endsWith(".stringsdict") && f !== TABLE2
6513
6643
  );
6514
6644
  if (others.length) {
@@ -6982,7 +7112,7 @@ var init_run2 = __esm({
6982
7112
  });
6983
7113
 
6984
7114
  // src/server/lint/outputs.ts
6985
- import { readFileSync as readFileSync23, existsSync as existsSync12 } from "fs";
7115
+ import { readFileSync as readFileSync24, existsSync as existsSync13 } from "fs";
6986
7116
  import { resolve as resolve8 } from "path";
6987
7117
  function checkOutputs(state, root) {
6988
7118
  const out = [];
@@ -6990,7 +7120,7 @@ function checkOutputs(state, root) {
6990
7120
  const result = getAdapter(output.adapter).export(state, output);
6991
7121
  for (const file of result.files) {
6992
7122
  const abs = resolve8(root, file.path);
6993
- const current = existsSync12(abs) ? readFileSync23(abs, "utf8") : null;
7123
+ const current = existsSync13(abs) ? readFileSync24(abs, "utf8") : null;
6994
7124
  if (current === null) {
6995
7125
  out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
6996
7126
  } else if (current !== file.contents) {
@@ -7493,12 +7623,12 @@ var init_explain_error = __esm({
7493
7623
  });
7494
7624
 
7495
7625
  // src/server/ui-prefs.ts
7496
- import { readFileSync as readFileSync24 } from "fs";
7626
+ import { readFileSync as readFileSync25 } from "fs";
7497
7627
  import { homedir as homedir2 } from "os";
7498
- import { join as join18 } from "path";
7628
+ import { join as join19 } from "path";
7499
7629
  function readJson2(path) {
7500
7630
  try {
7501
- const parsed = JSON.parse(readFileSync24(path, "utf8"));
7631
+ const parsed = JSON.parse(readFileSync25(path, "utf8"));
7502
7632
  return parsed && typeof parsed === "object" ? parsed : {};
7503
7633
  } catch {
7504
7634
  return {};
@@ -7523,7 +7653,7 @@ var init_ui_prefs = __esm({
7523
7653
  THEMES = ["system", "light", "dark"];
7524
7654
  isThemeMode = (v) => THEMES.includes(v);
7525
7655
  isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
7526
- defaultUiPrefsPath = () => join18(homedir2(), ".glotfile", "ui.json");
7656
+ defaultUiPrefsPath = () => join19(homedir2(), ".glotfile", "ui.json");
7527
7657
  DEFAULTS = { theme: "system" };
7528
7658
  }
7529
7659
  });
@@ -7557,7 +7687,7 @@ var init_events = __esm({
7557
7687
 
7558
7688
  // src/server/watch.ts
7559
7689
  import { statSync as statSync9, readdirSync as readdirSync15 } from "fs";
7560
- import { join as join19 } from "path";
7690
+ import { join as join20 } from "path";
7561
7691
  import { createHash as createHash2 } from "crypto";
7562
7692
  function hashState(state) {
7563
7693
  return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
@@ -7573,15 +7703,15 @@ function signature(statePath) {
7573
7703
  const parts = [];
7574
7704
  for (const rel of ["config.json", "keys.json"]) {
7575
7705
  try {
7576
- const s = statSync9(join19(dir, rel));
7706
+ const s = statSync9(join20(dir, rel));
7577
7707
  parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
7578
7708
  } catch {
7579
7709
  }
7580
7710
  }
7581
7711
  try {
7582
- for (const name of readdirSync15(join19(dir, "locales")).sort()) {
7712
+ for (const name of readdirSync15(join20(dir, "locales")).sort()) {
7583
7713
  if (!name.endsWith(".json")) continue;
7584
- const s = statSync9(join19(dir, "locales", name));
7714
+ const s = statSync9(join20(dir, "locales", name));
7585
7715
  parts.push(`${name}:${s.size}:${s.mtimeMs}`);
7586
7716
  }
7587
7717
  } catch {
@@ -7660,13 +7790,13 @@ var init_watch = __esm({
7660
7790
  // src/server/api.ts
7661
7791
  import { Hono } from "hono";
7662
7792
  import { streamSSE } from "hono/streaming";
7663
- import { readFileSync as readFileSync25, existsSync as existsSync13, readdirSync as readdirSync16, statSync as statSync10, rmSync as rmSync6 } from "fs";
7793
+ import { readFileSync as readFileSync26, existsSync as existsSync14, readdirSync as readdirSync16, statSync as statSync10, rmSync as rmSync7 } from "fs";
7664
7794
  import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
7665
7795
  function projectName(root) {
7666
7796
  const nameFile = resolve9(root, ".idea", ".name");
7667
- if (existsSync13(nameFile)) {
7797
+ if (existsSync14(nameFile)) {
7668
7798
  try {
7669
- const name = readFileSync25(nameFile, "utf8").trim();
7799
+ const name = readFileSync26(nameFile, "utf8").trim();
7670
7800
  if (name) return name;
7671
7801
  } catch {
7672
7802
  }
@@ -7880,7 +8010,7 @@ function createApi(deps) {
7880
8010
  if (name.startsWith(".") || name === "node_modules") continue;
7881
8011
  const abs = resolve9(dir, name);
7882
8012
  let filePath = null;
7883
- if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync13(resolve9(abs, "config.json"))) {
8013
+ if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync14(resolve9(abs, "config.json"))) {
7884
8014
  filePath = resolve9(dir, `${name}.json`);
7885
8015
  } else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
7886
8016
  filePath = abs;
@@ -7914,7 +8044,7 @@ function createApi(deps) {
7914
8044
  const resolved = resolve9(projectRoot, path);
7915
8045
  const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
7916
8046
  if (!inside) return c.json({ error: "file is outside the project" }, 400);
7917
- if (!existsSync13(resolved)) return c.json({ error: "file not found" }, 400);
8047
+ if (!existsSync14(resolved)) return c.json({ error: "file not found" }, 400);
7918
8048
  loadState(resolved);
7919
8049
  deps.statePath = resolved;
7920
8050
  watcher.retarget(resolved);
@@ -7976,9 +8106,9 @@ function createApi(deps) {
7976
8106
  const abs = resolve9(root, screenshot);
7977
8107
  const rel = relative4(root, abs);
7978
8108
  const seg0 = rel.split(sep2)[0] ?? "";
7979
- if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync13(abs)) {
8109
+ if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync14(abs)) {
7980
8110
  try {
7981
- rmSync6(abs);
8111
+ rmSync7(abs);
7982
8112
  } catch {
7983
8113
  }
7984
8114
  }
@@ -8059,7 +8189,6 @@ function createApi(deps) {
8059
8189
  if (clearContext === true) {
8060
8190
  delete entry.context;
8061
8191
  delete entry.contextSource;
8062
- delete entry.contextAt;
8063
8192
  }
8064
8193
  updated++;
8065
8194
  }
@@ -8307,6 +8436,93 @@ function createApi(deps) {
8307
8436
  await stream.writeSSE({ event: "done", data: JSON.stringify({ added: added.length, terms: added }) });
8308
8437
  });
8309
8438
  });
8439
+ app.post("/glossary/suggest/estimate", async (c) => {
8440
+ const body = await c.req.json().catch(() => ({}));
8441
+ const s = load();
8442
+ const sources = selectGlossarySources(s, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
8443
+ const aiCfg = loadLocalSettings(projectRoot).ai;
8444
+ return c.json(estimateGlossarySuggest(sources, knownTermList(s), aiCfg));
8445
+ });
8446
+ app.get("/glossary/suggest/batch/status", async (c) => {
8447
+ const aiCfg = loadLocalSettings(projectRoot).ai;
8448
+ let supported = false;
8449
+ let provider;
8450
+ try {
8451
+ provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
8452
+ supported = supportsBatchComplete(provider);
8453
+ } catch {
8454
+ }
8455
+ const pending = loadPendingGlossaryBatch(projectRoot);
8456
+ if (!pending) return c.json({ supported, pending: null });
8457
+ const base = { batchId: pending.batchId, createdAt: pending.createdAt, model: pending.model, total: pending.total };
8458
+ if (!provider || !supportsBatchComplete(provider)) {
8459
+ return c.json({ supported, pending: { ...base, status: "unknown", counts: null } });
8460
+ }
8461
+ try {
8462
+ const status = await provider.translationBatchStatus(pending.batchId);
8463
+ return c.json({ supported, pending: { ...base, status: status.status, counts: status.counts } });
8464
+ } catch (e) {
8465
+ return c.json({ supported, pending: { ...base, status: "unknown", counts: null, error: e.message } });
8466
+ }
8467
+ });
8468
+ app.post("/glossary/suggest/batch", (c) => withTranslateLock(async () => {
8469
+ const body = await c.req.json().catch(() => ({}));
8470
+ const s = load();
8471
+ const sources = selectGlossarySources(s, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
8472
+ if (!sources.length) return c.json({ error: "No source strings to scan." }, 400);
8473
+ const aiCfg = loadLocalSettings(projectRoot).ai;
8474
+ let provider;
8475
+ try {
8476
+ provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
8477
+ } catch (e) {
8478
+ return c.json({ error: e.message }, 400);
8479
+ }
8480
+ if (!supportsBatchComplete(provider)) {
8481
+ return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
8482
+ }
8483
+ const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
8484
+ let pending;
8485
+ try {
8486
+ pending = await submitGlossarySuggestBatch(provider, sources, knownTermList(s), batchSize, aiCfg.model, projectRoot);
8487
+ } catch (e) {
8488
+ return c.json({ error: e.message }, 409);
8489
+ }
8490
+ appendLog(projectRoot, {
8491
+ at: (/* @__PURE__ */ new Date()).toISOString(),
8492
+ kind: "glossary",
8493
+ summary: `Submitted glossary suggestion batch ${pending.batchId} (${pending.total} sources)`,
8494
+ model: aiCfg.model
8495
+ });
8496
+ return c.json({ batchId: pending.batchId, total: pending.total });
8497
+ }));
8498
+ app.post("/glossary/suggest/batch/apply", (c) => withTranslateLock(async () => {
8499
+ const pending = loadPendingGlossaryBatch(projectRoot);
8500
+ if (!pending) return c.json({ error: "No pending glossary suggestion batch." }, 404);
8501
+ const aiCfg = loadLocalSettings(projectRoot).ai;
8502
+ let provider;
8503
+ try {
8504
+ provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
8505
+ } catch (e) {
8506
+ return c.json({ error: e.message }, 400);
8507
+ }
8508
+ if (!supportsBatchComplete(provider)) {
8509
+ return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
8510
+ }
8511
+ const outcome = await applyGlossarySuggestBatchResults(load, persist, provider, pending, projectRoot, aiCfg);
8512
+ return c.json(outcome);
8513
+ }));
8514
+ app.post("/glossary/suggest/batch/cancel", async (c) => {
8515
+ const pending = loadPendingGlossaryBatch(projectRoot);
8516
+ if (!pending) return c.json({ error: "No pending glossary suggestion batch." }, 404);
8517
+ const aiCfg = loadLocalSettings(projectRoot).ai;
8518
+ try {
8519
+ const provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
8520
+ if (supportsBatchComplete(provider)) await provider.cancelTranslationBatch(pending.batchId);
8521
+ } catch {
8522
+ }
8523
+ clearPendingGlossaryBatch(projectRoot);
8524
+ return c.json({ canceled: pending.batchId });
8525
+ });
8310
8526
  app.post("/keys/:key/screenshot", async (c) => {
8311
8527
  const key = c.req.param("key");
8312
8528
  const body = await c.req.parseBody();
@@ -8860,7 +9076,7 @@ function createApi(deps) {
8860
9076
  if (signal?.aborted) break;
8861
9077
  const batch = raw;
8862
9078
  const fresh = load();
8863
- const { written, errors } = applyContext(fresh, chunk2, batch.items ?? [], void 0, body.force === true);
9079
+ const { written, errors } = applyContext(fresh, chunk2, batch.items ?? [], body.force === true);
8864
9080
  const usage = provider.takeUsage?.();
8865
9081
  appendLog(projectRoot, {
8866
9082
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -9026,6 +9242,8 @@ var init_api = __esm({
9026
9242
  init_pending_batch();
9027
9243
  init_context_batch_run();
9028
9244
  init_pending_context_batch();
9245
+ init_glossary_batch_run();
9246
+ init_pending_glossary_batch();
9029
9247
  init_estimate();
9030
9248
  init_pricing();
9031
9249
  init_price_fetch();
@@ -9054,7 +9272,7 @@ __export(server_exports, {
9054
9272
  import { Hono as Hono2 } from "hono";
9055
9273
  import { serve } from "@hono/node-server";
9056
9274
  import { fileURLToPath } from "url";
9057
- import { dirname as dirname4, join as join20, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
9275
+ import { dirname as dirname4, join as join21, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
9058
9276
  import { readFile, stat } from "fs/promises";
9059
9277
  import { createServer } from "net";
9060
9278
  import open from "open";
@@ -9110,7 +9328,7 @@ function buildApp(opts) {
9110
9328
  const file = await readFileResponse(target);
9111
9329
  if (file) return file;
9112
9330
  }
9113
- const index = await readFileResponse(join20(root, "index.html"));
9331
+ const index = await readFileResponse(join21(root, "index.html"));
9114
9332
  if (index) return index;
9115
9333
  return c.notFound();
9116
9334
  });
@@ -9180,7 +9398,7 @@ var init_server = __esm({
9180
9398
  init_scanner();
9181
9399
  init_usage();
9182
9400
  here = dirname4(fileURLToPath(import.meta.url));
9183
- DEFAULT_UI_DIR = join20(here, "..", "ui");
9401
+ DEFAULT_UI_DIR = join21(here, "..", "ui");
9184
9402
  MIME = {
9185
9403
  ".html": "text/html; charset=utf-8",
9186
9404
  ".js": "text/javascript; charset=utf-8",
@@ -9212,8 +9430,8 @@ var init_server = __esm({
9212
9430
  // src/server/cli.ts
9213
9431
  init_state();
9214
9432
  init_stats();
9215
- import { resolve as resolve11, dirname as dirname5, join as join21, basename as basename2 } from "path";
9216
- import { readFileSync as readFileSync26, existsSync as existsSync14, mkdirSync as mkdirSync6, cpSync } from "fs";
9433
+ import { resolve as resolve11, dirname as dirname5, join as join22, basename as basename2 } from "path";
9434
+ import { readFileSync as readFileSync27, existsSync as existsSync15, mkdirSync as mkdirSync7, cpSync } from "fs";
9217
9435
  import { fileURLToPath as fileURLToPath2 } from "url";
9218
9436
 
9219
9437
  // src/server/agent-cli.ts
@@ -9344,6 +9562,8 @@ init_batch_run();
9344
9562
  init_pending_batch();
9345
9563
  init_context_batch_run();
9346
9564
  init_pending_context_batch();
9565
+ init_glossary_batch_run();
9566
+ init_pending_glossary_batch();
9347
9567
  init_estimate();
9348
9568
  init_glossary_suggest();
9349
9569
  init_pricing();
@@ -9604,7 +9824,7 @@ function translateSelection(args) {
9604
9824
  }
9605
9825
  function readStdin() {
9606
9826
  try {
9607
- return readFileSync26(0, "utf8");
9827
+ return readFileSync27(0, "utf8");
9608
9828
  } catch {
9609
9829
  return "";
9610
9830
  }
@@ -9777,13 +9997,15 @@ async function runBatch(args) {
9777
9997
  const projectRoot = dirname5(resolve11(args.statePath));
9778
9998
  const pending = loadPendingBatch(projectRoot);
9779
9999
  const ctxPending = loadPendingContextBatch(projectRoot);
9780
- if (!pending && !ctxPending) {
9781
- console.log("No pending batch. Start one with `glotfile translate --batch` or `glotfile build-context --batch`.");
10000
+ const glossPending = loadPendingGlossaryBatch(projectRoot);
10001
+ if (!pending && !ctxPending && !glossPending) {
10002
+ console.log("No pending batch. Start one with `glotfile translate --batch`, `glotfile build-context --batch`, or `glotfile suggest-glossary --batch`.");
9782
10003
  return;
9783
10004
  }
9784
10005
  const action = args.batchAction ?? "status";
9785
10006
  if (pending) await runTranslationBatchAction(args, pending, action, projectRoot);
9786
10007
  if (ctxPending) await runContextBatchAction(args, ctxPending, action, projectRoot);
10008
+ if (glossPending) await runGlossaryBatchAction(args, glossPending, action, projectRoot);
9787
10009
  }
9788
10010
  async function runTranslationBatchAction(args, pending, action, projectRoot) {
9789
10011
  if (action === "cancel") {
@@ -9869,18 +10091,65 @@ async function runContextBatchAction(args, pending, action, projectRoot) {
9869
10091
  if (outcome.retried) console.log(`${outcome.retried} job(s) re-run synchronously (batch entries failed or were malformed).`);
9870
10092
  for (const e of outcome.errors) console.warn(`skip ${e.key}: ${e.error}`);
9871
10093
  }
10094
+ async function runGlossaryBatchAction(args, pending, action, projectRoot) {
10095
+ if (action === "cancel") {
10096
+ let remoteFailed = false;
10097
+ try {
10098
+ const ai2 = loadLocalSettings(projectRoot).ai;
10099
+ const provider2 = makeProvider(ai2);
10100
+ if (supportsBatchComplete(provider2)) {
10101
+ await provider2.cancelTranslationBatch(pending.batchId);
10102
+ } else {
10103
+ remoteFailed = true;
10104
+ }
10105
+ } catch {
10106
+ remoteFailed = true;
10107
+ }
10108
+ clearPendingGlossaryBatch(projectRoot);
10109
+ const suffix = remoteFailed ? " (remote cancel failed \u2014 it will expire server-side)" : "";
10110
+ console.log(`Canceled glossary suggestion batch ${pending.batchId}.${suffix}`);
10111
+ return;
10112
+ }
10113
+ const ai = loadLocalSettings(projectRoot).ai;
10114
+ const provider = makeProviderOrExit(ai);
10115
+ if (!provider) return;
10116
+ if (!supportsBatchComplete(provider)) {
10117
+ console.error(`Pending glossary batch was submitted via anthropic, but the configured provider "${ai.provider}" has no batch support.`);
10118
+ process.exitCode = 1;
10119
+ return;
10120
+ }
10121
+ const status = await provider.translationBatchStatus(pending.batchId);
10122
+ const c = status.counts;
10123
+ console.log(`Glossary suggestion batch ${pending.batchId} (${pending.total} source(s), submitted ${pending.createdAt})`);
10124
+ console.log(` ${status.status} \u2014 ${c.succeeded} succeeded, ${c.processing} processing, ${c.errored} errored, ${c.expired} expired, ${c.canceled} canceled`);
10125
+ if (status.status !== "ended") {
10126
+ if (action === "apply") console.log("Not finished yet \u2014 try again later.");
10127
+ return;
10128
+ }
10129
+ const outcome = await applyGlossarySuggestBatchResults(
10130
+ () => loadState(args.statePath),
10131
+ (s) => saveState(args.statePath, s),
10132
+ provider,
10133
+ pending,
10134
+ projectRoot,
10135
+ ai
10136
+ );
10137
+ console.log(`Found ${outcome.added} new candidate term(s).`);
10138
+ if (outcome.retried) console.log(`${outcome.retried} job(s) re-run synchronously (batch entries failed or were malformed).`);
10139
+ for (const e of outcome.errors) console.warn(`batch job failed: ${e.error}`);
10140
+ }
9872
10141
  function sarifContextFor(statePath) {
9873
10142
  if (detectFormat(statePath) === "split") {
9874
10143
  const dir = splitDirFor(statePath);
9875
- const keysPath = join21(dir, "keys.json");
10144
+ const keysPath = join22(dir, "keys.json");
9876
10145
  return {
9877
10146
  keysUri: `${basename2(dir)}/keys.json`,
9878
- keysRawText: existsSync14(keysPath) ? readFileSync26(keysPath, "utf8") : ""
10147
+ keysRawText: existsSync15(keysPath) ? readFileSync27(keysPath, "utf8") : ""
9879
10148
  };
9880
10149
  }
9881
10150
  return {
9882
10151
  keysUri: basename2(statePath),
9883
- keysRawText: existsSync14(statePath) ? readFileSync26(statePath, "utf8") : ""
10152
+ keysRawText: existsSync15(statePath) ? readFileSync27(statePath, "utf8") : ""
9884
10153
  };
9885
10154
  }
9886
10155
  function printReport(report, format, statePath) {
@@ -9949,7 +10218,7 @@ async function runImportCmd(args) {
9949
10218
  const { runImport: runImport2 } = await Promise.resolve().then(() => (init_run3(), run_exports));
9950
10219
  const projectRoot = args.importSource ? resolve11(args.importSource) : dirname5(resolve11(args.statePath));
9951
10220
  const out = resolve11(projectRoot, "glotfile.json");
9952
- if (existsSync14(out) && !args.importForce) {
10221
+ if (existsSync15(out) && !args.importForce) {
9953
10222
  console.error(`${out} already exists; pass --force to overwrite`);
9954
10223
  process.exitCode = 1;
9955
10224
  return;
@@ -10155,6 +10424,31 @@ async function runSuggestGlossary(args) {
10155
10424
  const system = buildGlossarySuggestSystemPrompt();
10156
10425
  const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
10157
10426
  const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
10427
+ if (args.batch) {
10428
+ if (!supportsBatchComplete(provider)) {
10429
+ console.error(`Provider "${aiCfg.provider}" does not support batch mode. Currently anthropic only.`);
10430
+ process.exitCode = 1;
10431
+ return;
10432
+ }
10433
+ let pending;
10434
+ try {
10435
+ pending = await submitGlossarySuggestBatch(provider, sources, known, batchSize, aiCfg.model, projectRoot);
10436
+ } catch (e) {
10437
+ console.error(e.message);
10438
+ process.exitCode = 1;
10439
+ return;
10440
+ }
10441
+ appendLog(projectRoot, {
10442
+ at: (/* @__PURE__ */ new Date()).toISOString(),
10443
+ kind: "glossary",
10444
+ summary: `Submitted glossary suggestion batch ${pending.batchId} (${pending.total} sources)`,
10445
+ model: aiCfg.model,
10446
+ system
10447
+ });
10448
+ console.log(`Submitted glossary suggestion batch ${pending.batchId} \u2014 ${pending.total} source string(s) at 50% batch pricing.`);
10449
+ console.log("Check progress with `glotfile batch`; it applies results automatically when finished.");
10450
+ return;
10451
+ }
10158
10452
  const chunks = [];
10159
10453
  for (let i = 0; i < sources.length; i += batchSize) chunks.push(sources.slice(i, i + batchSize));
10160
10454
  const all = [];
@@ -10259,19 +10553,19 @@ function runSplit(args) {
10259
10553
  `Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
10260
10554
  );
10261
10555
  }
10262
- var SKILL_SRC = join21(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
10556
+ var SKILL_SRC = join22(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
10263
10557
  function runSkill(args) {
10264
10558
  if (args.print) {
10265
- console.log(readFileSync26(join21(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
10559
+ console.log(readFileSync27(join22(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
10266
10560
  return;
10267
10561
  }
10268
10562
  const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
10269
- if (existsSync14(dest) && !args.importForce) {
10563
+ if (existsSync15(dest) && !args.importForce) {
10270
10564
  console.error(`${dest} already exists; pass --force to overwrite`);
10271
10565
  process.exitCode = 1;
10272
10566
  return;
10273
10567
  }
10274
- mkdirSync6(dirname5(dest), { recursive: true });
10568
+ mkdirSync7(dirname5(dest), { recursive: true });
10275
10569
  cpSync(SKILL_SRC, dest, { recursive: true });
10276
10570
  console.log(`Installed the glotfile skill to ${dest}. Restart Claude Code to pick it up.`);
10277
10571
  }
@@ -10564,12 +10858,13 @@ var COMMAND_HELP = {
10564
10858
  },
10565
10859
  "suggest-glossary": {
10566
10860
  summary: "AI-scan source strings for candidate glossary terms (adds a review queue; existing terms are skipped).",
10567
- usage: "glotfile suggest-glossary [--key <glob>] [--limit <n>] [--since <date>] [--estimate]",
10861
+ usage: "glotfile suggest-glossary [--key <glob>] [--limit <n>] [--since <date>] [--estimate] [--batch]",
10568
10862
  options: [
10569
10863
  ["--key <glob>", "Only scan keys matching this glob"],
10570
10864
  ["--limit <n>", "Scan at most n source strings"],
10571
10865
  ["--since <date>", "Only keys added since this date"],
10572
- ["--estimate", "Print batches, tokens and estimated cost without scanning"]
10866
+ ["--estimate", "Print batches, tokens and estimated cost without scanning"],
10867
+ ["--batch", "Submit via the provider's batch API (50% cost, async; anthropic only)"]
10573
10868
  ]
10574
10869
  },
10575
10870
  scan: {
@@ -10704,8 +10999,8 @@ ${formatOpts([...options, ...GLOBAL_OPTS])}`);
10704
10999
  );
10705
11000
  }
10706
11001
  function printVersion() {
10707
- const pkgPath = join21(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
10708
- console.log(JSON.parse(readFileSync26(pkgPath, "utf8")).version);
11002
+ const pkgPath = join22(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
11003
+ console.log(JSON.parse(readFileSync27(pkgPath, "utf8")).version);
10709
11004
  }
10710
11005
  async function main(argv) {
10711
11006
  const args = parseArgs(argv);