@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/README.md +120 -11
- package/dist/client/Angy.svelte +751 -546
- package/dist/client/RotationWarningDialog.svelte +177 -0
- package/dist/client/TranslationHelperForm.svelte +111 -61
- package/dist/client/VibeTooltip.svelte +18 -14
- package/dist/client/dragItem.ts +65 -39
- package/dist/client/toggleQA.shared.ts +23 -15
- package/dist/client/translationDrafts.ts +59 -0
- package/dist/plugin.js +31 -8
- package/dist/server/types.ts +30 -8
- package/dist/server.d.ts +2 -2
- package/dist/server.js +383 -96
- package/dist/server.js.map +3 -3
- package/package.json +2 -2
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
284
|
-
if (!
|
|
285
|
-
|
|
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
|
|
326
|
-
const path = catalog === "base" ? basePoPath :
|
|
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
|
-
|
|
349
|
-
await
|
|
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
|
|
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(
|
|
370
|
-
await copyFile(
|
|
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
|
-
|
|
388
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
535
|
-
|
|
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
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
1062
|
+
msgstr: state.effectiveEntry.msgstr,
|
|
805
1063
|
references: entry.references,
|
|
806
1064
|
extractedComments: entry.extractedComments,
|
|
807
|
-
flags:
|
|
808
|
-
previous:
|
|
809
|
-
obsolete:
|
|
810
|
-
hasTranslation:
|
|
811
|
-
isFuzzy:
|
|
812
|
-
isCommittedToWorking:
|
|
813
|
-
matchesTargetTranslation:
|
|
814
|
-
translationOrigin:
|
|
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
|
|
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:
|
|
828
|
-
hasTranslation:
|
|
829
|
-
isFuzzy:
|
|
830
|
-
isCommittedToWorking:
|
|
831
|
-
matchesTargetTranslation:
|
|
832
|
-
translationOrigin:
|
|
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
|
-
|
|
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
|
|
869
|
-
|
|
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
|