diff --git a/docs/analytics.md b/docs/analytics.md index f792b5d..bc1bca9 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -110,8 +110,10 @@ whole browsing session and sends activity messages over it — JSON text frames matching the server's `Ping` msgspec struct with the fields `fr` (source path), `to` (navigation target), `read` (active seconds on `fr` since the last report), `lang` (the rendered language of the page the -activity happened on — its ``) and `hide`; falsy fields are -omitted. One channel +activity happened on — its ``, except the language-switch +navigation ping, which passes the picked tag explicitly because the view +transition applies the new `` only after the ping goes out) and +`hide`; falsy fields are omitted. One channel follows the session, so the activity of a visit stays tied together, and while the user is active the accumulated reading time is flushed every few seconds: the times are incremental, so a disconnection simply leaves the diff --git a/docs/localization.md b/docs/localization.md index 20c5abe..b3613e1 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -33,14 +33,9 @@ Deliberately simple — **q-values are ignored**: - Selection rule (`select_language` in `pagerite/i18n.py`): 1. If `?lang=` is present, use it (if a translation exists; otherwise fall through to header logic). - 2. If the article's original language appears anywhere in the header list, - use the **original**. Rationale: an AI translation is strictly worse - than the original for anyone who has that language configured at all - (e.g. `fi-FI, fi, en-US, en` gets English, not machine-translated - Finnish). - 3. Otherwise walk the header list in order and use the first language for - which a translation exists. - 4. Fall back to the original. + 2. Otherwise walk the header list in order and use the first language that + can be served — the original, or one with an available translation. + 3. Fall back to the original. Region tags normalize to their base subtag (`fi-FI` → `fi`). diff --git a/frontend/src/pagerite.js b/frontend/src/pagerite.js index d9179c5..e640ec2 100644 --- a/frontend/src/pagerite.js +++ b/frontend/src/pagerite.js @@ -628,7 +628,7 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect"; wsQueue.push(msg); } - function ping({ to, fr = currentPath, read = 0 } = {}) { + function ping({ to, fr = currentPath, read = 0, lang } = {}) { // Reading-time updates from the analytics page itself are not tracked // (/_a is admin machinery; the server would reject the path anyway). if (!to && currentPath === "/_a") return; @@ -637,7 +637,10 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect"; if (to) msg.to = to; const secs = Math.round(read / 1000); if (secs > 0) msg.read = secs; - const lang = document.documentElement.lang; + // The rendered language: normally the live , but a language + // switch passes it explicitly — the swap that updates runs inside + // the view-transition callback, after the switch ping goes out. + lang = lang || document.documentElement.lang; if (lang) msg.lang = lang; if (!msg.to && !msg.read) return; report(msg); @@ -818,9 +821,10 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect"; await load(currentPath, false); scrollTo(0, y); // Log the switch as a trail event in the new language (load() updated - // ): the ping matches the switch's GET server-side, so it is - // not misclassified as a crawler hit. - ping({ to: currentPath }); + // , but possibly inside a still-pending view transition, so + // pass the tag explicitly): the ping matches the switch's GET + // server-side, so it is not misclassified as a crawler hit. + ping({ to: currentPath, lang: tag }); }); // --- Fetch navigation ------------------------------------------------ diff --git a/pagerite/i18n.py b/pagerite/i18n.py index 4badf5e..da5163d 100644 --- a/pagerite/i18n.py +++ b/pagerite/i18n.py @@ -87,21 +87,16 @@ def select_language( 1. ``?lang=`` wins when a translation exists for it (otherwise falls through to the header logic). - 2. The original language anywhere in the header list wins — an AI - translation is strictly worse than the original for anyone who has - English configured at all. - 3. Otherwise the first header language with an available translation. - 4. Fall back to the original. + 2. Otherwise the first header language that can be served — the + original, or one with an available translation. + 3. Fall back to the original. """ if query_lang: tag = base_tag(query_lang) if tag == original or (tag and is_available(tag)): return tag - langs = parse_accept_language(accept_language or "") - if original in langs: - return original - for lang in langs: - if lang != original and is_available(lang): + for lang in parse_accept_language(accept_language or ""): + if lang == original or is_available(lang): return lang return original