Chunk keys as 9-byte bytes digests; trans nested by hash -> lang

This commit is contained in:
2026-09-02 02:09:27 +00:00
parent f63bb2c482
commit cc750236bc
6 changed files with 55 additions and 38 deletions
+18 -12
View File
@@ -36,13 +36,13 @@ class Node(msgspec.Struct, omit_defaults=True):
...
#: Replaces `content: str | None`. None = pure category label;
#: a list (possibly empty) = a page, as ordered chunk hashes.
chunks: list[str] | None = None
chunks: list[bytes] | None = None
#: Primary language of the article (BCP-47 base tag). "" = inherit
#: (nearest ancestor, front page last, site default "en" final).
language: str = ""
#: Chunk hashes the editor marked "do not translate" (always served
#: from the original). Presence-keys, value always True.
no_trans: dict[str, True] = {}
no_trans: dict[bytes, True] = {}
#: Languages this article is available in (besides its primary
#: language). Presence-keys, value always True — rendering, language
#: selection and hreflang alternates read this set instead of probing
@@ -52,12 +52,14 @@ class Node(msgspec.Struct, omit_defaults=True):
class Data(msgspec.Struct):
...
#: All original-language text, content-addressed: blake3(normalized,
#: digest 16) hex -> Markdown chunk. Shared by every article.
chunks: dict[str, str] = {}
#: Machine translations: f"{chunk_hash}:{lang}" -> translated Markdown.
#: All original-language text, content-addressed: blake3(normalized)
#: digest[:9] -> Markdown chunk. Shared by every article. Keys are
#: bytes; kanta/msgspec base64-encode them at the JSON level.
chunks: dict[bytes, str] = {}
#: Machine translations: chunk hash -> lang -> translated Markdown
#: (nested, not tuple keys: msgspec's JSON serializer rejects them).
#: Also used for node titles (hash of the title text).
trans: dict[str, str] = {}
trans: dict[bytes, dict[str, str]] = {}
#: User override patches per article and language:
#: f"{path}:{lang}" -> ordered patches (see localization.md).
patches: dict[str, list[Patch]] = {}
@@ -70,7 +72,7 @@ Notes:
building hrefs. `migrate_v3` audits existing stored paths (translation
keys, analytics references, any path-valued fields) and normalizes them.
- **Titles are chunks too**, by hash only: the nav renderer looks up
`trans.get(f"{hash(node.title)}:{lang}")`. No separate title storage;
`trans.get(hash(node.title), {}).get(lang)`. No separate title storage;
editing a title invalidates its translations automatically.
- **Per-hunk options** live in two places: *inherent* options are derived at
chunking time (code fences and HTML blocks are marked no-translate without
@@ -88,7 +90,7 @@ that article rendering, `select_language`'s availability check, and hreflang
alternate links never enumerate chunks. It is written by whoever writes
translation data, in the same transaction:
- **Translator job:** after writing `trans[f"{h}:{lang}"]` entries for an
- **Translator job:** after writing `trans[h][lang]` entries for an
article's chunks (or its title), set `node.langs[lang] = True`.
- **Translated-view save:** appending the first patch for `f"{path}:{lang}"`
sets `node.langs[lang] = True` (patches alone make the version exist).
@@ -101,7 +103,7 @@ translation data, in the same transaction:
- **Render:** `text = "\n\n".join(chunks[h] for h in node.chunks)` for the
original; for language `L` (only ever attempted when `L in node.langs`),
per chunk `trans[f"{h}:{L}"]` unless missing or `h in node.no_trans`,
per chunk `trans.get(h, {}).get(L)` unless missing or `h in node.no_trans`,
falling back to `chunks[h]`; then apply `patches.get(f"{path}:{L}", [])`
in order (per-hunk, best effort); then `markdown.render` as today. All of
this assembles the `Translation` the phase-1 plumbing already consumes.
@@ -135,10 +137,14 @@ Chunking must be deterministic and shared with render/save, so
## Implementation notes (deviations from the plan above)
- Chunking lives in `pagerite/chunks.py`; hashing uses the `blake3` package
(already a dependency) with a 16-byte digest (`hexdigest(16)`).
(already a dependency), truncated to a 9-byte `bytes` digest (kanta's
JSON persistence base64-encodes bytes keys to 12-char strings).
- `trans` is keyed `hash -> lang -> text` (nested dict), not by
`f"{hash}:{lang}"` tuples: msgspec's JSON serializer only supports
str-like/number-like dict keys, and kanta persists as JSON lines.
- `Translation.titles` stayed keyed by node path (phase-1 shape, views
untouched): `get_translation` builds it by walking the menu with the same
per-title `trans[f"{chunk_key(node.title)}:{lang}"]` lookups.
per-title `trans.get(chunk_key(node.title), {}).get(lang)` lookups.
- Insert hunks anchor on the whole preceding block (not just its tail) —
a stronger, simpler search context.
- `make_patch` diffs with `SequenceMatcher(autojunk=False)` so patches are