- 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