@walkinissue/angy 0.2.17 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -3,10 +3,11 @@ import { json as json4 } from "@sveltejs/kit";
3
3
 
4
4
  // src/lib/server/commit.ts
5
5
  import { json } from "@sveltejs/kit";
6
+ import { basename as basename3 } from "node:path";
6
7
 
7
8
  // src/lib/server/catalog.ts
8
9
  import { constants as fsConstants } from "node:fs";
9
- import { access, copyFile, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
10
+ import { access, copyFile, readFile as readFile2, rename, rm, writeFile as writeFile2 } from "node:fs/promises";
10
11
  import { dirname as dirname2, extname as extname2, join, basename as basename2 } from "node:path";
11
12
  import gettextParser from "gettext-parser";
12
13
  import Fuse from "fuse.js";
@@ -102,6 +103,21 @@ function validateLocaleAliasUsage(sourceLocale, targetLocale) {
102
103
  throw new Error('[angy] targetLocale cannot be "base". Use an explicit target locale or "working".');
103
104
  }
104
105
  }
106
+ function validateCatalogPathSemantics(next) {
107
+ const baseLocale = inferLocaleFromCatalogPath(next.basePoPath);
108
+ const workingLocale = inferLocaleFromCatalogPath(next.workingPoPath);
109
+ const expectedWorkingLocale = `${next.targetLocale}-working`;
110
+ if (baseLocale !== next.targetLocale) {
111
+ throw new Error(
112
+ `[angy] basePoPath must point to the ${next.targetLocale}.po catalog. Received "${baseLocale ?? "unknown"}".`
113
+ );
114
+ }
115
+ if (workingLocale !== expectedWorkingLocale) {
116
+ throw new Error(
117
+ `[angy] workingPoPath must point to the ${expectedWorkingLocale}.po catalog. Received "${workingLocale ?? "unknown"}".`
118
+ );
119
+ }
120
+ }
105
121
  function suspendWorkingCatalogWatch(path, delayMs = 800) {
106
122
  const normalizedPath = normalizeFsPath(path);
107
123
  for (const controller of workingCatalogWatchControllers) {
@@ -124,12 +140,14 @@ function resolveConfiguredLocaleAliases(next) {
124
140
  const resolvedSourceLocale = resolveLocaleAlias(rawSourceLocale, next);
125
141
  const resolvedTargetLocale = resolveLocaleAlias(rawTargetLocale, next);
126
142
  const usesDefaultSystemMessage = next.systemMessage === buildDefaultSystemMessage(rawSourceLocale, rawTargetLocale);
127
- return {
143
+ const resolved = {
128
144
  ...next,
129
145
  sourceLocale: resolvedSourceLocale,
130
146
  targetLocale: resolvedTargetLocale,
131
147
  systemMessage: usesDefaultSystemMessage ? buildDefaultSystemMessage(resolvedSourceLocale, resolvedTargetLocale) : next.systemMessage
132
148
  };
149
+ validateCatalogPathSemantics(resolved);
150
+ return resolved;
133
151
  }
134
152
  function completeAngyConfig(input) {
135
153
  assertNonEmptyString(input.basePoPath, "basePoPath");
@@ -137,8 +155,8 @@ function completeAngyConfig(input) {
137
155
  assertNonEmptyString(input.sourceLocale, "sourceLocale");
138
156
  assertNonEmptyString(input.targetLocale, "targetLocale");
139
157
  validateLocaleAliasUsage(input.sourceLocale, input.targetLocale);
140
- if (typeof input.apiKey !== "string") {
141
- throw new Error(`[angy] apiKey is required and must be a string. Use an empty string to disable suggestions.`);
158
+ if (typeof input.apiKey !== "string" && typeof input.apiKey !== "undefined") {
159
+ throw new Error(`[angy] apiKey must be a string when provided. Use an empty string to disable suggestions.`);
142
160
  }
143
161
  validateRoutePath(input.routePath);
144
162
  validateWatchIgnore(input.watchIgnore);
@@ -149,7 +167,7 @@ function completeAngyConfig(input) {
149
167
  sourceLocale: input.sourceLocale,
150
168
  targetLocale: input.targetLocale,
151
169
  routePath: input.routePath ?? "/api/translations",
152
- apiKey: input.apiKey,
170
+ apiKey: input.apiKey ?? "",
153
171
  systemMessage: input.systemMessage ?? buildDefaultSystemMessage(input.sourceLocale, input.targetLocale),
154
172
  suggestionModel: input.suggestionModel ?? "gpt-4.1-mini",
155
173
  watchIgnore: input.watchIgnore ?? ["**/en-working.po"],
@@ -157,7 +175,7 @@ function completeAngyConfig(input) {
157
175
  };
158
176
  }
159
177
  function defineAngyConfig(config2) {
160
- return completeAngyConfig(config2);
178
+ return resolveConfiguredLocaleAliases(completeAngyConfig(config2));
161
179
  }
162
180
  async function fileExists(path) {
163
181
  try {
@@ -177,13 +195,19 @@ async function loadTsConfigModule(path) {
177
195
  const tempPath = `${path}.angy.tmp.mjs`;
178
196
  await writeFile(tempPath, transformed.code, "utf8");
179
197
  try {
180
- return await import(`${pathToFileURL(tempPath).href}?t=${Date.now()}`);
198
+ return await import(
199
+ /* @vite-ignore */
200
+ `${pathToFileURL(tempPath).href}?t=${Date.now()}`
201
+ );
181
202
  } finally {
182
203
  await unlink(tempPath).catch(() => void 0);
183
204
  }
184
205
  }
185
206
  async function loadJsConfigModule(path) {
186
- return import(pathToFileURL(path).href);
207
+ return import(
208
+ /* @vite-ignore */
209
+ pathToFileURL(path).href
210
+ );
187
211
  }
188
212
  async function loadAngyConfigFromRoot(root) {
189
213
  if (loadedConfigRoot === root) {
@@ -207,6 +231,14 @@ async function loadAngyConfigFromRoot(root) {
207
231
 
208
232
  // src/lib/server/catalog.ts
209
233
  var runtimeTranslations = /* @__PURE__ */ new Map();
234
+ var CatalogIntegrityError = class extends Error {
235
+ issues;
236
+ constructor(message, issues) {
237
+ super(message);
238
+ this.name = "CatalogIntegrityError";
239
+ this.issues = issues;
240
+ }
241
+ };
210
242
  function catalogEntryKey(msgid, msgctxt) {
211
243
  return `${msgctxt ?? ""}::${msgid}`;
212
244
  }
@@ -280,20 +312,9 @@ function createFuse(entries) {
280
312
  function buildEntryMap(entries) {
281
313
  return new Map(entries.map((entry) => [catalogEntryKey(entry.msgid, entry.msgctxt), entry]));
282
314
  }
283
- function applyWorkingState(baseEntry, workingEntry) {
284
- if (!workingEntry) {
285
- return baseEntry;
286
- }
287
- return {
288
- ...baseEntry,
289
- msgstr: workingEntry.msgstr,
290
- flags: workingEntry.flags,
291
- previous: workingEntry.previous,
292
- obsolete: workingEntry.obsolete,
293
- hasTranslation: workingEntry.hasTranslation,
294
- isFuzzy: workingEntry.isFuzzy,
295
- translationOrigin: workingEntry.translationOrigin
296
- };
315
+ function getEntryValue(entry) {
316
+ if (!entry?.msgstr?.length) return "";
317
+ return entry.msgstr.find((item) => item.trim().length > 0)?.trim() ?? "";
297
318
  }
298
319
  async function fileExists2(path) {
299
320
  try {
@@ -303,27 +324,9 @@ async function fileExists2(path) {
303
324
  return false;
304
325
  }
305
326
  }
306
- async function readCatIndex(catalog) {
307
- const { basePoPath, workingPoPath } = getTranslationHelperConfig();
308
- let path = "";
309
- if (catalog === "base") {
310
- path = basePoPath;
311
- } else {
312
- const exists = await fileExists2(workingPoPath);
313
- if (!exists) return null;
314
- path = workingPoPath;
315
- }
316
- const raw = await readFile2(path);
317
- const parsed = gettextParser.po.parse(raw);
318
- const entries = flattenPoEntries(parsed, catalog);
319
- return {
320
- entries,
321
- fuse: createFuse(entries)
322
- };
323
- }
324
327
  async function readParsedCatalog(catalog) {
325
- const { basePoPath, workingPoPath } = getTranslationHelperConfig();
326
- const path = catalog === "base" ? basePoPath : workingPoPath;
328
+ const { basePoPath } = getTranslationHelperConfig();
329
+ const path = catalog === "base" ? basePoPath : await getEffectiveWorkingPoPath();
327
330
  if (catalog === "working") {
328
331
  const exists = await fileExists2(path);
329
332
  if (!exists) return null;
@@ -338,15 +341,183 @@ async function ensureWorkingCatalog() {
338
341
  await copyFile(basePoPath, workingPoPath);
339
342
  }
340
343
  }
344
+ function getWorkingDraftPoPath() {
345
+ const { workingPoPath } = getTranslationHelperConfig();
346
+ const extension = extname2(workingPoPath);
347
+ const stem = workingPoPath.slice(0, workingPoPath.length - extension.length);
348
+ return `${stem}.angy-draft${extension}`;
349
+ }
350
+ async function getEffectiveWorkingPoPath() {
351
+ const draftPoPath = getWorkingDraftPoPath();
352
+ if (await fileExists2(draftPoPath)) {
353
+ return draftPoPath;
354
+ }
355
+ return getTranslationHelperConfig().workingPoPath;
356
+ }
357
+ async function ensureWorkingDraftCatalog() {
358
+ await ensureWorkingCatalog();
359
+ const { workingPoPath } = getTranslationHelperConfig();
360
+ const draftPoPath = getWorkingDraftPoPath();
361
+ const exists = await fileExists2(draftPoPath);
362
+ if (!exists) {
363
+ await copyFile(workingPoPath, draftPoPath);
364
+ }
365
+ }
341
366
  function removeFuzzyFlag(flagString) {
342
367
  if (!flagString) return "";
343
368
  return flagString.split(",").map((item) => item.trim()).filter(Boolean).filter((item) => item !== "fuzzy").join(", ");
344
369
  }
345
370
  async function writeWorkingCatalog(parsed) {
371
+ await ensureWorkingDraftCatalog();
372
+ const draftPoPath = getWorkingDraftPoPath();
373
+ const compiled = gettextParser.po.compile(parsed);
374
+ await writeFile2(draftPoPath, compiled);
375
+ }
376
+ async function promoteWorkingDraftToRuntime() {
377
+ await ensureWorkingDraftCatalog();
346
378
  const { workingPoPath } = getTranslationHelperConfig();
379
+ const draftPoPath = getWorkingDraftPoPath();
380
+ const publishPath = `${workingPoPath}.angy-publish`;
381
+ const compiled = await readFile2(draftPoPath);
347
382
  suspendWorkingCatalogWatch(workingPoPath);
348
- const compiled = gettextParser.po.compile(parsed);
349
- await writeFile2(workingPoPath, compiled);
383
+ await writeFile2(publishPath, compiled);
384
+ await rm(workingPoPath, { force: true });
385
+ await rename(publishPath, workingPoPath);
386
+ }
387
+ function collectCatalogIntegrityIssues(baseEntries, workingEntries) {
388
+ const issues = [];
389
+ const baseMap = buildEntryMap(baseEntries);
390
+ const workingMap = buildEntryMap(workingEntries);
391
+ for (const baseEntry of baseEntries) {
392
+ const key = catalogEntryKey(baseEntry.msgid, baseEntry.msgctxt);
393
+ const workingEntry = workingMap.get(key);
394
+ if (!workingEntry) {
395
+ issues.push({
396
+ type: "key_mismatch",
397
+ msgid: baseEntry.msgid,
398
+ msgctxt: baseEntry.msgctxt,
399
+ reason: "missing_in_working"
400
+ });
401
+ continue;
402
+ }
403
+ const baseValue = getEntryValue(baseEntry);
404
+ const workingValue = getEntryValue(workingEntry);
405
+ if (baseValue && !workingValue) {
406
+ issues.push({
407
+ type: "missing_working_translation",
408
+ msgid: baseEntry.msgid,
409
+ msgctxt: baseEntry.msgctxt,
410
+ baseValue
411
+ });
412
+ }
413
+ }
414
+ for (const workingEntry of workingEntries) {
415
+ const key = catalogEntryKey(workingEntry.msgid, workingEntry.msgctxt);
416
+ if (!baseMap.has(key)) {
417
+ issues.push({
418
+ type: "key_mismatch",
419
+ msgid: workingEntry.msgid,
420
+ msgctxt: workingEntry.msgctxt,
421
+ reason: "missing_in_base"
422
+ });
423
+ }
424
+ }
425
+ return issues;
426
+ }
427
+ function assertCatalogIntegrity(baseEntries, workingEntries) {
428
+ const issues = collectCatalogIntegrityIssues(baseEntries, workingEntries);
429
+ if (issues.length) {
430
+ throw new CatalogIntegrityError(
431
+ "Working catalog is out of sync with the base catalog. Regenerate or replace the working catalog before using Angy.",
432
+ issues
433
+ );
434
+ }
435
+ }
436
+ function collectRotationImpact(baseEntries, workingMap) {
437
+ const impacted = [];
438
+ for (const baseEntry of baseEntries) {
439
+ const workingEntry = workingMap.get(catalogEntryKey(baseEntry.msgid, baseEntry.msgctxt));
440
+ if (!workingEntry) continue;
441
+ const baseValue = getEntryValue(baseEntry);
442
+ const workingValue = getEntryValue(workingEntry);
443
+ if (!workingValue) continue;
444
+ if (baseValue === workingValue) continue;
445
+ impacted.push({
446
+ msgid: baseEntry.msgid,
447
+ msgctxt: baseEntry.msgctxt,
448
+ baseValue,
449
+ workingValue
450
+ });
451
+ }
452
+ return impacted;
453
+ }
454
+ function resolveTranslationState(baseEntry, workingEntry) {
455
+ const baseValue = getEntryValue(baseEntry);
456
+ const workingValue = getEntryValue(workingEntry);
457
+ const activeEntry = workingEntry && workingValue ? workingEntry : baseEntry;
458
+ const fuzzy = Boolean(activeEntry.isFuzzy);
459
+ let translationOrigin = "base";
460
+ let translationStatus = "none";
461
+ let effectiveEntry = baseEntry;
462
+ if (workingEntry && workingValue) {
463
+ effectiveEntry = workingEntry;
464
+ if (!baseValue || workingValue !== baseValue) {
465
+ translationOrigin = "working";
466
+ translationStatus = "working";
467
+ } else {
468
+ translationOrigin = "base";
469
+ translationStatus = "base";
470
+ }
471
+ } else if (baseValue) {
472
+ effectiveEntry = baseEntry;
473
+ translationOrigin = "base";
474
+ translationStatus = "base";
475
+ }
476
+ if (fuzzy) {
477
+ translationStatus = "fuzzy";
478
+ }
479
+ return {
480
+ effectiveEntry,
481
+ baseValue,
482
+ workingValue,
483
+ translationOrigin,
484
+ translationStatus,
485
+ hasTranslation: Boolean(getEntryValue(effectiveEntry)),
486
+ isFuzzy: fuzzy
487
+ };
488
+ }
489
+ async function readCatalogPair(options) {
490
+ if (options?.ensureWorking) {
491
+ await ensureWorkingCatalog();
492
+ }
493
+ const [baseParsed, workingParsed] = await Promise.all([
494
+ readParsedCatalog("base"),
495
+ readParsedCatalog("working")
496
+ ]);
497
+ if (!baseParsed || !workingParsed) {
498
+ throw new Error("Unable to load base and working catalogs.");
499
+ }
500
+ const baseEntries = flattenPoEntries(baseParsed, "base");
501
+ const workingEntries = flattenPoEntries(workingParsed, "working");
502
+ assertCatalogIntegrity(baseEntries, workingEntries);
503
+ const baseMap = buildEntryMap(baseEntries);
504
+ const workingMap = buildEntryMap(workingEntries);
505
+ const rotationImpact = collectRotationImpact(baseEntries, workingMap);
506
+ return {
507
+ baseEntries,
508
+ workingEntries,
509
+ baseMap,
510
+ workingMap,
511
+ rotationImpact
512
+ };
513
+ }
514
+ async function getRotationPreflight() {
515
+ const { rotationImpact } = await readCatalogPair({ ensureWorking: true });
516
+ return {
517
+ safe: true,
518
+ status: "ok",
519
+ affected: rotationImpact
520
+ };
350
521
  }
351
522
  function timestampForBackup() {
352
523
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -357,18 +528,21 @@ function buildBackupPath(path, label) {
357
528
  const stem = basename2(path, extension);
358
529
  return join(directory, `${stem}.${label}.${timestampForBackup()}${extension}`);
359
530
  }
360
- async function rotateCatalogs() {
531
+ async function rotateCatalogs(options) {
361
532
  const { basePoPath, workingPoPath } = getTranslationHelperConfig();
362
- const workingExists = await fileExists2(workingPoPath);
533
+ const draftPoPath = getWorkingDraftPoPath();
534
+ const effectiveWorkingPoPath = await getEffectiveWorkingPoPath();
535
+ const workingExists = await fileExists2(effectiveWorkingPoPath);
363
536
  if (!workingExists) {
364
537
  return { ok: false, error: "Working catalog does not exist" };
365
538
  }
366
539
  const baseBackupPath = buildBackupPath(basePoPath, "base-backup");
367
540
  const workingBackupPath = buildBackupPath(workingPoPath, "working-backup");
368
541
  await copyFile(basePoPath, baseBackupPath);
369
- await copyFile(workingPoPath, workingBackupPath);
370
- await copyFile(workingPoPath, basePoPath);
542
+ await copyFile(effectiveWorkingPoPath, workingBackupPath);
543
+ await copyFile(effectiveWorkingPoPath, basePoPath);
371
544
  await copyFile(basePoPath, workingPoPath);
545
+ await copyFile(workingPoPath, draftPoPath);
372
546
  return {
373
547
  ok: true,
374
548
  basePoPath,
@@ -384,10 +558,8 @@ function runtimeKey(msgid, msgctxt) {
384
558
  }
385
559
  async function writeTranslationToWorkingCatalog(resolvedMsgid, resolvedMsgctxt, translationValue) {
386
560
  await ensureWorkingCatalog();
387
- const [baseParsed, workingParsed] = await Promise.all([
388
- readParsedCatalog("base"),
389
- readParsedCatalog("working")
390
- ]);
561
+ await readCatalogPair({ ensureWorking: true });
562
+ const [baseParsed, workingParsed] = await Promise.all([readParsedCatalog("base"), readParsedCatalog("working")]);
391
563
  if (!baseParsed || !workingParsed) {
392
564
  return { ok: false, error: "Unable to load catalogs" };
393
565
  }
@@ -409,11 +581,12 @@ async function writeTranslationToWorkingCatalog(resolvedMsgid, resolvedMsgctxt,
409
581
  entry.comments.flag = removeFuzzyFlag(entry.comments.flag);
410
582
  await writeWorkingCatalog(workingParsed);
411
583
  runtimeTranslations.set(runtimeKey(resolvedMsgid, resolvedMsgctxt), translationValue);
584
+ const workingCatalogName = inferLocaleFromCatalogPath(getTranslationHelperConfig().workingPoPath) ?? "working";
412
585
  return {
413
586
  ok: true,
414
587
  msgid: resolvedMsgid,
415
588
  msgctxt: resolvedMsgctxt,
416
- workingCatalog: "en-working.po"
589
+ workingCatalog: workingCatalogName
417
590
  };
418
591
  }
419
592
  function cloneEntryForWorking(baseEntry, msgid, msgctxt) {
@@ -443,6 +616,22 @@ async function handleCommitBatch(request) {
443
616
  { status: 400 }
444
617
  );
445
618
  }
619
+ try {
620
+ await readCatalogPair({ ensureWorking: true });
621
+ } catch (error) {
622
+ if (error instanceof CatalogIntegrityError) {
623
+ return json(
624
+ {
625
+ success: false,
626
+ error: error.message,
627
+ code: "catalog_integrity_error",
628
+ issues: error.issues
629
+ },
630
+ { status: 409 }
631
+ );
632
+ }
633
+ throw error;
634
+ }
446
635
  const results = [];
447
636
  for (const item of items) {
448
637
  const resolvedMsgid = item.resolvedMsgid?.trim();
@@ -490,7 +679,7 @@ async function handleCommitBatch(request) {
490
679
  }
491
680
  return json({
492
681
  success: true,
493
- message: `Committed ${results.length} translations to en-working.po`,
682
+ message: `Committed ${results.length} translations to Angy draft working catalog`,
494
683
  results
495
684
  });
496
685
  }
@@ -525,21 +714,85 @@ async function handleCommit(request) {
525
714
  }
526
715
  return json({
527
716
  success: true,
528
- message: "Translation written to en-working.po",
717
+ message: "Translation written to Angy draft working catalog",
529
718
  msgid: result.msgid,
530
719
  msgctxt: result.msgctxt,
531
720
  workingCatalog: result.workingCatalog
532
721
  });
533
722
  }
534
- async function handleRotateCatalogs() {
535
- const result = await rotateCatalogs();
723
+ async function handlePromoteWorkingPreview() {
724
+ try {
725
+ await readCatalogPair({ ensureWorking: true });
726
+ await promoteWorkingDraftToRuntime();
727
+ return json({
728
+ success: true,
729
+ message: `Promoted Angy draft into ${basename3(getTranslationHelperConfig().workingPoPath)}`
730
+ });
731
+ } catch (error) {
732
+ if (error instanceof CatalogIntegrityError) {
733
+ return json(
734
+ {
735
+ success: false,
736
+ error: error.message,
737
+ code: "catalog_integrity_error",
738
+ issues: error.issues
739
+ },
740
+ { status: 409 }
741
+ );
742
+ }
743
+ throw error;
744
+ }
745
+ }
746
+ async function handleRotatePreflight() {
747
+ try {
748
+ const preflight = await getRotationPreflight();
749
+ return json({
750
+ success: true,
751
+ safe: preflight.safe,
752
+ status: preflight.status,
753
+ affected: preflight.affected
754
+ });
755
+ } catch (error) {
756
+ if (error instanceof CatalogIntegrityError) {
757
+ return json(
758
+ {
759
+ success: false,
760
+ error: error.message,
761
+ code: "catalog_integrity_error",
762
+ issues: error.issues
763
+ },
764
+ { status: 409 }
765
+ );
766
+ }
767
+ throw error;
768
+ }
769
+ }
770
+ async function handleRotateCatalogs(request) {
771
+ const data = await request.json().catch(() => null);
772
+ const confirmDestructive = data?.confirmDestructive === true;
773
+ const preflight = await getRotationPreflight();
774
+ if (!preflight.safe && !confirmDestructive) {
775
+ return json(
776
+ {
777
+ success: false,
778
+ error: "Catalog rotation would overwrite working-only translations. Confirm rotation explicitly to continue.",
779
+ code: "rotation_confirmation_required",
780
+ status: preflight.status,
781
+ affected: preflight.affected
782
+ },
783
+ { status: 409 }
784
+ );
785
+ }
786
+ const result = await rotateCatalogs({ allowOutOfSync: confirmDestructive });
536
787
  if (!result.ok) {
537
788
  return json(
538
789
  {
539
790
  success: false,
540
- error: result.error
791
+ error: result.error,
792
+ status: "status" in result ? result.status : void 0,
793
+ affected: "affected" in result ? result.affected : void 0
541
794
  },
542
- { status: 400 }
795
+ { status: "status" in result && result.status === "out_of_sync" ? 409 : 400 }
543
796
  );
544
797
  }
545
798
  return json({
@@ -725,13 +978,16 @@ function buildAlternativePool(topDirectAlternatives, sharedReferenceAlternatives
725
978
  return alternatives.slice(0, MAX_ALTERNATIVES);
726
979
  }
727
980
  async function findTranslationContext(key, currentPath) {
728
- const baseIndex = await readCatIndex("base");
729
- if (!baseIndex) return null;
730
- const workingIndex = await readCatIndex("working");
731
- const workingMap = workingIndex ? buildEntryMap(workingIndex.entries) : /* @__PURE__ */ new Map();
732
- const effectiveEntries = baseIndex.entries.map(
733
- (entry) => applyWorkingState(entry, workingMap.get(entryKey(entry.msgid, entry.msgctxt)))
734
- );
981
+ const catalogPair = await readCatalogPair({ ensureWorking: true });
982
+ const effectiveEntries = catalogPair.baseEntries.map((entry) => {
983
+ const state = resolveTranslationState(entry, catalogPair.workingMap.get(entryKey(entry.msgid, entry.msgctxt)));
984
+ return {
985
+ ...state.effectiveEntry,
986
+ hasTranslation: state.hasTranslation,
987
+ isFuzzy: state.isFuzzy,
988
+ translationOrigin: state.translationOrigin
989
+ };
990
+ });
735
991
  const effectiveIndex = {
736
992
  entries: effectiveEntries,
737
993
  fuse: createFuse(effectiveEntries)
@@ -789,47 +1045,48 @@ async function findTranslationContext(key, currentPath) {
789
1045
  untranslatedSimilarityAlternatives
790
1046
  ),
791
1047
  routeReference,
792
- workingMap
1048
+ baseMap: catalogPair.baseMap,
1049
+ workingMap: catalogPair.workingMap,
1050
+ catalogState: {
1051
+ status: "ok",
1052
+ affectedCount: catalogPair.rotationImpact.length
1053
+ }
793
1054
  };
794
1055
  }
795
1056
  function toPublicEntry(entry, workingEntry) {
796
- const effective = workingEntry ?? entry;
797
- const baseValue = entry.msgstr[0] ?? "";
798
- const workingValue = workingEntry?.msgstr[0] ?? "";
799
- const workingDiffersFromBase = Boolean(workingEntry?.hasTranslation) && workingValue !== baseValue;
1057
+ const state = resolveTranslationState(entry, workingEntry);
800
1058
  return {
801
1059
  msgid: entry.msgid,
802
1060
  msgctxt: entry.msgctxt,
803
1061
  msgidPlural: entry.msgidPlural,
804
- msgstr: effective.msgstr,
1062
+ msgstr: state.effectiveEntry.msgstr,
805
1063
  references: entry.references,
806
1064
  extractedComments: entry.extractedComments,
807
- flags: effective.flags,
808
- previous: effective.previous,
809
- obsolete: effective.obsolete,
810
- hasTranslation: effective.hasTranslation,
811
- isFuzzy: effective.isFuzzy,
812
- isCommittedToWorking: workingDiffersFromBase,
813
- matchesTargetTranslation: Boolean(effective.hasTranslation) && (!workingEntry?.hasTranslation || workingValue === baseValue),
814
- translationOrigin: workingDiffersFromBase ? "working" : "base"
1065
+ flags: state.effectiveEntry.flags,
1066
+ previous: state.effectiveEntry.previous,
1067
+ obsolete: state.effectiveEntry.obsolete,
1068
+ hasTranslation: state.hasTranslation,
1069
+ isFuzzy: state.isFuzzy,
1070
+ isCommittedToWorking: state.translationOrigin === "working",
1071
+ matchesTargetTranslation: state.translationOrigin === "base" && state.hasTranslation,
1072
+ translationOrigin: state.translationOrigin,
1073
+ translationStatus: state.translationStatus
815
1074
  };
816
1075
  }
817
1076
  function toPublicAlternative(entry, score, workingEntry) {
818
- const effective = workingEntry ?? entry;
819
- const baseValue = entry.msgstr[0] ?? "";
820
- const workingValue = workingEntry?.msgstr[0] ?? "";
821
- const workingDiffersFromBase = Boolean(workingEntry?.hasTranslation) && workingValue !== baseValue;
1077
+ const state = resolveTranslationState(entry, workingEntry);
822
1078
  return {
823
1079
  msgid: entry.msgid,
824
1080
  msgctxt: entry.msgctxt,
825
1081
  score,
826
1082
  references: entry.references,
827
- msgstr: effective.msgstr,
828
- hasTranslation: effective.hasTranslation,
829
- isFuzzy: effective.isFuzzy,
830
- isCommittedToWorking: workingDiffersFromBase,
831
- matchesTargetTranslation: Boolean(effective.hasTranslation) && (!workingEntry?.hasTranslation || workingValue === baseValue),
832
- translationOrigin: workingDiffersFromBase ? "working" : "base"
1083
+ msgstr: state.effectiveEntry.msgstr,
1084
+ hasTranslation: state.hasTranslation,
1085
+ isFuzzy: state.isFuzzy,
1086
+ isCommittedToWorking: state.translationOrigin === "working",
1087
+ matchesTargetTranslation: state.translationOrigin === "base" && state.hasTranslation,
1088
+ translationOrigin: state.translationOrigin,
1089
+ translationStatus: state.translationStatus
833
1090
  };
834
1091
  }
835
1092
  async function handleContext(request) {
@@ -845,7 +1102,23 @@ async function handleContext(request) {
845
1102
  { status: 400 }
846
1103
  );
847
1104
  }
848
- const baseFound = await findTranslationContext(key, currentPath);
1105
+ let baseFound;
1106
+ try {
1107
+ baseFound = await findTranslationContext(key, currentPath);
1108
+ } catch (error) {
1109
+ if (error instanceof CatalogIntegrityError) {
1110
+ return json2(
1111
+ {
1112
+ success: false,
1113
+ error: error.message,
1114
+ code: "catalog_integrity_error",
1115
+ issues: error.issues
1116
+ },
1117
+ { status: 409 }
1118
+ );
1119
+ }
1120
+ throw error;
1121
+ }
849
1122
  if (!baseFound) {
850
1123
  return json2(
851
1124
  {
@@ -855,7 +1128,7 @@ async function handleContext(request) {
855
1128
  { status: 404 }
856
1129
  );
857
1130
  }
858
- const bestBase = baseFound.best.entry;
1131
+ const bestBase = baseFound.baseMap.get(entryKey(baseFound.best.entry.msgid, baseFound.best.entry.msgctxt)) ?? baseFound.best.entry;
859
1132
  const bestWorking = baseFound.workingMap.get(entryKey(bestBase.msgid, bestBase.msgctxt));
860
1133
  return json2({
861
1134
  success: true,
@@ -863,10 +1136,12 @@ async function handleContext(request) {
863
1136
  score: baseFound.best.score,
864
1137
  via: baseFound.best.variant
865
1138
  },
1139
+ catalogState: baseFound.catalogState,
866
1140
  entry: toPublicEntry(bestBase, bestWorking),
867
1141
  alternatives: baseFound.alternatives.map((item) => {
868
- const workingAlt = baseFound.workingMap.get(entryKey(item.entry.msgid, item.entry.msgctxt));
869
- return toPublicAlternative(item.entry, item.score, workingAlt);
1142
+ const baseAlt = baseFound.baseMap.get(entryKey(item.entry.msgid, item.entry.msgctxt)) ?? item.entry;
1143
+ const workingAlt = baseFound.workingMap.get(entryKey(baseAlt.msgid, baseAlt.msgctxt));
1144
+ return toPublicAlternative(baseAlt, item.score, workingAlt);
870
1145
  })
871
1146
  });
872
1147
  }
@@ -1078,7 +1353,13 @@ async function handleTranslationRequest(request, url, options) {
1078
1353
  return handleSuggestions(request);
1079
1354
  }
1080
1355
  if (intent === "rotate-catalogs") {
1081
- return handleRotateCatalogs();
1356
+ return handleRotateCatalogs(request);
1357
+ }
1358
+ if (intent === "rotate-preflight") {
1359
+ return handleRotatePreflight();
1360
+ }
1361
+ if (intent === "promote-working-preview") {
1362
+ return handlePromoteWorkingPreview();
1082
1363
  }
1083
1364
  return handleCommit(request);
1084
1365
  }
@@ -1087,8 +1368,14 @@ function createAngyHandler(options) {
1087
1368
  }
1088
1369
  var handler = createAngyHandler();
1089
1370
  export {
1371
+ CatalogIntegrityError,
1372
+ collectCatalogIntegrityIssues,
1373
+ collectRotationImpact,
1090
1374
  defineAngyConfig,
1375
+ getRotationPreflight,
1091
1376
  handleTranslationRequest,
1092
- handler
1377
+ handler,
1378
+ readCatalogPair,
1379
+ resolveTranslationState
1093
1380
  };
1094
1381
  //# sourceMappingURL=server.js.map