PageSourceSearch

https://prospektor.ai/assets/js/i18n.0dedf27f2a.js

js prospektor.ai collected 2026-10-05 15:45:57 UTC 4,561 bytes, 93 lines download raw bytes

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.