PageSourceSearch

https://homezero.ai/js/hz-i18n.js?v=freeze-fix-20260602

js homezero.ai collected 2026-10-05 16:10:13 UTC 12,156 bytes, 292 lines download raw bytes

1/* HomeZero native i18n runtime.
2 * Walks the DOM, replaces text nodes + a few attributes using a NL->target dictionary.
3 * Re-runs on DOM mutations so dynamically-injected CMS content is also translated.
4 *
5 * Dictionaries are loaded on demand from /i18n/{lang}.json.
6 * Original Dutch text is captured per node into a private map so switching back to NL works.
7 */
8(function () {
9  if (window.__hzI18n) return;
10
11  var STORAGE = "hz_lang";
12  var SUPPORTED = ["nl", "en", "de", "fr"];
13  var ATTR_KEYS = ["alt", "title", "placeholder", "aria-label"];
14  var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, NOSCRIPT: 1, CODE: 1, PRE: 1, SVG: 1, TEMPLATE: 1 };
15
16  var dict = {}; // { "nl text": "translated" }
17  var origText = new WeakMap(); // textNode -> nl
18  var origAttr = new WeakMap(); // element -> { attr: nl }
19  var selfAttr = new WeakMap(); // element -> { attr: true } for writes caused by this runtime
20  var lang = readLang();
21  var ready = false;
22  var observer = null;
23  var subs = [];
24
25  function readLang() {
26    try {
27      var l = localStorage.getItem(STORAGE);
28      if (l && SUPPORTED.indexOf(l) !== -1) return l;
29    } catch (_) {}
30    var nav = (navigator.language || "nl").slice(0, 2).toLowerCase();
31    return SUPPORTED.indexOf(nav) !== -1 ? nav : "nl";
32  }
33
34  function persistLang(l) {
35    try { localStorage.setItem(STORAGE, l); } catch (_) {}
36  }
37
38  function fetchDict(l) {
39    if (l === "nl") return Promise.resolve({});
40    return fetch("/i18n/" + l + ".json", { cache: "force-cache" })
41      .then(function (r) { return r.ok ? r.json() : {}; })
42      .catch(function () { return {}; });
43  }
44
45  function normalize(s) {
46    return s == null ? "" : String(s).replace(/\s+/g, " ").trim();
47  }
48
49  function isNoI18n(el) {
50    return !!(el && el.nodeType === 1 && el.closest && el.closest("[data-no-i18n]"));
51  }
52
53  function setTranslatedAttr(el, attr, value) {
54    var flags = selfAttr.get(el);
55    if (!flags) {
56      flags = {};
57      selfAttr.set(el, flags);
58    }
59    flags[attr] = true;
60    el.setAttribute(attr, value);
61  }
62
63  function consumeSelfAttr(el, attr) {
64    var flags = selfAttr.get(el);
65    if (!flags || !flags[attr]) return false;
66    delete flags[attr];
67    return true;
68  }
69
70  function translateText(node) {
71    var nl = origText.get(node);
72    if (nl == null) {
73      var raw = node.nodeValue;
74      if (!raw) return;
75      var n = normalize(raw);
76      if (n.length < 2 || !/[a-zA-ZÀ-ÿ]/.test(n)) return;
77      origText.set(node, n);
78      nl = n;
79    }
80    if (lang === "nl") {
81      // restore: only set if differs to avoid layout thrash
82      if (normalize(node.nodeValue) !== nl) node.nodeValue = nl;
83      return;
84    }
85    var t = dict[nl];
86    if (t && normalize(node.nodeValue) !== t) {
87      node.nodeValue = t;
88    }
89  }
90
91  function translateAttr(el) {
92    if (isNoI18n(el)) return;
93    var stash = origAttr.get(el);
94    if (!stash) {
95      stash = {};
96      for (var i = 0; i < ATTR_KEYS.length; i++) {
97        var k = ATTR_KEYS[i];
98        var v = el.getAttribute(k);
99        if (v) stash[k] = normalize(v);
100      }
101      origAttr.set(el, stash);
102    }
103    for (var k2 in stash) {
104      var nl2 = stash[k2];
105      if (!nl2) continue;
106      if (lang === "nl") {
107        if (normalize(el.getAttribute(k2)) !== nl2) setTranslatedAttr(el, k2, nl2);
108        continue;
109      }
110      var t = dict[nl2];
111      if (t && normalize(el.getAttribute(k2)) !== t) setTranslatedAttr(el, k2, t);
112    }
113  }
114
115  function walk(root) {
116    if (!root) return;
117    if (root.nodeType === 3) { translateText(root); return; }
118    if (root.nodeType !== 1) return;
119    if (SKIP_TAGS[root.tagName]) return;
120    if (root.getAttribute && (root.getAttribute("data-no-i18n") != null)) return;
121    if (root.tagName) translateAttr(root);
122    var c = root.firstChild;
123    while (c) {
124      var next = c.nextSibling;
125      if (c.nodeType === 3) translateText(c);
126      else walk(c);
127      c = next;
128    }
129  }
130
131  function reapply() {
132    if (!document.body) return;
133    walk(document.body);
134    document.documentElement.setAttribute("lang", lang);
135  }
136
137  function startObserver() {
138    if (observer || !document.body) return;
139    var queue = [];
140    var scheduled = false;
141    function processMut(m) {
142      if (m.type === "characterData") {
143        translateText(m.target);
144      } else if (m.type === "childList") {
145        m.addedNodes && m.addedNodes.forEach(function (n) { walk(n); });
146      } else if (m.type === "attributes") {
147        var el = m.target;
148        if (el && el.nodeType === 1) {
149          if (isNoI18n(el)) return;
150          if (consumeSelfAttr(el, m.attributeName)) return;
151          origAttr.delete(el);
152          translateAttr(el);
153        }
154      }
155    }
156    observer = new MutationObserver(function (muts) {
157      for (var i = 0; i < muts.length; i++) queue.push(muts[i]);
158      if (scheduled) return;
159      scheduled = true;
160      requestAnimationFrame(function () {
161        scheduled = false;
162        // Drain any mutations that arrived between callback and rAF.
163        var extra = observer.takeRecords();
164        for (var j = 0; j < extra.length; j++) queue.push(extra[j]);
165        var batch = queue;
166        queue = [];
167        for (var k = 0; k < batch.length; k++) processMut(batch[k]);
168      });
169    });
170    observer.observe(document.body, {
171      childList: true, subtree: true, characterData: true,
172      attributes: true, attributeFilter: ATTR_KEYS,
173    });
174  }
175
176
177  function setLang(next) {
178    if (SUPPORTED.indexOf(next) === -1) return Promise.resolve(false);
179    persistLang(next);
180    return fetchDict(next).then(function (d) {
181      dict = d || {};
182      lang = next;
183      ready = true;
184      reapply();
185      subs.forEach(function (fn) { try { fn(lang); } catch (_) {} });
186      return true;
187    });
188  }
189
190  function init() {
191    if (ready) return;
192    if (!document.body) {
193      document.addEventListener("DOMContentLoaded", init, { once: true });
194      return;
195    }
196    startObserver();
197    setLang(lang);
198  }
199
200  window.__hzI18n = {
201    SUPPORTED: SUPPORTED,
202    get: function () { return lang; },
203    set: setLang,
204    onChange: function (fn) { subs.push(fn); return function () { subs = subs.filter(function (s) { return s !== fn; }); }; },
205    reapply: reapply,
206  };
207
208  // Boot
209  init();
210
211  function flagSvg(code) {
212    var attrs = 'width="24" height="17" viewBox="0 0 30 21" aria-hidden="true" focusable="false" style="display:block;border-radius:3px"';
213    if (code === "nl") return '<svg ' + attrs + ' xmlns="http://www.w3.org/2000/svg"><rect width="30" height="7" fill="#AE1C28"/><rect y="7" width="30" height="7" fill="#fff"/><rect y="14" width="30" height="7" fill="#21468B"/></svg>';
214    if (code === "fr") return '<svg ' + attrs + ' xmlns="http://www.w3.org/2000/svg"><rect width="10" height="21" fill="#0055A4"/><rect x="10" width="10" height="21" fill="#fff"/><rect x="20" width="10" height="21" fill="#EF4135"/></svg>';
215    if (code === "de") return '<svg ' + attrs + ' xmlns="http://www.w3.org/2000/svg"><rect width="30" height="7" fill="#000"/><rect y="7" width="30" height="7" fill="#DD0000"/><rect y="14" width="30" height="7" fill="#FFCE00"/></svg>';
216    return '<svg ' + attrs + ' xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none"><path d="M0,0 v21 h30 v-21 z" fill="#012169"/><path d="M0,0 L30,21 M30,0 L0,21" stroke="#fff" stroke-width="3"/><path d="M0,0 L30,21 M30,0 L0,21" stroke="#C8102E" stroke-width="2"/><path d="M15,0 v21 M0,10.5 h30" stroke="#fff" stroke-width="5"/><path d="M15,0 v21 M0,10.5 h30" stroke="#C8102E" stroke-width="3"/></svg>';
217  }
218
219  function renderFallbackSwitcher() {
220    var hosts = document.querySelectorAll(".lng-cont");
221    if (!hosts.length) return;
222    var labels = { nl: "Nederlands", en: "English", fr: "Français", de: "Deutsch" };
223    hosts.forEach(function (host) {
224      if (host.getAttribute("data-hz-switcher") === "react") return;
225      // Idempotent: skip if already rendered for current language.
226      if (host.getAttribute("data-hz-switcher") === "native" && host.getAttribute("data-hz-lang") === lang) return;
227      host.setAttribute("data-no-i18n", "");
228      host.setAttribute("data-hz-switcher", "native");
229      host.setAttribute("data-hz-lang", lang);
230      host.innerHTML = '<div class="hz-native-switcher" style="position:relative;display:inline-flex;align-items:center">'
231        + '<button type="button" class="hz-native-current" aria-haspopup="listbox" aria-expanded="false" aria-label="Taal kiezen" style="appearance:none;background:transparent;border:0;padding:4px;cursor:pointer;display:inline-flex;align-items:center;line-height:0">' + flagSvg(lang) + '</button>'
232        + '<div class="hz-native-menu" role="listbox" style="display:none;position:absolute;right:0;top:calc(100% + 8px);z-index:10000;background:#fff;color:#111;box-shadow:0 12px 32px rgba(0,0,0,.18);border-radius:10px;padding:6px;min-width:44px">'
233        + SUPPORTED.map(function (l) { return '<button type="button" role="option" aria-selected="' + (lang === l ? "true" : "false") + '" data-hz-lang="' + l + '" title="' + labels[l] + '" style="appearance:none;width:100%;border:0;background:' + (lang === l ? "rgba(0,0,0,.06)" : "transparent") + ';border-radius:7px;padding:6px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:0">' + flagSvg(l) + '</button>'; }).join("")
234        + '</div></div>';
235      var current = host.querySelector(".hz-native-current");
236      var menu = host.querySelector(".hz-native-menu");
237      current.addEventListener("click", function (e) {
238        e.preventDefault(); e.stopPropagation();
239        var open = menu.style.display !== "none";
240        menu.style.display = open ? "none" : "block";
241        current.setAttribute("aria-expanded", open ? "false" : "true");
242      });
243      host.querySelectorAll("[data-hz-lang]").forEach(function (btn) {
244        btn.addEventListener("click", function (e) {
245          e.preventDefault(); e.stopPropagation();
246          setLang(btn.getAttribute("data-hz-lang")).then(function () {
247            // Force re-render with new lang.
248            document.querySelectorAll('.lng-cont[data-hz-switcher="native"]').forEach(function (h) {
249              h.removeAttribute("data-hz-lang");
250            });
251            renderFallbackSwitcher();
252          });
253        });
254      });
255    });
256  }
257  document.addEventListener("click", function () {
258    document.querySelectorAll(".hz-native-menu").forEach(function (m) { m.style.display = "none"; });
259    document.querySelectorAll(".hz-native-current").forEach(function (b) { b.setAttribute("aria-expanded", "false"); });
260  });
261  // Try to render the fallback switcher; React switcher will mark itself with data-hz-switcher="react".
262  if (document.readyState !== "loading") renderFallbackSwitcher();
263  else document.addEventListener("DOMContentLoaded", renderFallbackSwitcher);
264
265  // Re-render fallback if a NEW .lng-cont is added later. Throttle + ignore mutations
266  // that originate inside an existing switcher (otherwise innerHTML rewrites trigger
267  // an infinite mutation loop that locks up the tab).
268  var lngScheduled = false;
269  function scheduleLngRender(muts) {
270    var relevant = false;
271    for (var i = 0; i < muts.length; i++) {
272      var added = muts[i].addedNodes;
273      if (!added || !added.length) continue;
274      for (var j = 0; j < added.length; j++) {
275        var n = added[j];
276        if (n.nodeType !== 1) continue;
277        if (n.closest && n.closest(".hz-native-switcher")) continue;
278        if (n.classList && n.classList.contains("lng-cont")) { relevant = true; break; }
279        if (n.querySelector && n.querySelector(".lng-cont:not([data-hz-switcher])")) { relevant = true; break; }
280      }
281      if (relevant) break;
282    }
283    if (!relevant || lngScheduled) return;
284    lngScheduled = true;
285    requestAnimationFrame(function () { lngScheduled = false; renderFallbackSwitcher(); });
286  }
287  var lngObs = new MutationObserver(scheduleLngRender);
288  if (document.body) lngObs.observe(document.body, { childList: true, subtree: true });
289  else document.addEventListener("DOMContentLoaded", function () {
290    lngObs.observe(document.body, { childList: true, subtree: true });
291  });
292})();

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.