The model mangles Markdown it can see (Seed-X dropped the ** and the
glued-on colon in '**Pagerite**: ...'), so emphasis now crosses stripped,
inline in its sentence, and the syntax is weight-mapped back like links.
Also fix _place_marks swallowing trailing punctuation into the inner text
(end at the slice's last unit) — same-language round trips are now exact.
- Node.language sets an article's primary language, inherited down the
hierarchy, configured per row in the structure tab; English is now a
regular language in the lang tab, still the root fallback
- Flags map a base tag to the language's home country (en → GB,
pt → PT); explicit variant tags resolve their own region
- Lang tab flag grid: geographic group rows, shading-only selection
- LangSelect.vue: one language selector for the page and structure tabs
(small flag, clean dropdown), v-modeled on the shell-wide editorLang —
switching in either panel switches both AND the page preview. While the
panel is open the selection overrides the normal language preferences
(swapdoc.setLangOverride: loadPlain and pagerite.js fetches/prefetches
pin ?lang=, the primary language by its own code); closing restores.
- reconnect.js: every socket (page/banner editors, analytics view, the
activity channel) now connects through a staggered slot — simultaneous
attempts at page load (Vite's HMR socket plus ours, repeated on every
refresh) trip the browser's WebSocket throttling, leaving all sockets
to the host "pending" for minutes, which was the recurring empty
editor. A watchdog closes sockets stuck CONNECTING so they reschedule
through the policy (jittered exponential backoff, reset only by a
healthy connection) instead of hanging forever; a reconnected editor
re-opens its document when the previous open died with its socket.
- segments.py: a block of plain text with inline links now stays ONE wire
segment (Mark/Span) — the link label crosses in sentence context, so
translations come back grammatically coherent instead of incompatible
snippets. join() re-inserts the link markdown into the translated block
at weight-mapped positions (word units; CJK ideographs and kana runs
count one unit each), degrading to the source label on empty slices and
still rejecting markup injection. No sentinels on the wire: boundaries
are found by text processing alone.
- translate.py: the Dispatcher tracks Span (offsets + link marks) for the
in-flight job.
- 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.
- New editor-shell tab (lang): translatable languages as clickable flag
tiles (shared langs.js mapping of the 28 Seed-X languages to flags),
saved via the settings round-trip; translator service URL(s) shown.
- Bootstrap seeds translate_langs with Spanish and Chinese (the original
language is never a target).
- GET /_api/settings also returns primary_lang.
- PageEditor's flag/name helpers move to the shared langs.js.
- pagerite/masking.py: technical spans (code, URLs, {placeholders}, attrs,
footnote/link labels, container names, HTML tags) become numbered sentinels
for the LLM round trip; results are restored by number and rejected when a
sentinel is mangled (skipped for the rest of the run, stays pending).
Chunks with no prose left after masking are never dispatched.
- Data.translate_key -> translate_keys dict (key -> name); the first key is
generated at bootstrap, result transactions record the key as user=, and
startup logs the service URL(s) via translate.log_service_urls.
- Fix /_translate proxying through the Vite dev server (missing slash).
- Server replicates a ?lang= override onto the navigation links it
renders (nav, sidebar, cards, brand), so clicks and prefetches stay in
the chosen language even without JS; link_lang is part of the ETag and
body cache key (query and header renders of the same language differ
in their links).
- pagerite.js drops the Accept-Language header hack: the remembered
language rides internal fetches as ?lang= instead (added when a link
lacks one), the page cache keys on path+query, and history/address bar
keep the pretty query-less URL.
- Canonical names the actually served language (plain URL for the
original, ?lang= for translations); hreflang alternates are site-wide
from translate_langs, identical on every page: x-default (the plain
autodetecting URL) first, then every language explicitly, default
included, emitted right after canonical before the social tags.
- pagerite/i18n.py: parse_accept_language, select_language, Translation struct,
get_translation/available_languages stubs
- app.py: language threaded through show_page/_render_html/_cached_body,
ETag and Vary: accept-language
- views.py: <html lang>, translated nav titles with fallback, canonical
with ?lang=, hreflang alternates
- pagerite.js: strip ?lang= via replaceState, send it as Accept-Language
on fetch-navigation/preloads, lang-aware page cache
- docs/localization.md: negotiation + URL scheme + phase-2 fragment model
- docs/migrate.md: migrate_v3 content-addressed chunk storage plan