Segments, not sentinels: prose-only translation wire protocol

- pagerite/segments.py replaces masking.py: a fragment is parsed with the
  project's own markdown-it (markdown.make_md(verbatim=True), byte-identical
  tokens) and split into pure-prose segments with source spans; only the
  segments plus per-segment context surrounds cross the wire (Job.texts /
  contexts / Result.texts) and translations splice back by offset — markup
  can no longer break, it never leaves the server. Count/empty/non-prose
  results are rejected and skipped for the run.
- Localization machinery out of app.py: the translator dispatcher (clients,
  job pipeline, validation skip-list) moves into translate.Dispatcher;
  translated-edit recording moves into i18n (add_patch, set_title_translation,
  clear_translations). app.py keeps only the routes.
- Structure editor localized: flag strip switches the language titles are
  shown/edited in (GET /_api/pages?lang= flags translated rows, originals
  dimmed); retitling in a translation writes a per-language title fragment
  via StructureOp.lang — slugs, order and hierarchy stay language-independent.
- Localization tab: refresh-all button (DELETE /_api/translations) drops
  machine translations, keeps user patches and clears the skip-list so the
  dispatcher re-translates everything.
- PageEditor always opens in the primary language; editor socket gets
  reconnect/doc-mismatch logging. Reference translator: per-segment calls
  with context prompts, deterministic punctuation matching and the "<"
  markup-bleed cut.
This commit is contained in:
2026-09-03 00:24:07 +00:00
parent b1e8f0b454
commit b0866fc4f7
16 changed files with 1136 additions and 494 deletions
+20 -2
View File
@@ -1,7 +1,10 @@
<script setup>
// Recursive site-structure tree with drag-and-drop ordering (vue-draggable).
// Nodes come from the server (GET /_api/pages via StructureEditor.vue) as
// {slug, path, title, order, published, has_content, children}.
// {slug, path, title, translated, order, published, has_content, children}.
// With a `lang` prop (StructureEditor's language strip) the titles shown
// are that language's; `translated` marks rows with an actual translation
// (untranslated rows show the original title, dimmed).
// Every node is real: a label whose title and slug are always editable
// inline — the title saves while typing (and focusing it opens the page),
// the slug commits on blur/Enter since it renames the path, moving the
@@ -30,6 +33,10 @@ const props = defineProps({
nodes: { type: Array, required: true },
parentPath: { type: String, default: '' },
depth: { type: Number, default: 0 },
// StructureEditor's selected language ('' = original). Only used for the
// untranslated-title styling here; the fetch and title edits live in the
// parent (handlers.titleInput posts the lang with the op).
lang: { type: String, default: '' },
})
const handlers = inject('structureHandlers')
@@ -126,8 +133,11 @@ function onEnd() {
<template v-else>
<input
class="edit title-edit"
:class="{ untranslated: lang && !element.translated }"
:value="element.title"
title="Label in the navigation — saves while typing; click opens the page"
:title="lang && !element.translated
? 'No translation yet showing the original; typing creates the translated title'
: 'Label in the navigation saves while typing; click opens the page'"
@input="handlers.titleInput(element, $event)"
@focus="handlers.open(element.path)"
/>
@@ -158,6 +168,7 @@ function onEnd() {
:nodes="element.children"
:parent-path="element.path"
:depth="depth + 1"
:lang="lang"
/>
</div>
</template>
@@ -274,6 +285,13 @@ body.tree-dragging .treelist {
cursor: text;
}
/* With a language selected (StructureEditor's strip), rows without an
actual translation show the original title dimmed and italic. */
.title-edit.untranslated {
color: var(--muted);
font-style: italic;
}
.slug-edit {
font-family: var(--font-code);
}