1(function initializeXAgentI18n(root) { 2 "use strict"; 3 4 const catalog = root && root.XAgentI18nCatalog; 5 if (!root || !root.document || !catalog) return; 6 const doc = root.document; 7 const supported = new Set(catalog.locales); 8 const textSources = new WeakMap(); 9 const attributeSources = new WeakMap(); 10 let currentLocale = "en"; 11 12 function normalizeLocale(raw) { 13 const value = String(raw || "").trim().toLowerCase().replaceAll("_", "-"); 14 if (/^zh-(tw|hk|mo|hant)/.test(value) || value === "zh-hant") return "zh-Hant"; 15 if (value.startsWith("zh")) return "zh-Hans"; 16 return catalog.locales.find((locale) => locale !== "zh-Hant" && locale !== "zh-Hans" && value.startsWith(locale.toLowerCase())) || "en"; 17 } 18 19 function preferredLocale() { 20 const requested = new URLSearchParams(root.location.search).get("lang"); 21 let stored = ""; 22 try { stored = root.localStorage.getItem("agent-id-lang") || ""; } catch (_error) {} 23 const candidates = [requested, stored, ...(root.navigator.languages || [root.navigator.language])]; 24 for (const candidate of candidates) { 25 if (!candidate) continue; 26 const locale = normalizeLocale(candidate); 27 if (supported.has(locale)) return locale; 28 } 29 return "en"; 30 } 31 32 function messageFor(source, locale = currentLocale) { 33 const canonical = catalog.aliases[source] || source; 34 return catalog.messages[canonical]?.[locale] || source; 35 } 36 37 function shouldSkip(node) { 38 const parent = node.parentElement; 39 if (!parent) return true; 40 if (parent.closest("script,style,code,pre,textarea,[data-i18n-skip]")) return true; 41 return parent.classList.contains("lang-en") || parent.classList.contains("lang-zh"); 42 } 43 44 function translateTextNode(node) { 45 if (shouldSkip(node)) return; 46 const visible = node.nodeValue || ""; 47 const trimmed = visible.trim(); 48 if (!trimmed) return; 49 if (!textSources.has(node)) textSources.set(node, trimmed); 50 const source = textSources.get(node); 51 const translated = messageFor(source); 52 if (translated === source && !catalog.messages[catalog.aliases[source] || source]) return; 53 const leading = visible.match(/^\s*/)?.[0] || ""; 54 const trailing = visible.match(/\s*$/)?.[0] || ""; 55 node.nodeValue = `${leading}${translated}${trailing}`; 56 } 57 58 function translateAttributes(element) { 59 const names = ["aria-label", "title", "placeholder"]; 60 let sources = attributeSources.get(element); 61 if (!sources) { 62 sources = {}; 63 names.forEach((name) => { if (element.hasAttribute(name)) sources[name] = element.getAttribute(name); }); 64 attributeSources.set(element, sources); 65 } 66 Object.entries(sources).forEach(([name, source]) => element.setAttribute(name, messageFor(source))); 67 } 68 69 function translateTree(rootNode = doc.body) { 70 if (!rootNode) return; 71 if (rootNode.nodeType === Node.TEXT_NODE) { 72 translateTextNode(rootNode); 73 return; 74 } 75 if (rootNode.nodeType !== Node.ELEMENT_NODE) return; 76 translateAttributes(rootNode); 77 const walker = doc.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); 78 let node = walker.nextNode(); 79 while (node) { 80 if (node.nodeType === Node.TEXT_NODE) translateTextNode(node); 81 else translateAttributes(node); 82 node = walker.nextNode(); 83 } 84 } 85 86 function ensureSelector() { 87 let select = doc.querySelector("[data-language-select]"); 88 if (select) return select; 89 const control = doc.createElement("label"); 90 control.className = "site-language-control"; 91 control.setAttribute("aria-label", "Language"); 92 control.innerHTML = '<span aria-hidden="true">Aa</span><select data-language-select aria-label="Language"></select>'; 93 select = control.querySelector("select"); 94 const header = doc.querySelector(".site-header .nav, .topbar .nav, .gtm-nav"); 95 if (header) { 96 const mobile = header.querySelector?.(".mobile-nav"); 97 header.insertBefore(control, mobile || null); 98 } else { 99 control.classList.add("is-floating"); 100 doc.body.prepend(control); 101 } 102 return select; 103 } 104 105 function fillSelector(select) { 106 if (!select || select.options.length === catalog.locales.length) return; 107 select.replaceChildren(...catalog.locales.map((locale) => { 108 const option = doc.createElement("option"); 109 option.value = locale; 110 option.textContent = catalog.labels[locale]; 111 return option; 112 })); 113 } 114 115 function setLocale(raw, persist = true) { 116 const locale = supported.has(raw) ? raw : normalizeLocale(raw); 117 currentLocale = locale; 118 doc.documentElement.lang = locale; 119 doc.body.dataset.lang = locale; 120 const select = ensureSelector(); 121 fillSelector(select); 122 select.value = locale; 123 translateTree(doc.body); 124 const originalTitle = doc.documentElement.dataset.i18nTitle || doc.title; 125 doc.documentElement.dataset.i18nTitle = originalTitle; 126 doc.title = messageFor(originalTitle, locale); 127 const description = doc.querySelector('meta[name="description"]'); 128 if (description) { 129 const original = description.dataset.i18nSource || description.content; 130 description.dataset.i18nSource = original; 131 description.content = messageFor(original, locale); 132 } 133 if (persist) {
134 try { root.localStorage.setItem("agent-id-lang", locale); } catch (_error) {} 135 try { 136 const url = new URL(root.location.href); 137 url.searchParams.set("lang", locale); 138 root.history.replaceState(root.history.state, "", url); 139 } catch (_error) {} 140 } 141 root.dispatchEvent(new CustomEvent("xagent:languagechange", { detail: { locale } })); 142 } 143 144 const select = ensureSelector(); 145 fillSelector(select); 146 select.addEventListener("change", (event) => setLocale(event.target.value)); 147 setLocale(preferredLocale(), false); 148 149 const observer = new MutationObserver((records) => { 150 for (const record of records) { 151 for (const node of record.addedNodes) translateTree(node); 152 } 153 }); 154 observer.observe(doc.body, { childList: true, subtree: true }); 155 156 root.XAgentI18n = { 157 locales: [...catalog.locales], 158 getLocale: () => currentLocale, 159 setLocale, 160 t: (source) => messageFor(source), 161 translate: translateTree, 162 }; 163})(typeof window !== "undefined" ? window : globalThis);
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.