Catch-up programmes and file chapters both reach the seek bar through timelineSegments, so the fix sits on that shared path: - aria-valuetext reads the translated "<title> · <time>" whenever the scrub or playback value (the keyboard target included) lies in a titled segment, else the plain time. - ControlsTimelineHover becomes ControlsTimelineLabel: besides a hovering pointer, keyboard focus (:focus-visible or a seek key) and a drag preview, which is how touch sees it, anchor the label at the slider value. - The label is two parts: the title ellipsizes at min(320px, 60cqw), the time never truncates. Once laid out it is clamped by its measured width to stay 8px inside the player (clampTimelineLabelLeft). - Boundaries hold 3:1 over any frame: an opaque --pc-timeline-track and white --pc-timeline-separator separators filling the (now 2px) gaps. - LIVE is the translated LIVE_BADGE; the badge and the --:-- placeholder are role="img" named LIVE_STREAM / DURATION_UNKNOWN, in the shared and the native-view Embedded MPV dock. New keys go to all 18 locales; the identical-English baseline gains the punctuation-only template and "LIVE" for de and nl. The epglong Xtream mock scenario adds a 120-character programme title for the new Electron E2E, which tabs to the bar in both themes at 1280 and 800px and in de/ru. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
i18n fill-missing tool
One-shot helper for bringing every locale file in apps/web/src/assets/i18n/
to full coverage of en.json.
How it works
en.json is the source of truth. For every other locale, the script:
- Walks
en.jsonin order (top-level + nested). - For each leaf key:
- Keeps the locale's existing translation if present.
- Otherwise pulls the translation from
tools/i18n/patches/<lang>.json(a flat object keyed by dotted path). - If neither exists, leaves the key absent and exits non-zero.
- Writes the merged tree back to the locale file with 4-space indent and a trailing newline.
Existing translations are never overwritten.
Workflow
# 1. Generate one missing-strings dump per locale (flat dotted-path → EN string)
node tools/i18n/fill-missing.mjs --emit-missing
# → tools/i18n/missing/<lang>.json
# 2. Translate each missing/<lang>.json into the equivalent patch file
# in tools/i18n/patches/<lang>.json (same shape: { dotted.path: translated }).
# 3. Merge patches into the locale files
node tools/i18n/fill-missing.mjs
# 4. Verify
node -e "
const fs=require('fs');
const en=JSON.parse(fs.readFileSync('apps/web/src/assets/i18n/en.json','utf8'));
const collect=(d,p='')=>Object.entries(d).flatMap(([k,v])=>v&&typeof v==='object'?collect(v,p?p+'.'+k:k):[p?p+'.'+k:k]);
const enKeys=new Set(collect(en));
for(const f of fs.readdirSync('apps/web/src/assets/i18n').filter(x=>x.endsWith('.json')&&x!=='en.json')){
const k=new Set(collect(JSON.parse(fs.readFileSync('apps/web/src/assets/i18n/'+f,'utf8'))));
console.log(f,'missing:',[...enKeys].filter(x=>!k.has(x)).length,'extra:',[...k].filter(x=>!enKeys.has(x)).length);
}"
For a non-mutating CI/agent check, run:
pnpm run i18n:check
The check fails on missing or extra keys against en.json, and on values that
are identical to English unless identical-en-baseline.json lists that exact
English text for the locale and key. Do not copy English into a locale as a
placeholder: translate it. The contract, including when an entry stops
covering a key, is in the
validation map.
Baseline entries that are no longer identical are reported, not fatal. After translating baselined values, or for a value that is legitimately the same in a language (a brand, a technical name, a loanword), rewrite the baseline and review every added entry in the diff:
pnpm run i18n:baseline:update
The checker's own tests run with pnpm run i18n:test; CI runs both through
pnpm run i18n:validate. For a full audit that ignores the baseline, run:
node tools/i18n/check-drift.mjs --fail-on-identical
Translation rules
- Preserve
{{interpolation}}placeholders verbatim. - Preserve any inline HTML tags.
- Match the locale's existing punctuation, casing, and terminology.
- Keep brand names (IPTVnator, Xtream, Stalker, M3U, EPG, MPV, VLC) untranslated.
- Short UI strings stay short — no length blow-up.