Redesign translation overrides: keyed, structural, hash-anchored

Replace the old "patches" format (path:lang composite keys, ordered
hunk lists, text-anchored matching) with Data.overrides:
path -> lang -> LangEdits, keyed throughout so a save's database diff
touches only the edited chunks. The old "patches" key is ignored on
decode, discarding legacy data without a migration.

- Whole-paragraph additions/deletions are structural: a drop flag on
  the original chunk hash, and additions in their own dict anchored
  from the neighboring chunks' before/after (first live referrer wins),
  so they stay in place across retranslation and one-sided original
  edits.
- Within-paragraph edits (up to a full paragraph rewrite or split) are
  full-chunk replace patches applied by chunk hash alone: a
  retranslation is overridden wholesale, so user edits survive AI
  re-runs; editing the original changes the hash and orphans the patch.
  The old search-matching staleness gate is gone.
- Saving a translation on a page without original chunks is rejected
  (REST 400 / WS error); emptying the original afterwards renders the
  translation empty, with the orphaned overrides inert.
This commit is contained in:
2026-09-21 18:14:48 +00:00
parent cc2cc23b3b
commit 91a16f56c5
11 changed files with 500 additions and 236 deletions
+2 -2
View File
@@ -7,7 +7,7 @@
// Flag clicks toggle and save immediately; the settings round-trip
// re-reads the payload, so this tab only ever changes translate_langs. The
// settings write's invalidation hook kicks the translation dispatcher. The
// refresh button drops all machine translations (user patches are kept),
// refresh button drops all machine translations (user overrides are kept),
// making the dispatcher re-translate everything. Translator keys are
// managed inline ( add, name edit, ✕ delete); new keys are generated
// here in the server's format and everything rides the settings
@@ -100,7 +100,7 @@ async function toggle(code) {
// Delete all machine translations server-side; the dispatcher re-fills
// them (a connected translator starts getting jobs right away). User
// patches survive — they are edits, not machine output.
// overrides survive — they are edits, not machine output.
const refreshing = ref(false)
async function refresh() {
if (refreshing.value) return
+2 -2
View File
@@ -20,7 +20,7 @@
// edited as its effective (hybrid) Markdown; the hybrid the session
// started from is kept as a shadow copy (shadowBase) and sent along at
// save time, so the server diffs the user's changes only and stores them
// as a patch — edits to a translation never touch the original, while
// as per-chunk overrides — edits to a translation never touch the original, while
// edits to the primary language re-chunk the original (and thereby
// invalidate the affected translation fragments). The live preview always
// renders the version being edited, whichever language the page itself
@@ -208,7 +208,7 @@ function save() {
if (lang.value) {
msg.lang = lang.value
// The shadow copy this session started from: the server diffs base →
// markdown and stores only the user's changes as a patch.
// markdown and stores only the user's changes as overrides.
msg.base = shadowBase
// An untouched title field is not sent: it holds the served
// translation, which a save must not freeze into an override fragment.