localise chrome, smooth prose, in English and Bengali

The engine now owns the words it puts on a page that the author did not write
(ADR-0034). `internal/render/chrome.go` holds the phrase table, Gregorian month
names and decimal digits per language, keyed phrase-then-language so both forms
sit side by side and a half-translated row is visible while reading. Templates
reach it through `t`, `num` and `day`, registered before parsing so a site
override's blocks may call them too.

The reference theme stops hardcoding English: "Newer", "Page 2 of 2" and every
date now come from the table, while `datetime` attributes stay ASCII because a
parser reads them.

Authored text gets goldmark's typographer and nothing else — quotes, dashes and
ellipses smoothed, code spans untouched because it works on the parsed tree. It
is a parser option rather than a function over a page, so the transforms counter
does not move; state.md now says why, so the next reader does not miscount.

Deliberately absent: relative dates, which need a validity window that only the
cache entry will have, and body widow prevention, which cannot be done safely by
a pass over rendered HTML.
This commit is contained in:
2026-07-30 03:24:29 +06:00
parent 7c903a76a3
commit 430a5ae43b
8 changed files with 282 additions and 16 deletions
+23 -1
View File
@@ -50,6 +50,28 @@ A listing page receives `.Title`, `.Lang`, `.Canonical`, `.Style` as above, plus
| `.PrevURL`, `.NextURL` | empty at the ends; *newer* is `prev`, because the order is newest first |
| `.Groups` | set instead of `.Items` when entries are grouped — a tag listing groups by section, each `.Name` and `.Items` |
## Chrome text and formatting
Three functions, available in every template including a site root's own blocks. They exist so no template
hardcodes English: the words the engine supplies are the engine's to localise (ADR-0034).
| Call | Gives |
|---|---|
| `{{t .Lang "newer"}}` | one phrase in that language. Extra arguments fill `%s` placeholders in order |
| `{{num .Lang .Page}}` | an integer in that language's digits — `12`, `১২` |
| `{{day .Lang .Date}}` | a date as that language reads it — `8 March 2026`, `৮ মার্চ ২০২৬`; empty for a zero date |
Phrase keys today: `newer`, `older`, `empty`, `page-of` (two arguments), `position` (two arguments). An
unknown language falls back to the default locale and an unknown key returns itself, so a missing
translation can never blank a page or fail a render.
Two rules a theme must keep: put the machine-readable form in the attribute and the localised form in the
text — `<time datetime="{{.Date.Format "2006-01-02"}}">{{day .Lang .Date}}</time>` — and never localise
`.Title`, `.HTML` or anything else the author wrote.
A site root cannot add or override a phrase yet. A theme needing its own words writes them in its own
block; site-supplied strings wait for the settings cascade (`ideas/deferred-decisions.md`).
## The stability rule
Fields and names are **added, never renamed or removed**. Absence is always legal: a template reading a
@@ -69,7 +91,7 @@ Breaking the contract is not a feature — it is a new contract version, and it
| named template lookup | per-type sets; a theme redefines a named block and inherits the rest (ADR-0019) |
| URLs | every path the engine emits, so a theme never constructs one by hand |
| per-page assets | the `styles` / `scripts` frontmatter lists, resolved relative to the bundle |
| chrome strings | looked up by key and language, never hardcoded English in a template |
| chrome strings | looked up by key and language, never hardcoded English in a template — live, see above |
| validity windows | a template that renders time-dependent output declares one |
## Extras view