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.