(function () { "use strict"; const DEFAULT_LOCALE = "en"; const SUPPORTED_LOCALES = ["en", "zh-CN"]; const STORAGE_KEY = "occ.locale"; const loadedMessages = new Map(); const SKIP_TAGS = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA", "CODE", "PRE"]); let currentLocale = DEFAULT_LOCALE; let activeMessages = {}; let observer = null; let isApplyingTranslations = false; function normalizeLocale(input) { if (!input || typeof input !== "string") return DEFAULT_LOCALE; const lc = input.toLowerCase(); if (lc === "zh" || lc.startsWith("zh-")) return "zh-CN"; return "en"; } function getInitialLocale() { const fromQuery = new URLSearchParams(window.location.search).get("lang"); if (fromQuery) return normalizeLocale(fromQuery); const fromStorage = localStorage.getItem(STORAGE_KEY); if (fromStorage) return normalizeLocale(fromStorage); return normalizeLocale(navigator.language || DEFAULT_LOCALE); } async function loadLocaleMessages(locale) { const normalized = normalizeLocale(locale); if (loadedMessages.has(normalized)) return loadedMessages.get(normalized); const response = await fetch(`/locales/${normalized}.json`, { cache: "no-cache" }); if (!response.ok) throw new Error(`Failed to load locale: ${normalized}`); const data = await response.json(); loadedMessages.set(normalized, data || {}); return data || {}; } function getByPath(obj, path) { return String(path) .split(".") .reduce( (acc, key) => acc && Object.prototype.hasOwnProperty.call(acc, key) ? acc[key] : undefined, obj, ); } function interpolate(template, params = {}) { if (typeof template !== "string") return template; return template.replace(/\{(\w+)\}/g, (_, key) => { return Object.prototype.hasOwnProperty.call(params, key) ? String(params[key]) : `{${key}}`; }); } function t(key, params = {}, fallback = undefined) { const value = getByPath(activeMessages, key); if (value === undefined || value === null) { if (fallback !== undefined) return interpolate(fallback, params); return String(key); } return interpolate(value, params); } function buildReverseMap(source = {}) { const reversed = {}; for (const [from, to] of Object.entries(source)) { if (typeof from !== "string" || typeof to !== "string") continue; if (!Object.prototype.hasOwnProperty.call(reversed, to)) { reversed[to] = from; } } return reversed; } function getExactPhraseMap() { const exact = getByPath(activeMessages, "phrases.exact") || {}; if (currentLocale !== DEFAULT_LOCALE) return exact; const zh = loadedMessages.get("zh-CN") || {}; const zhExact = getByPath(zh, "phrases.exact") || {}; return buildReverseMap(zhExact); } function getPatternRules() { const localeRules = getByPath(activeMessages, "phrases.patterns"); if (Array.isArray(localeRules)) return localeRules; if (currentLocale !== DEFAULT_LOCALE) return []; const zh = loadedMessages.get("zh-CN") || {}; const zhRules = getByPath(zh, "phrases.reversePatterns"); return Array.isArray(zhRules) ? zhRules : []; } function translateTextValue(input) { if (typeof input !== "string") return input; if (!input.trim()) return input; const leading = input.match(/^\s*/)?.[0] || ""; const trailing = input.match(/\s*$/)?.[0] || ""; let core = input.trim(); const exactMap = getExactPhraseMap(); if (Object.prototype.hasOwnProperty.call(exactMap, core)) { return `${leading}${exactMap[core]}${trailing}`; } for (const rule of getPatternRules()) { if (!rule || typeof rule.pattern !== "string" || typeof rule.replace !== "string") continue; try { const regex = new RegExp(rule.pattern); if (regex.test(core)) { core = core.replace(regex, rule.replace); return `${leading}${core}${trailing}`; } } catch { continue; } } return input; } function setAttrIfChanged(el, attr, value) { if (!el || typeof value !== "string") return; if (el.getAttribute(attr) !== value) { el.setAttribute(attr, value); } } function translateLooseAttributes(el) { if (el.hasAttribute("data-i18n-title")) { setAttrIfChanged(el, "title", t(el.getAttribute("data-i18n-title"))); } else if (el.hasAttribute("title")) { setAttrIfChanged(el, "title", translateTextValue(el.getAttribute("title"))); } if (el.hasAttribute("data-i18n-placeholder")) { setAttrIfChanged(el, "placeholder", t(el.getAttribute("data-i18n-placeholder"))); } else if (el.hasAttribute("placeholder")) { setAttrIfChanged(el, "placeholder", translateTextValue(el.getAttribute("placeholder"))); } if (el.hasAttribute("data-i18n-aria-label")) { setAttrIfChanged(el, "aria-label", t(el.getAttribute("data-i18n-aria-label"))); } else if (el.hasAttribute("aria-label")) { setAttrIfChanged(el, "aria-label", translateTextValue(el.getAttribute("aria-label"))); } if (el.hasAttribute("data-tooltip")) { setAttrIfChanged(el, "data-tooltip", translateTextValue(el.getAttribute("data-tooltip"))); } } function translateTextNodes(root) { if (!root) return; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes = []; let node; while ((node = walker.nextNode())) { nodes.push(node); } for (const textNode of nodes) { const parent = textNode.parentElement; if (!parent) continue; if (SKIP_TAGS.has(parent.tagName)) continue; if (parent.hasAttribute("data-i18n")) continue; const translated = translateTextValue(textNode.nodeValue || ""); if (translated !== textNode.nodeValue) { textNode.nodeValue = translated; } } } function translateElement(el) { const textKey = el.getAttribute("data-i18n"); if (textKey) { const translatedText = t(textKey); if (el.textContent !== translatedText) { el.textContent = translatedText; } } const titleKey = el.getAttribute("data-i18n-title"); if (titleKey) { setAttrIfChanged(el, "title", t(titleKey)); } const placeholderKey = el.getAttribute("data-i18n-placeholder"); if (placeholderKey) { setAttrIfChanged(el, "placeholder", t(placeholderKey)); } const ariaLabelKey = el.getAttribute("data-i18n-aria-label"); if (ariaLabelKey) { setAttrIfChanged(el, "aria-label", t(ariaLabelKey)); } translateLooseAttributes(el); } function translateSubtree(root = document) { if (!root) return; isApplyingTranslations = true; if (root.nodeType === Node.ELEMENT_NODE) { translateElement(root); } root .querySelectorAll( "[data-i18n], [data-i18n-title], [data-i18n-placeholder], [data-i18n-aria-label]", ) .forEach(translateElement); const elementRoot = root.nodeType === Node.DOCUMENT_NODE ? root.body || root.documentElement : root; if (elementRoot) { translateTextNodes(elementRoot); if (elementRoot.querySelectorAll) { elementRoot .querySelectorAll("[title], [placeholder], [aria-label], [data-tooltip]") .forEach(translateLooseAttributes); } } isApplyingTranslations = false; } function updateDocumentLang() { document.documentElement.lang = currentLocale; } function renderLanguageSwitcher() { const header = document.querySelector("header"); if (!header) return; let container = document.getElementById("lang-switcher"); let select = document.getElementById("lang-select"); if (!container) { container = document.createElement("div"); container.id = "lang-switcher"; container.style.display = "inline-flex"; container.style.alignItems = "center"; container.style.gap = "6px"; container.style.marginLeft = "12px"; container.style.flexShrink = "0"; const label = document.createElement("span"); label.style.fontSize = "0.75rem"; label.style.opacity = "0.8"; label.textContent = "🌐"; container.appendChild(label); select = document.createElement("select"); select.id = "lang-select"; select.style.fontSize = "0.8rem"; select.style.padding = "3px 8px"; select.style.background = "var(--card-bg, #161b22)"; select.style.color = "var(--text, #c9d1d9)"; select.style.border = "1px solid var(--border, #30363d)"; select.style.borderRadius = "4px"; select.style.cursor = "pointer"; select.innerHTML = ` `; container.appendChild(select); const targetHost = header.querySelector(".header-left") || header; targetHost.appendChild(container); } select = document.getElementById("lang-select"); if (select && !select.dataset.i18nBound) { select.addEventListener("change", (e) => { setLocale(e.target.value, { persist: true }); }); select.dataset.i18nBound = "1"; } if (select) { if (!select.options.length) { select.innerHTML = ` `; } select.value = currentLocale; } } function installObserver() { if (observer) observer.disconnect(); if (!document.body) return; observer = new MutationObserver((mutations) => { if (isApplyingTranslations) return; isApplyingTranslations = true; try { for (const mutation of mutations) { if (mutation.type === "childList") { mutation.addedNodes.forEach((addedNode) => { if (addedNode.nodeType === Node.TEXT_NODE) { const parent = addedNode.parentElement; if (parent && !SKIP_TAGS.has(parent.tagName)) { const translated = translateTextValue(addedNode.nodeValue || ""); if (translated !== addedNode.nodeValue) addedNode.nodeValue = translated; } return; } if (addedNode.nodeType === Node.ELEMENT_NODE) { translateSubtree(addedNode); } }); } if (mutation.type === "characterData" && mutation.target?.nodeType === Node.TEXT_NODE) { const textNode = mutation.target; const parent = textNode.parentElement; if (parent && !SKIP_TAGS.has(parent.tagName)) { const translated = translateTextValue(textNode.nodeValue || ""); if (translated !== textNode.nodeValue) textNode.nodeValue = translated; } } } } finally { isApplyingTranslations = false; } }); observer.observe(document.body, { subtree: true, childList: true, characterData: true, attributes: false, }); } async function setLocale(locale, { persist = true } = {}) { const normalized = normalizeLocale(locale); const targetLocale = SUPPORTED_LOCALES.includes(normalized) ? normalized : DEFAULT_LOCALE; let localeMessages; try { localeMessages = await loadLocaleMessages(targetLocale); } catch (error) { console.error("[i18n] Failed to load locale, fallback to English:", error); localeMessages = await loadLocaleMessages(DEFAULT_LOCALE); } currentLocale = targetLocale; activeMessages = localeMessages; if (persist) { localStorage.setItem(STORAGE_KEY, currentLocale); } updateDocumentLang(); translateSubtree(document); renderLanguageSwitcher(); installObserver(); window.dispatchEvent(new CustomEvent("i18n:updated", { detail: { locale: currentLocale } })); } async function init() { await loadLocaleMessages(DEFAULT_LOCALE); await loadLocaleMessages("zh-CN").catch(() => null); const initialLocale = getInitialLocale(); await setLocale(initialLocale, { persist: false }); } window.I18N = { init, t, setLocale, getLocale: () => currentLocale, translateSubtree, }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init, { once: true }); } else { init(); } })();