Fix language-switch tracking ping; serve first matching Accept-Language
The language-switch trail ping read <html lang> before the view transition applied the swap, so it reported the previous language (and read pings never update trail-item languages server-side): multi- language visits recorded as single-language trails and the analytics flag combining had nothing to show. The switch ping now passes the picked tag explicitly. Language negotiation no longer prefers the article's original language anywhere in the Accept-Language list — nearly every browser lists English as a fallback, so translations were almost never served. The first servable header language now wins, the original counting only in its natural position.
This commit is contained in:
+4
-2
@@ -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`
|
frames matching the server's `Ping` msgspec struct with the fields `fr`
|
||||||
(source path), `to` (navigation target), `read` (active seconds on `fr`
|
(source path), `to` (navigation target), `read` (active seconds on `fr`
|
||||||
since the last report), `lang` (the rendered language of the page the
|
since the last report), `lang` (the rendered language of the page the
|
||||||
activity happened on — its `<html lang>`) and `hide`; falsy fields are
|
activity happened on — its `<html lang>`, except the language-switch
|
||||||
omitted. One channel
|
navigation ping, which passes the picked tag explicitly because the view
|
||||||
|
transition applies the new `<html lang>` 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
|
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
|
while the user is active the accumulated reading time is flushed every few
|
||||||
seconds: the times are incremental, so a disconnection simply leaves the
|
seconds: the times are incremental, so a disconnection simply leaves the
|
||||||
|
|||||||
@@ -33,14 +33,9 @@ Deliberately simple — **q-values are ignored**:
|
|||||||
- Selection rule (`select_language` in `pagerite/i18n.py`):
|
- Selection rule (`select_language` in `pagerite/i18n.py`):
|
||||||
1. If `?lang=<tag>` is present, use it (if a translation exists; otherwise
|
1. If `?lang=<tag>` is present, use it (if a translation exists; otherwise
|
||||||
fall through to header logic).
|
fall through to header logic).
|
||||||
2. If the article's original language appears anywhere in the header list,
|
2. Otherwise walk the header list in order and use the first language that
|
||||||
use the **original**. Rationale: an AI translation is strictly worse
|
can be served — the original, or one with an available translation.
|
||||||
than the original for anyone who has that language configured at all
|
3. Fall back to the original.
|
||||||
(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.
|
|
||||||
|
|
||||||
Region tags normalize to their base subtag (`fi-FI` → `fi`).
|
Region tags normalize to their base subtag (`fi-FI` → `fi`).
|
||||||
|
|
||||||
|
|||||||
@@ -628,7 +628,7 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect";
|
|||||||
wsQueue.push(msg);
|
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
|
// Reading-time updates from the analytics page itself are not tracked
|
||||||
// (/_a is admin machinery; the server would reject the path anyway).
|
// (/_a is admin machinery; the server would reject the path anyway).
|
||||||
if (!to && currentPath === "/_a") return;
|
if (!to && currentPath === "/_a") return;
|
||||||
@@ -637,7 +637,10 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect";
|
|||||||
if (to) msg.to = to;
|
if (to) msg.to = to;
|
||||||
const secs = Math.round(read / 1000);
|
const secs = Math.round(read / 1000);
|
||||||
if (secs > 0) msg.read = secs;
|
if (secs > 0) msg.read = secs;
|
||||||
const lang = document.documentElement.lang;
|
// The rendered language: normally the live <html lang>, but a language
|
||||||
|
// switch passes it explicitly — the swap that updates <html> runs inside
|
||||||
|
// the view-transition callback, after the switch ping goes out.
|
||||||
|
lang = lang || document.documentElement.lang;
|
||||||
if (lang) msg.lang = lang;
|
if (lang) msg.lang = lang;
|
||||||
if (!msg.to && !msg.read) return;
|
if (!msg.to && !msg.read) return;
|
||||||
report(msg);
|
report(msg);
|
||||||
@@ -818,9 +821,10 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect";
|
|||||||
await load(currentPath, false);
|
await load(currentPath, false);
|
||||||
scrollTo(0, y);
|
scrollTo(0, y);
|
||||||
// Log the switch as a trail event in the new language (load() updated
|
// Log the switch as a trail event in the new language (load() updated
|
||||||
// <html lang>): the ping matches the switch's GET server-side, so it is
|
// <html lang>, but possibly inside a still-pending view transition, so
|
||||||
// not misclassified as a crawler hit.
|
// pass the tag explicitly): the ping matches the switch's GET
|
||||||
ping({ to: currentPath });
|
// server-side, so it is not misclassified as a crawler hit.
|
||||||
|
ping({ to: currentPath, lang: tag });
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Fetch navigation ------------------------------------------------
|
// --- Fetch navigation ------------------------------------------------
|
||||||
|
|||||||
+5
-10
@@ -87,21 +87,16 @@ def select_language(
|
|||||||
|
|
||||||
1. ``?lang=`` wins when a translation exists for it (otherwise falls
|
1. ``?lang=`` wins when a translation exists for it (otherwise falls
|
||||||
through to the header logic).
|
through to the header logic).
|
||||||
2. The original language anywhere in the header list wins — an AI
|
2. Otherwise the first header language that can be served — the
|
||||||
translation is strictly worse than the original for anyone who has
|
original, or one with an available translation.
|
||||||
English configured at all.
|
3. Fall back to the original.
|
||||||
3. Otherwise the first header language with an available translation.
|
|
||||||
4. Fall back to the original.
|
|
||||||
"""
|
"""
|
||||||
if query_lang:
|
if query_lang:
|
||||||
tag = base_tag(query_lang)
|
tag = base_tag(query_lang)
|
||||||
if tag == original or (tag and is_available(tag)):
|
if tag == original or (tag and is_available(tag)):
|
||||||
return tag
|
return tag
|
||||||
langs = parse_accept_language(accept_language or "")
|
for lang in parse_accept_language(accept_language or ""):
|
||||||
if original in langs:
|
if lang == original or is_available(lang):
|
||||||
return original
|
|
||||||
for lang in langs:
|
|
||||||
if lang != original and is_available(lang):
|
|
||||||
return lang
|
return lang
|
||||||
return original
|
return original
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user