# 25 - English Leak Deep-Dive (Post-Lockdown "Abspielen" Persistence) > Investigation triggered after the 2026-05-08 multi-agent English-only > lockdown sweep landed (server-wide UICulture, per-user UICulture for 9/9, > DisplayPreferences CustomPrefs.language for 32 entries, web shim with > `navigator.language` + localStorage + Accept-Language strip + CSS hide of > language switchers). Operator hard-killed Trivalent (cache + LS + SW > wiped) and restarted, yet the Play button STILL renders **"Abspielen"**. > Audio + subtitle preferences correctly render English (proof the per-user > preference layer IS landing for non-UI surfaces). Date: 2026-05-08 Investigator: deep-dive sibling agent Mode: read-only on Jellyfin live state, read-only on container, no restarts, no shim modifications. Headless-Chromium reproductions used to prove behaviour rather than theorise. Prior reading (do not repeat findings from): `docs/15-force-english.md`, `docs/19-english-only-audit.md`, `docs/20-english-only-lockdown.md`, `docs/22-jellyfin-runtime-perf-audit.md`. --- ## 1. Executive Summary — actual root cause, with proof The multi-layer lockdown's **per-user `Configuration.UICulture` pin is inert with respect to the web SPA's UI-string locale**. The web SPA's `jellyfin-web` bundle does not read `Configuration.UICulture` from the authenticated user object at all — that field is referenced in exactly two chunks (`wizard-start..chunk.js` and `25583..chunk.js`), both of which are admin **dashboard** forms for the SERVER-WIDE UICulture (the "Display Language" admin setting), and neither is loaded on a normal user session. Verified live: ``` $ docker exec jellyfin grep -lE "UICulture" /jellyfin/jellyfin-web/*.js /jellyfin/jellyfin-web/index.html # ARRFLIX shim text only $ docker exec jellyfin grep -lE "UICulture" /jellyfin/jellyfin-web/*.chunk.js /jellyfin/jellyfin-web/25583.95a80bf8834e61a9a8e4.chunk.js /jellyfin/jellyfin-web/wizard-start.a4dfcf169516d40c4e52.chunk.js $ docker exec jellyfin grep -oE ".{40}UICulture.{60}" \ /jellyfin/jellyfin-web/wizard-start.a4dfcf169516d40c4e52.chunk.js up/Configuration")).then((function(n){n.UICulture=$("#selectLocalizationLanguage",t).val(), e.ajax({type:"POST... ``` Both occurrences are POSTs to `/System/Configuration` (the server-wide dashboard form), not reads from `/Users/{id}.Configuration`. **The SPA's actual locale resolver** (decompiled from `main.jellyfin.bundle.js`) is: ```js function g(){ return document.documentElement.getAttribute("data-culture") || (navigator.language ? navigator.language : navigator.userLanguage ? navigator.userLanguage : (navigator.languages?.length) ? navigator.languages[0] : "en-us"); } function w(){ var e; try { e = i.currentSettings.language() } // localStorage.getItem("language") catch(e){ } b(e = e || g()); l = S(e); // S = lowercase + replace _ with - document.documentElement.setAttribute("lang", l); ... } ``` `i.currentSettings.language()` reads `localStorage.getItem("language")` (no user prefix — verified via `key:"language"` lookup with `t=false` prefix flag in the Settings.get implementation). Per-user `Configuration.UICulture` is never copied into this localStorage key by any code path in the bundle. **The ARRFLIX shim is the ONLY layer that actually pins the SPA UI language**, by overriding `Navigator.prototype.language`, `Navigator.prototype.languages`, and pre-seeding `localStorage.language` to `en-US`. Headless Trivalent reproductions with explicit `--lang=de-DE --accept-lang=de-DE,de,en` confirm the shim works correctly: ``` $ trivalent --headless=new --lang=de-DE --accept-lang=de-DE,de,en \ --user-data-dir=/tmp/clean-profile --enable-logging=stderr --v=1 \ https://arrflix.s8n.ru/web/index.html $ grep -E "json.*chunk.js" /tmp/headless.log ...NotifyBeforeURLRequest: https://arrflix.s8n.ru/web/en-us-json.667484b4a441712c7e05.chunk.js $ grep " ``` So the shim produces the correct chunk request and the correct `` attribute when running against a freshly-isolated browser profile that never hit arrflix.s8n.ru before. The de-json chunk is **never** fetched in this scenario. **Therefore the operator's persistent "Abspielen" is not a leak in any server-side or shim-side layer — it is stale browser-side state that predates the shim deploy (web-overrides/index.html mtime 2026-05-08 17:22:00) and survived the operator's wipe.** The candidate stale-state vectors, in order of likelihood: 1. **Stale `index.html` in HTTP disk cache.** The server emits **no `Cache-Control` header** on `/web/index.html`; `last-modified` is `Fri, 08 May 2026 16:22:00 GMT`. Per RFC 7234 §4.2.2, Chromium heuristically caches at `0.1 * (now − Last-Modified)` ≈ 24 minutes when an asset has no Cache-Control. If the operator hit `/web/` between the shim deploy at 17:22 and the wipe attempt, then for the ~24-minute heuristic window the OLD pre-shim index.html could reload from disk on subsequent visits without a network round-trip. Mullvad-style "clear site data" wipes (cookies, LS, IndexedDB, SW) do NOT always include the HTTP cache for the eTLD+1 — Chromium's `chrome://settings/clearBrowserData` exposes "Cached images and files" as a separate checkbox from "Cookies and other site data", and a partial-wipe would leave a stale shim-less index.html in place. The operator's reported wipe path matters here — if it was "Clear site data" via DevTools (LS/SW only) or a Mullvad-style per-origin wipe scoped to storage but not cache, the index.html survives. 2. **A second browser profile / second browser the operator forgot.** The operator has multiple Chromium-family browsers installed (`~/.var/app/{com.google.Chrome, com.google.ChromeDev, org.chromium.Chromium, io.github.ungoogled_software.ungoogled_chromium, net.mullvad.MullvadBrowser}`) plus Trivalent at `~/.config/trivalent`. Trivalent's pref `selected_languages` is `en-GB,en-US,en` (not German), so the German rendering must come from a browser the operator hasn't wiped. A profile that hit arrflix.s8n.ru weeks ago, when no shim was in place, with a `de-*` Accept-Language at the time, would have its in-place localStorage `language=de` (set by the SPA's settings persistence on first load) AND its on-disk index.html cache predating the shim. 3. **The operator's screenshot was captured BEFORE the wipe.** "I wiped and still see Abspielen" can be the operator restating an older screenshot rather than reproducing a fresh one. Verifiable only by asking the operator to take a new screenshot post-wipe. **Severity: LOW.** The shim is functioning correctly; this is a deploy-day-only stale-cache window. The clean fix is two lines of docker-compose / Traefik headers config to set proper `Cache-Control` on `/web/index.html` so future shim deploys propagate without manual operator wiping. --- ## 2. Per-Hypothesis Verdicts | # | Hypothesis | Verdict | Probe + evidence | |---|---|---|---| | 1 | `` for the de chunk fires before the inline shim runs | **Ruled out** | `grep -ciE 'rel="?(prefetch\|preload\|modulepreload)' index_de.html` → `0`. The bundle uses `