1// ââ THE PAGE'S LANGUAGE, IN THE BROWSER (#114) ââ 2// Two jobs, both small. First: `t()`, the one function every other script on 3// the site says its sentences through. The English sentence is the key 4// (the studio's convention, #113); the page ships the translations it needs 5// in `<script type="application/json" id="i18n">`, written by the build for 6// exactly the sentences the scripts ask for, and only on a page that exists 7// in more than one language. No block, or a sentence the block lacks, means 8// English â the same fallback the build applies. 9// 10// Second: the nudge (#114 â #544). A visitor on an ENGLISH page whose 11// browser ranks first a language the page is built in is offered the twin, 12// in that language â one line that is itself the link, one way to close it, 13// nothing to explain. Offered, never redirected: the spec is one line 14// (`HANDOVER-website-funnel.md`, 23 Aug) â "`Accept-Language` may SUGGEST a 15// language; it must never silently redirect somebody who typed the English 16// URL" â and the operator's ask for #544 keeps it: *"do the nudge for 17// German/foreign-language browsers - one time only"*. Only the browser's 18// first-ranked language counts, for #113's reason: reading further down the 19// list would offer Spanish to a machine whose owner listed it third; the IP 20// and a geo lookup never count at all â a German VPN is not a German reader. 21// One time only means the flag is written the moment the line is drawn, so a 22// visitor who neither takes it nor closes it is not asked again on the next 23// page; taking it overwrites the flag with the language they went on in. 24// 25// Deferred and first in <head>, so `t` exists before any deferred script 26// after it runs; it looks nothing up in the DOM until the document is parsed. 27(() => { 28 let data = null; 29 try { 30 const el = document.getElementById('i18n'); 31 data = el ? JSON.parse(el.textContent) : null; 32 } catch (e) { data = null; } 33 const strings = (data && data.strings) || {}; 34 const fill = (s, vars) => vars 35 ? String(s).replace(/\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m)) 36 : String(s); 37 window.t = (key, vars) => 38 fill(Object.prototype.hasOwnProperty.call(strings, key) ? strings[key] : key, vars); 39 40 if (!data) return; 41 42 // ââ The nudge ââ 43 // English pages only: a translated page was chosen â by a click, a link 44 // or a typed URL â and the build sends it nothing to nudge with. 45 const here = data.lang; 46 if (here !== 'en' || !data.suggest) return; 47 const KEY = 'prospektor.lang'; 48 let remembered = ''; 49 try { remembered = localStorage.getItem(KEY) || ''; } catch (e) { /* private mode: once per page */ } 50 if (remembered) return; 51 52 const prefs = (navigator.languages && navigator.languages.length) ? navigator.languages : [navigator.language]; 53 const want = String(prefs[0] || '').toLowerCase().split(/[-_]/)[0]; 54 if (!want || want === here) return; 55 56 // The twins this page has, from its own hreflang links â derived from the 57 // build, never a list kept here. 58 const twins = {}; 59 document.querySelectorAll('link[rel="alternate"][hreflang]').forEach(l => { 60 const code = l.getAttribute('hreflang'); 61 if (!code || code === 'x-default') return; 62 // As a path on THIS origin: the hreflang href is absolute (that is what 63 // a crawler needs), and the offer must stay on whichever host served it â 64 // a preview, a local drive, production alike. 65 try { 66 const u = new URL(l.getAttribute('href'), location.href); 67 twins[code] = u.pathname + u.search + u.hash; 68 } catch (e) { /* an unreadable href is no twin */ } 69 }); 70 const say = data.suggest[want]; 71 if (!twins[want] || !say) return; 72 73 const remember = code => { try { localStorage.setItem(KEY, code); } catch (e) { /* nothing to do */ } }; 74 75 // One line, which is the link; one Ã, named for a screen reader; nothing else. 76 const bar = document.createElement('div'); 77 bar.className = 'lang-suggest'; 78 bar.setAttribute('lang', want); 79 bar.id = 'langSuggest'; 80 const go = document.createElement('a'); 81 go.href = twins[want]; 82 go.hreflang = want; 83 go.textContent = say.line; 84 go.addEventListener('click', () => remember(want)); 85 const close = document.createElement('button'); 86 close.type = 'button'; 87 close.setAttribute('aria-label', say.stay); 88 close.textContent = 'Ã'; 89 close.addEventListener('click', () => bar.remove()); 90 bar.append(go, close); 91 document.body.prepend(bar); 92 remember(here); // drawn once on this browser â taken, closed or ignored alike 93})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.