i18n Key Finder

自动捕获页面 i18n JSON 请求,点击任意文字即可查找对应的多语言 key(支持精确匹配、反查匹配、变量模糊匹配)。提供 Key/翻译值搜索、页面高亮定位、React 组件源码跳转(VS Code/Cursor/WebStorm/GoLand)、硬编码文本检测、多语言对照与缺失检测,一键复制 key。

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         i18n Key Finder
// @namespace    https://github.com/i18n-find
// @version      1.0.12
// @description  自动捕获页面 i18n JSON 请求,点击任意文字即可查找对应的多语言 key(支持精确匹配、反查匹配、变量模糊匹配)。提供 Key/翻译值搜索、页面高亮定位、React 组件源码跳转(VS Code/Cursor/WebStorm/GoLand)、硬编码文本检测、多语言对照与缺失检测,一键复制 key。
// @license      MIT
// @include      *://*shopline*/*
// @include      *://localhost/*
// @include      *://localhost:*/*
// @include      *://127.0.0.1/*
// @include      *://127.0.0.1:*/*
// @match        *://*.smartpushedm.com/*
// @connect      *
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @run-at       document-start
// ==/UserScript==

(function() {
	"use strict";
	var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
	var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
	var _GM_xmlhttpRequest = (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();
	var siteKey = location.hostname;
	function loadSources() {
		const raw = _GM_getValue(`i18n_sources_${siteKey}`, "");
		if (!raw) return [];
		try {
			return JSON.parse(raw);
		} catch {
			return [];
		}
	}
	function saveSources(list) {
		_GM_setValue(`i18n_sources_${siteKey}`, JSON.stringify(list));
	}
	var sources = loadSources();
	var I18N_URL_RE = /\/i18n\/.*\.json/;
	function extractName(url) {
		try {
			const parts = new URL(url).pathname.split("/");
			const i18nIdx = parts.indexOf("i18n");
			if (i18nIdx >= 0 && i18nIdx + 1 < parts.length) return parts[i18nIdx + 1];
		} catch {}
		return "auto";
	}
	function extractLang(url) {
		try {
			const lang = (new URL(url).pathname.split("/").pop() || "").split(".")[0];
			if (lang) return lang;
		} catch {}
		return "unknown";
	}
	function addCapturedSource(url) {
		if (sources.some((s) => s.url === url)) return;
		const name = extractName(url);
		sources.push({
			name,
			url
		});
		saveSources(sources);
		clearCache();
		refreshSettingsPanel();
	}
	function setupRequestCapture() {
		const origOpen = XMLHttpRequest.prototype.open;
		XMLHttpRequest.prototype.open = function(method, url, ...rest) {
			const urlStr = url.toString();
			if (I18N_URL_RE.test(urlStr)) addCapturedSource(urlStr);
			return origOpen.apply(this, [
				method,
				url,
				...rest
			]);
		};
		const origFetch = window.fetch;
		window.fetch = function(input, init) {
			const url = input instanceof Request ? input.url : input.toString();
			if (I18N_URL_RE.test(url)) addCapturedSource(url);
			return origFetch.call(this, input, init);
		};
		new PerformanceObserver((list) => {
			for (const entry of list.getEntries()) if (I18N_URL_RE.test(entry.name)) addCapturedSource(entry.name);
		}).observe({
			type: "resource",
			buffered: true
		});
	}
	var i18nMap = null;
	var flatMap = null;
	var keySourceMap = null;
	var perSourceFlat = new Map();
	var sourceMeta = new Map();
	var fuzzyIndex = [];
	var searchIndex = [];
	var loading = false;
	var loadedKey = "";
	function flattenJson(obj, prefix, result) {
		for (const key of Object.keys(obj)) {
			const fullKey = prefix ? `${prefix}.${key}` : key;
			const val = obj[key];
			if (typeof val === "string") result.set(fullKey, val);
			else if (val && typeof val === "object" && !Array.isArray(val)) flattenJson(val, fullKey, result);
		}
	}
	function buildReverseMap(flat) {
		const reverse = new Map();
		for (const [key, val] of flat) {
			const trimmed = val.trim();
			if (!trimmed) continue;
			const existing = reverse.get(trimmed);
			if (existing) existing.push(key);
			else reverse.set(trimmed, [key]);
		}
		return reverse;
	}
	function clearCache() {
		i18nMap = null;
		flatMap = null;
		keySourceMap = null;
		perSourceFlat = new Map();
		sourceMeta = new Map();
		fuzzyIndex = [];
		searchIndex = [];
		loadedKey = "";
	}
	function sourcesCacheKey() {
		return sources.map((s) => s.url).sort().join("|");
	}
	function fetchOneJson(url) {
		return new Promise((resolve, reject) => {
			_GM_xmlhttpRequest({
				method: "GET",
				url,
				onload(res) {
					try {
						resolve(JSON.parse(res.responseText));
					} catch (e) {
						reject(e);
					}
				},
				onerror: (err) => reject(err)
			});
		});
	}
	function fetchI18nData() {
		if (sources.length === 0) return Promise.reject(new Error("未配置数据源,请等待自动捕获或手动添加"));
		const key = sourcesCacheKey();
		if (i18nMap && flatMap && loadedKey === key) return Promise.resolve({
			flat: flatMap,
			reverse: i18nMap
		});
		if (loadedKey !== key) clearCache();
		if (loading) return new Promise((resolve) => {
			const timer = setInterval(() => {
				if (i18nMap && flatMap) {
					clearInterval(timer);
					resolve({
						flat: flatMap,
						reverse: i18nMap
					});
				}
			}, 100);
		});
		loading = true;
		return Promise.all(sources.map((s) => fetchOneJson(s.url).catch(() => null))).then((results) => {
			const merged = new Map();
			const srcMap = new Map();
			const perSrc = new Map();
			const metaMap = new Map();
			for (let i = 0; i < results.length; i++) {
				const json = results[i];
				if (!json) continue;
				const tmp = new Map();
				flattenJson(json, "", tmp);
				const { name, url } = sources[i];
				const lang = extractLang(url);
				perSrc.set(url, tmp);
				metaMap.set(url, {
					name,
					lang,
					label: lang !== "unknown" ? `${name}/${lang}` : name
				});
				for (const [k, v] of tmp) if (!merged.has(k)) {
					merged.set(k, v);
					srcMap.set(k, name);
				}
			}
			flatMap = merged;
			keySourceMap = srcMap;
			perSourceFlat = perSrc;
			sourceMeta = metaMap;
			i18nMap = buildReverseMap(merged);
			const varRe = /\{\{.*?\}\}|\$\{.*?\}|\{[a-zA-Z_]\w*\}/g;
			const fIdx = [];
			for (const [k, v] of merged) if (varRe.test(v)) {
				varRe.lastIndex = 0;
				const s = v.replace(varRe, "").trim();
				if (s) fIdx.push({
					key: k,
					stripped: s
				});
			}
			fuzzyIndex = fIdx;
			const sIdx = [];
			for (const [k, v] of merged) sIdx.push({
				key: k,
				value: v,
				keyLower: k.toLowerCase(),
				valueLower: v.toLowerCase()
			});
			searchIndex = sIdx;
			loadedKey = key;
			return {
				flat: flatMap,
				reverse: i18nMap
			};
		}).finally(() => {
			loading = false;
		});
	}
	var inlineScriptBlocked = false;
	function detectInlineScriptBlocked() {
		try {
			const probe = document.createElement("script");
			probe.textContent = `document.documentElement.dataset.i18nCspProbe='1';`;
			document.documentElement.appendChild(probe);
			probe.remove();
			inlineScriptBlocked = document.documentElement.dataset.i18nCspProbe !== "1";
			delete document.documentElement.dataset.i18nCspProbe;
		} catch {
			inlineScriptBlocked = true;
		}
	}
	function isExactMatchReady() {
		return document.documentElement.dataset.i18nHookReady === "1";
	}
	function tryHookI18n() {
		const s = document.createElement("script");
		s.textContent = `(function(){
if(window.__i18nKeyMap)return;
window.__i18nUsedKeys=Object.create(null);
window.__i18nKeyMap=[];
var keyIdx={};
function hook(inst){
  if(!inst||inst.__i18nHooked)return false;
  var orig=inst.t.bind(inst);
  inst.t=function(){var key=arguments[0];
    var result=orig.apply(null,arguments);
    if(typeof key==='string'){
      window.__i18nUsedKeys[key]=1;
      if(typeof result==='string'){
        var idx=keyIdx[key];
        if(idx===undefined){idx=window.__i18nKeyMap.length;window.__i18nKeyMap.push(key);keyIdx[key]=idx;}
        var bits=idx.toString(2),m='\\u200B\\u200B\\u200B';
        for(var i=0;i<bits.length;i++)m+=bits[i]==='0'?'\\u200B':'\\u200C';
        return m+'\\u200D'+result;}}
    return result;};
  inst.__i18nHooked=true;
  document.documentElement.dataset.i18nHookReady='1';
  var op=history.pushState;
  history.pushState=function(){window.__i18nUsedKeys=Object.create(null);window.__i18nKeyMap=[];keyIdx={};return op.apply(this,arguments);};
  window.addEventListener('popstate',function(){window.__i18nUsedKeys=Object.create(null);window.__i18nKeyMap=[];keyIdx={};});
  setTimeout(function(){if(inst.emit)inst.emit('languageChanged',inst.language);},200);
  return true;
}
function attempt(){return hook(window.__i18n__);}
if(!attempt()){var n=0,id=setInterval(function(){if(attempt()||++n>30)clearInterval(id);},500);}
})();`;
		document.documentElement.appendChild(s);
		s.remove();
	}
	var ZWS = String.fromCharCode(8203);
	var ZWNJ = String.fromCharCode(8204);
	var ZWJ = String.fromCharCode(8205);
	var MARKER_START = "​​​";
	var MARKER_RE = new RegExp(`[${ZWS}${ZWNJ}${ZWJ}]`, "g");
	function extractExactKey(text) {
		const si = text.indexOf(MARKER_START);
		if (si < 0) return null;
		let i = si + 3;
		let bits = "";
		while (i < text.length) {
			const c = text[i];
			if (c === ZWJ) break;
			if (c === ZWS) bits += "0";
			else if (c === ZWNJ) bits += "1";
			else break;
			i++;
		}
		if (!bits || text[i] !== ZWJ) return null;
		const idx = parseInt(bits, 2);
		try {
			const el = document.createElement("script");
			el.textContent = `document.documentElement.dataset.i18nExact=(window.__i18nKeyMap&&window.__i18nKeyMap[${idx}])||'';`;
			document.documentElement.appendChild(el);
			el.remove();
			return document.documentElement.dataset.i18nExact || null;
		} catch {
			return null;
		}
	}
	function stripMarkers(text) {
		return text.replace(MARKER_RE, "");
	}
	function escapeHtml(s) {
		return s.replace(/[&<>"']/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === "\"" ? "&quot;" : "&#39;");
	}
	var _usedKeysCache = null;
	var _usedKeysCacheTime = 0;
	function readUsedKeysFromPage() {
		const now = Date.now();
		if (_usedKeysCache && now - _usedKeysCacheTime < 1e3) return _usedKeysCache;
		try {
			const el = document.createElement("script");
			el.textContent = `document.documentElement.dataset.i18nUsed=JSON.stringify(Object.keys(window.__i18nUsedKeys||{}));`;
			document.documentElement.appendChild(el);
			el.remove();
			const raw = document.documentElement.dataset.i18nUsed;
			if (raw) {
				_usedKeysCache = new Set(JSON.parse(raw));
				_usedKeysCacheTime = now;
				return _usedKeysCache;
			}
		} catch {}
		return new Set();
	}
	function isKeyUsedOnPage(key) {
		return readUsedKeysFromPage().has(key);
	}
	function findKeys(map, text) {
		const trimmed = text.trim();
		if (!trimmed) return {
			used: [],
			other: []
		};
		let all = map.get(trimmed) ?? [];
		let fuzzy = false;
		if (all.length === 0 && fuzzyIndex.length > 0) {
			fuzzy = true;
			const matched = [];
			for (const entry of fuzzyIndex) if (entry.stripped.includes(trimmed)) {
				matched.push(entry.key);
				if (matched.length >= 50) break;
			}
			all = matched;
			if (all.length === 0) fuzzy = false;
		}
		const pageKeys = readUsedKeysFromPage();
		if (pageKeys.size === 0) return {
			used: [],
			other: all,
			fuzzy
		};
		const used = [];
		const other = [];
		for (const k of all) (pageKeys.has(k) ? used : other).push(k);
		return {
			used,
			other,
			fuzzy
		};
	}
	var toastEl = null;
	var toastTimer = null;
	function showToast(msg) {
		if (!toastEl || !document.body.contains(toastEl)) {
			toastEl = document.createElement("div");
			toastEl.style.cssText = `
      position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:2147483647;
      background:#333;color:#fff;padding:8px 20px;border-radius:6px;font-size:13px;
      font-family:'SF Mono',Monaco,monospace;box-shadow:0 4px 12px rgba(0,0,0,0.15);
      transition:opacity 0.3s;pointer-events:none;white-space:nowrap;
    `;
			document.body.appendChild(toastEl);
		}
		toastEl.textContent = msg;
		toastEl.style.opacity = "1";
		toastEl.style.display = "block";
		if (toastTimer) clearTimeout(toastTimer);
		toastTimer = setTimeout(() => {
			if (toastEl) toastEl.style.opacity = "0";
			toastTimer = setTimeout(() => {
				if (toastEl) toastEl.style.display = "none";
			}, 300);
		}, 1500);
	}
	function renderMultiLang(key) {
		if (perSourceFlat.size <= 1) return "";
		const entries = [];
		for (const [url, flat] of perSourceFlat) {
			const val = flat.get(key);
			if (val !== void 0) entries.push({
				src: sourceMeta.get(url)?.label ?? url,
				val
			});
		}
		if (entries.length <= 1) return "";
		const items = entries.map((e) => {
			const display = e.val.length > 60 ? e.val.slice(0, 60) + "..." : e.val;
			return `<div style="padding:2px 0;font-size:11px;"><span style="color:#8b5cf6;font-weight:600;">[${escapeHtml(e.src)}]</span> <span style="color:#555;">${escapeHtml(display)}</span></div>`;
		}).join("");
		return `
    <div class="i18n-toggle-multilang" style="margin:4px 0 2px;color:#8b5cf6;font-size:11px;cursor:pointer;user-select:none;">多语言对照(${entries.length})▸</div>
    <div class="i18n-multilang-list" style="display:none;padding:4px 8px;margin:2px 0;background:#faf5ff;border:1px solid #e9d5ff;border-radius:4px;">${items}</div>
  `;
	}
	var highlightedEls = [];
	var highlightBadge = null;
	function clearHighlights() {
		for (const el of highlightedEls) {
			el.style.removeProperty("outline");
			el.style.removeProperty("background-color");
		}
		highlightedEls = [];
		if (highlightBadge) highlightBadge.style.display = "none";
	}
	function showHighlightBadge(count) {
		if (!highlightBadge || !document.body.contains(highlightBadge)) {
			highlightBadge = document.createElement("div");
			highlightBadge.style.cssText = `
      position:fixed;top:20px;right:20px;z-index:2147483647;
      background:#f59e0b;color:#fff;padding:8px 16px;border-radius:20px;
      font-size:13px;font-family:'SF Mono',Monaco,monospace;cursor:pointer;
      box-shadow:0 2px 8px rgba(245,158,11,0.3);
    `;
			highlightBadge.addEventListener("click", clearHighlights);
			document.body.appendChild(highlightBadge);
		}
		highlightBadge.textContent = `高亮中: ${count} 处 (点击清除)`;
		highlightBadge.style.display = "block";
	}
	function findElementsUsingKey(key) {
		const s = document.createElement("script");
		s.textContent = `document.documentElement.dataset.i18nKeyIdx=(function(){var m=window.__i18nKeyMap||[];for(var i=0;i<m.length;i++){if(m[i]===${JSON.stringify(key)})return i;}return -1;})().toString();`;
		document.documentElement.appendChild(s);
		s.remove();
		const idx = parseInt(document.documentElement.dataset.i18nKeyIdx || "-1", 10);
		delete document.documentElement.dataset.i18nKeyIdx;
		let marker = "";
		if (idx >= 0) {
			const bits = idx.toString(2);
			marker = MARKER_START;
			for (const b of bits) marker += b === "0" ? ZWS : ZWNJ;
			marker += ZWJ;
		}
		const translatedValue = flatMap?.get(key) ?? "";
		const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
		const matched = new Set();
		const result = [];
		let node;
		while (node = walker.nextNode()) {
			const text = node.textContent;
			if (!text) continue;
			const parent = node.parentElement;
			if (!parent || matched.has(parent)) continue;
			if (parent.closest("#i18n-finder-tooltip,#i18n-finder-panel,#i18n-finder-settings,#i18n-finder-help,#i18n-finder-toolbar,#i18n-react-overlay,#i18n-react-label")) continue;
			let hit = false;
			if (marker && text.includes(marker)) hit = true;
			else if (!marker && translatedValue && stripMarkers(text).trim() === translatedValue.trim()) hit = true;
			if (hit) {
				matched.add(parent);
				result.push(parent);
			}
		}
		return result;
	}
	function highlightKeyOnPage(key) {
		clearHighlights();
		const els = findElementsUsingKey(key);
		for (const el of els) {
			el.style.outline = "2px solid #f59e0b";
			el.style.backgroundColor = "rgba(245,158,11,0.1)";
			highlightedEls.push(el);
		}
		if (highlightedEls.length > 0) {
			highlightedEls[0].scrollIntoView({
				behavior: "smooth",
				block: "center"
			});
			showHighlightBadge(highlightedEls.length);
			showToast(`高亮了 ${highlightedEls.length} 处`);
		} else showToast("当前页面未找到使用该 key 的元素");
	}
	var hardcodeHighlights = [];
	var hardcodePanel = null;
	var hardcodeActive = false;
	var hcButton = null;
	function clearHardcodeHighlights() {
		for (const el of hardcodeHighlights) {
			el.style.removeProperty("outline");
			el.style.removeProperty("background-color");
		}
		hardcodeHighlights = [];
		if (hardcodePanel) hardcodePanel.style.display = "none";
		hardcodeActive = false;
		if (hcButton) {
			hcButton.style.background = "#fff";
			hcButton.style.color = "#666";
			hcButton.style.borderColor = "#ddd";
		}
	}
	function detectHardcoded() {
		if (hardcodeActive) {
			clearHardcodeHighlights();
			return;
		}
		if (inlineScriptBlocked) {
			showToast("当前站点 CSP 限制内联脚本,硬编码检测不可用");
			return;
		}
		if (!isExactMatchReady()) {
			showToast("需要精确匹配模式才能检测硬编码(未检测到 window.__i18n__)");
			return;
		}
		clearHardcodeHighlights();
		const SKIP_TAGS = new Set([
			"SCRIPT",
			"STYLE",
			"NOSCRIPT",
			"IFRAME",
			"SVG",
			"CODE",
			"PRE"
		]);
		const seen = new Set();
		const candidates = [];
		const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
		let node;
		while (node = walker.nextNode()) {
			const raw = node.textContent;
			if (!raw) continue;
			const text = raw.trim();
			if (text.length < 2) continue;
			const parent = node.parentElement;
			if (!parent || seen.has(parent)) continue;
			if (SKIP_TAGS.has(parent.tagName)) continue;
			if (parent.closest("#i18n-finder-tooltip,#i18n-finder-panel,#i18n-finder-settings,#i18n-finder-help,#i18n-finder-toolbar,#i18n-hardcode-panel")) continue;
			if (raw.includes(MARKER_START)) continue;
			if (/^[\d\s.,:;!?@#$%^&*()+={}\[\]|\\/<>'"~`\-_]+$/.test(text)) continue;
			if (!/[一-鿿㐀-䶿a-zA-Z]{2,}/u.test(text)) continue;
			seen.add(parent);
			candidates.push({
				el: parent,
				text: text.length > 40 ? text.slice(0, 40) + "..." : text
			});
		}
		const results = candidates.filter((c) => c.el.offsetParent || c.el.tagName === "BODY" || c.el.tagName === "HTML");
		for (const r of results) {
			r.el.style.outline = "2px dashed #ef4444";
			r.el.style.backgroundColor = "rgba(239,68,68,0.08)";
			hardcodeHighlights.push(r.el);
		}
		hardcodeActive = true;
		if (results.length === 0) {
			showToast("未检测到硬编码文本");
			hardcodeActive = false;
			return;
		}
		showToast(`检测到 ${results.length} 处疑似硬编码`);
		showHardcodePanel(results);
	}
	function showHardcodePanel(items) {
		if (!hardcodePanel || !document.body.contains(hardcodePanel)) {
			hardcodePanel = document.createElement("div");
			hardcodePanel.id = "i18n-hardcode-panel";
			hardcodePanel.style.cssText = `
      position:fixed;z-index:2147483647;width:380px;max-height:500px;
      background:#fff;border:1px solid #e0e0e0;border-radius:10px;
      box-shadow:0 4px 20px rgba(0,0,0,0.12);font-family:'SF Mono',Monaco,monospace;
      font-size:13px;display:none;overflow:hidden;
    `;
			document.body.appendChild(hardcodePanel);
		}
		const pos = getToolbarPos();
		hardcodePanel.style.right = `${pos.right + 60}px`;
		hardcodePanel.style.bottom = `${pos.bottom}px`;
		hardcodePanel.innerHTML = `
    <div style="padding:12px 16px;border-bottom:1px solid #eee;display:flex;align-items:center;">
      <div style="font-weight:600;color:#ef4444;font-size:14px;">硬编码检测</div>
      <div style="flex:1;"></div>
      <span style="font-size:11px;color:#999;margin-right:8px;">共 ${items.length} 处</span>
      <div id="i18n-hc-close" style="cursor:pointer;color:#999;font-size:18px;padding:0 4px;line-height:1;">&times;</div>
    </div>
    <div style="max-height:420px;overflow-y:auto;padding:8px 16px;">
      ${items.map((r, i) => `
        <div class="i18n-hc-item" data-idx="${i}" style="padding:6px 8px;margin:4px 0;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;cursor:pointer;font-size:12px;">
          <div style="color:#333;word-break:break-all;">${escapeHtml(r.text)}</div>
          <div style="color:#999;font-size:10px;margin-top:2px;">&lt;${r.el.tagName.toLowerCase()}&gt;${r.el.className ? "." + escapeHtml(String(r.el.className).split(" ")[0]) : ""}</div>
        </div>
      `).join("")}
    </div>
  `;
		hardcodePanel.style.display = "block";
		clampPanelInViewport(hardcodePanel);
		hardcodePanel.querySelector("#i18n-hc-close").addEventListener("click", clearHardcodeHighlights);
		hardcodePanel.querySelectorAll(".i18n-hc-item").forEach((item) => {
			item.addEventListener("click", () => {
				const idx = parseInt(item.dataset.idx, 10);
				if (hardcodeHighlights[idx]) {
					hardcodeHighlights[idx].scrollIntoView({
						behavior: "smooth",
						block: "center"
					});
					const el = hardcodeHighlights[idx];
					el.style.outline = "3px solid #ef4444";
					setTimeout(() => {
						el.style.outline = "2px dashed #ef4444";
					}, 800);
				}
			});
		});
	}
	var missingPanel = null;
	async function detectMissingTranslations() {
		try {
			await fetchI18nData();
		} catch {
			showToast("加载 i18n 数据失败");
			return;
		}
		if (perSourceFlat.size <= 1) {
			showToast("需要至少 2 个数据源才能对比");
			return;
		}
		const groups = new Map();
		for (const [url, meta] of sourceMeta) {
			const arr = groups.get(meta.name);
			if (arr) arr.push(url);
			else groups.set(meta.name, [url]);
		}
		const sections = [];
		for (const [name, urls] of groups) {
			if (urls.length < 2) continue;
			const langs = urls.map((u) => sourceMeta.get(u)?.lang ?? u);
			const allKeys = new Set();
			for (const u of urls) for (const k of perSourceFlat.get(u).keys()) allKeys.add(k);
			const rows = [];
			for (const k of allKeys) {
				const missing = [];
				for (let i = 0; i < urls.length; i++) {
					const v = perSourceFlat.get(urls[i]).get(k);
					if (v === void 0 || v.trim() === "") missing.push(langs[i]);
				}
				if (missing.length > 0) rows.push({
					key: k,
					missing
				});
			}
			rows.sort((a, b) => b.missing.length - a.missing.length || a.key.localeCompare(b.key));
			sections.push({
				name,
				langs,
				rows
			});
		}
		if (sections.length === 0) {
			showToast("未发现可对比的数据源(同项目需 ≥2 种语言)");
			return;
		}
		const totalMissing = sections.reduce((s, sec) => s + sec.rows.length, 0);
		if (totalMissing === 0) {
			showToast("未发现缺失,多语言翻译完整");
			return;
		}
		showToast(`发现 ${totalMissing} 个 key 存在语言缺失`);
		showMissingPanel(sections);
	}
	function showMissingPanel(sections) {
		if (!missingPanel || !document.body.contains(missingPanel)) {
			missingPanel = document.createElement("div");
			missingPanel.id = "i18n-missing-panel";
			missingPanel.style.cssText = `
      position:fixed;z-index:2147483647;width:440px;max-height:560px;
      background:#fff;border:1px solid #e0e0e0;border-radius:10px;
      box-shadow:0 4px 20px rgba(0,0,0,0.12);font-family:'SF Mono',Monaco,monospace;
      font-size:13px;display:none;overflow:hidden;flex-direction:column;
    `;
			document.body.appendChild(missingPanel);
		}
		hideAllPanels();
		const pos = getToolbarPos();
		missingPanel.style.right = `${pos.right + 60}px`;
		missingPanel.style.bottom = `${pos.bottom}px`;
		const body = sections.map((sec) => {
			const head = `<div style="margin:10px 0 4px;font-weight:600;color:#8b5cf6;font-size:12px;">${escapeHtml(sec.name)} <span style="color:#999;font-weight:400;">(${escapeHtml(sec.langs.join(" / "))}) · 缺失 ${sec.rows.length}</span></div>`;
			if (sec.rows.length === 0) return head + `<div style="color:#16a34a;font-size:11px;padding:2px 0;">无缺失</div>`;
			const rows = sec.rows.slice(0, 200).map((r) => {
				const ek = escapeHtml(r.key);
				return `
      <div class="i18n-missing-item" data-key="${ek}" style="padding:6px 8px;margin:3px 0;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;cursor:pointer;">
        <div style="color:#333;font-size:12px;word-break:break-all;">${ek}</div>
        <div style="margin-top:2px;font-size:10px;color:#999;">缺失: ${r.missing.map((l) => `<span style="color:#ef4444;">${escapeHtml(l)}</span>`).join("、")}</div>
      </div>`;
			}).join("");
			const more = sec.rows.length > 200 ? `<div style="color:#999;font-size:11px;padding:4px 0;">仅显示前 200 条,共 ${sec.rows.length} 条</div>` : "";
			return head + rows + more;
		}).join("");
		missingPanel.innerHTML = `
    <div style="padding:12px 16px;border-bottom:1px solid #eee;display:flex;align-items:center;flex-shrink:0;">
      <div style="font-weight:600;color:#333;font-size:14px;">多语言缺失检测</div>
      <div style="flex:1;"></div>
      <div id="i18n-missing-copy" style="font-size:11px;color:#2563eb;cursor:pointer;margin-right:10px;">复制全部</div>
      <div id="i18n-missing-close" style="cursor:pointer;color:#999;font-size:18px;padding:0 4px;line-height:1;">&times;</div>
    </div>
    <div style="padding:8px 16px;overflow-y:auto;flex:1;">${body}</div>
  `;
		missingPanel.style.display = "flex";
		clampPanelInViewport(missingPanel);
		missingPanel.querySelector("#i18n-missing-close").addEventListener("click", () => {
			missingPanel.style.display = "none";
		});
		missingPanel.querySelector("#i18n-missing-copy").addEventListener("click", () => {
			const lines = [];
			let count = 0;
			for (const sec of sections) for (const r of sec.rows) {
				lines.push(`${r.key}\t${r.missing.join(",")}`);
				count++;
			}
			copyToClipboard(lines.join("\n"), `已复制 ${count} 条缺失记录`);
		});
		missingPanel.querySelectorAll(".i18n-missing-item").forEach((item) => {
			item.addEventListener("click", () => {
				const key = item.dataset.key;
				if (!key) return;
				copyToClipboard(key);
				const el = item;
				el.style.background = "#e6f9ee";
				el.style.borderColor = "#34d399";
				setTimeout(() => {
					el.style.background = "#fffbeb";
					el.style.borderColor = "#fde68a";
				}, 600);
			});
		});
	}
	var EDITORS = [
		{
			key: "vscode",
			label: "VS Code"
		},
		{
			key: "cursor",
			label: "Cursor"
		},
		{
			key: "zed",
			label: "Zed"
		},
		{
			key: "webstorm",
			label: "WebStorm"
		},
		{
			key: "goland",
			label: "GoLand"
		},
		{
			key: "idea",
			label: "IDEA"
		}
	];
	function getPreferredEditor() {
		return _GM_getValue("i18n_editor", "vscode");
	}
	function setPreferredEditor(editor) {
		_GM_setValue("i18n_editor", editor);
	}
	function findReactSource(el) {
		const id = `_rs_${Date.now()}`;
		el.setAttribute("data-rs-id", id);
		const s = document.createElement("script");
		s.textContent = `(function(){
var el=document.querySelector('[data-rs-id="${id}"]');
if(!el)return;el.removeAttribute('data-rs-id');
function gf(n){var ks=Object.keys(n);for(var i=0;i<ks.length;i++){if(ks[i].startsWith('__reactFiber$')||ks[i].startsWith('__reactInternalInstance$'))return n[ks[i]];}return null;}
var cur=el;while(cur){var fb=gf(cur);if(fb){var f=fb;while(f){
if(f._debugSource){var nm='';if(f.type)nm=f.type.displayName||f.type.name||(typeof f.type==='string'?f.type:'');
document.documentElement.dataset.rsResult=JSON.stringify({fileName:f._debugSource.fileName,lineNumber:f._debugSource.lineNumber,columnNumber:f._debugSource.columnNumber||0,componentName:nm||'Component'});return;}
f=f.return;}}cur=cur.parentElement;}
document.documentElement.dataset.rsResult='';})();`;
		document.documentElement.appendChild(s);
		s.remove();
		const raw = document.documentElement.dataset.rsResult;
		delete document.documentElement.dataset.rsResult;
		if (!raw) return null;
		try {
			return JSON.parse(raw);
		} catch {
			return null;
		}
	}
	function findReactComponentChain(el) {
		if (inlineScriptBlocked) return [];
		const id = `_rc_${Date.now()}`;
		el.setAttribute("data-rc-id", id);
		const s = document.createElement("script");
		s.textContent = `(function(){
var el=document.querySelector('[data-rc-id="${id}"]');
if(!el){document.documentElement.dataset.rcResult='[]';return;}
el.removeAttribute('data-rc-id');
function gf(n){var ks=Object.keys(n);for(var i=0;i<ks.length;i++){if(ks[i].startsWith('__reactFiber$')||ks[i].startsWith('__reactInternalInstance$'))return n[ks[i]];}return null;}
function sp(o,d){
if(d>3)return'\\u2026';if(o==null)return o;var t=typeof o;
if(t==='string')return o.length>80?o.slice(0,80)+'\\u2026':o;
if(t==='number'||t==='boolean')return o;
if(t==='function')return'[Function'+(o.name?': '+o.name:'')+']';
if(o&&o.$$typeof)return'[ReactElement]';
if(Array.isArray(o)){if(o.length>5)return'[Array('+o.length+')]';return o.slice(0,5).map(function(x){return sp(x,d+1)});}
if(t==='object'){try{var r={},ks=Object.keys(o),c=0;for(var i=0;i<ks.length&&c<(d===0?50:8);i++){if(ks[i]==='children')continue;r[ks[i]]=sp(o[ks[i]],d+1);c++;}if(ks.length>c+1)r['\\u2026']='+'+(ks.length-c)+' more';return r;}catch(e){return'[Object]';}}
return String(o);}
function eh(fb){
var ic=fb.type&&fb.type.prototype&&fb.type.prototype.isReactComponent;
if(ic)return{hooks:[],classState:sp(fb.memoizedState,0)};
var dht=fb._debugHookTypes||[];
var hks=[],h=fb.memoizedState,idx=0;
while(h&&idx<30){
var tn=dht[idx]||'';
var sk=tn==='useEffect'||tn==='useLayoutEffect'||tn==='useInsertionEffect'||tn==='useDebugValue'||tn==='useImperativeHandle';
if(!sk){var vl=null;
if(tn==='useState'||tn==='useReducer'){vl=h.memoizedState;}
else if(tn==='useRef'){vl=h.memoizedState?h.memoizedState.current:null;}
else if(tn==='useMemo'||tn==='useCallback'){vl=Array.isArray(h.memoizedState)?h.memoizedState[0]:h.memoizedState;}
else if(tn){vl=h.memoizedState;}
else{if(h.queue!=null){tn='useState';vl=h.memoizedState;}
else if(h.memoizedState!=null&&typeof h.memoizedState==='object'){var ms=h.memoizedState;
if(ms.tag!==undefined&&ms.create!==undefined)sk=true;
else if('current' in ms){tn='useRef';vl=ms.current;}
else if(Array.isArray(ms)&&ms.length===2&&Array.isArray(ms[1])){tn='useMemo';vl=ms[0];}
else{tn='unknown';vl=ms;}}
else{tn='unknown';vl=h.memoizedState;}}}
if(!sk&&tn)hks.push({type:tn,value:sp(vl,0),idx:idx});
h=h.next;idx++;}
return{hooks:hks,classState:null};}
var chain=[],seen={};
var cur=el;while(cur){var fb=gf(cur);if(fb){var f=fb;while(f){
if(f._debugSource&&f.type){var nm=f.type.displayName||f.type.name||'';
if(nm&&nm.charCodeAt(0)>=65&&nm.charCodeAt(0)<=90){var uid=nm+'|'+f._debugSource.fileName+':'+f._debugSource.lineNumber;
if(!seen[uid]){seen[uid]=1;var p={};try{p=sp(f.memoizedProps||{},0);}catch(e){}
var hk={hooks:[],classState:null};try{hk=eh(f);}catch(e){}
chain.push({fileName:f._debugSource.fileName,lineNumber:f._debugSource.lineNumber,columnNumber:f._debugSource.columnNumber||0,componentName:nm,props:p,hooks:hk.hooks,classState:hk.classState});}}}
f=f.return;}break;}cur=cur.parentElement;}
document.documentElement.dataset.rcResult=JSON.stringify(chain);
})();`;
		document.documentElement.appendChild(s);
		s.remove();
		const raw = document.documentElement.dataset.rcResult;
		delete document.documentElement.dataset.rcResult;
		if (!raw) return [];
		try {
			const chain = JSON.parse(raw);
			chain.reverse();
			return chain;
		} catch {
			return [];
		}
	}
	function openViaProtocol(url) {
		const a = document.createElement("a");
		a.href = url;
		a.style.display = "none";
		document.body.appendChild(a);
		a.click();
		setTimeout(() => a.remove(), 1e3);
	}
	function openInEditor(source) {
		const editor = getPreferredEditor();
		const { fileName, lineNumber, columnNumber } = source;
		if (editor === "webstorm" || editor === "goland" || editor === "idea") {
			openViaProtocol(`${editor}://open?file=${encodeURIComponent(fileName)}&line=${lineNumber}&column=${columnNumber}`);
			_GM_xmlhttpRequest({
				method: "GET",
				url: `http://localhost:63342/api/file/${fileName}:${lineNumber}:${columnNumber}`,
				onload() {},
				onerror() {}
			});
			return;
		}
		openViaProtocol(`${editor}://file/${fileName}:${lineNumber}:${columnNumber}`);
	}
	function jumpToKeySource(key) {
		if (inlineScriptBlocked) {
			showToast("当前站点 CSP 限制内联脚本,源码跳转不可用");
			return;
		}
		const els = findElementsUsingKey(key);
		if (els.length === 0) {
			showToast("当前页面未找到使用该 key 的元素");
			return;
		}
		const source = findReactSource(els[0]);
		if (source) {
			els[0].scrollIntoView({
				behavior: "smooth",
				block: "center"
			});
			openInEditor(source);
			showToast(`跳转源码: <${source.componentName}>`);
		} else showToast("未找到 React 源码(需开发模式)");
	}
	function isDevMode() {
		const host = location.hostname;
		if (host === "localhost" || host === "127.0.0.1" || host === "[::1]") return true;
		if (location.port && location.port !== "80" && location.port !== "443") return true;
		return false;
	}
	var devMode = isDevMode();
	var inspectorOverlay = null;
	var inspectorLabel = null;
	var altHeld = false;
	var metaHeld = false;
	var metaTimer = null;
	var lastHoverEl = null;
	var lastInspectorChain = [];
	function getInspectorOverlay() {
		if (!inspectorOverlay || !document.body.contains(inspectorOverlay)) {
			inspectorOverlay = document.createElement("div");
			inspectorOverlay.id = "i18n-react-overlay";
			inspectorOverlay.style.cssText = `position:fixed;z-index:2147483646;border:2px solid #8b5cf6;background:rgba(139,92,246,0.08);pointer-events:none;display:none;border-radius:4px;`;
			document.body.appendChild(inspectorOverlay);
		}
		return inspectorOverlay;
	}
	function getInspectorLabel() {
		if (!inspectorLabel || !document.body.contains(inspectorLabel)) {
			inspectorLabel = document.createElement("div");
			inspectorLabel.id = "i18n-react-label";
			inspectorLabel.style.cssText = `position:fixed;z-index:2147483647;background:#8b5cf6;color:#fff;padding:3px 10px;border-radius:4px;font-size:12px;font-family:'SF Mono',Monaco,'Cascadia Code',monospace;pointer-events:none;display:none;white-space:nowrap;box-shadow:0 2px 8px rgba(139,92,246,0.3);max-width:700px;overflow:hidden;text-overflow:ellipsis;`;
			document.body.appendChild(inspectorLabel);
		}
		return inspectorLabel;
	}
	function updateInspectorOverlay(el) {
		const chain = findReactComponentChain(el);
		lastInspectorChain = chain;
		const nearest = chain[chain.length - 1];
		if (!nearest) {
			hideInspectorOverlay();
			return;
		}
		const rect = el.getBoundingClientRect();
		const overlay = getInspectorOverlay();
		overlay.style.top = `${rect.top}px`;
		overlay.style.left = `${rect.left}px`;
		overlay.style.width = `${rect.width}px`;
		overlay.style.height = `${rect.height}px`;
		overlay.style.display = "block";
		const label = getInspectorLabel();
		const shortFile = nearest.fileName.split("/").slice(-2).join("/");
		const editorName = EDITORS.find((e) => e.key === getPreferredEditor())?.label ?? "VS Code";
		const ancestors = chain.slice(0, -1);
		let breadcrumb = "";
		if (ancestors.length > 0) breadcrumb = `<span style="opacity:0.5;">${(ancestors.length > 3 ? ["…", ...ancestors.slice(-3).map((c) => c.componentName)] : ancestors.map((c) => c.componentName)).join(" › ")}</span> <span style="opacity:0.3;">›</span> `;
		label.innerHTML = `${breadcrumb}<span style="font-weight:600;">&lt;${nearest.componentName}&gt;</span> <span style="opacity:0.8;">${shortFile}:${nearest.lineNumber}</span> <span style="opacity:0.6;font-size:10px;">→ ${editorName}</span>`;
		label.title = nearest.fileName;
		label.style.display = "block";
		label.style.left = `${Math.max(0, Math.min(rect.left, window.innerWidth - 600))}px`;
		label.style.top = `${rect.top > 26 ? rect.top - 26 : rect.bottom + 4}px`;
	}
	function hideInspectorOverlay() {
		if (inspectorOverlay) inspectorOverlay.style.display = "none";
		if (inspectorLabel) inspectorLabel.style.display = "none";
		lastHoverEl = null;
	}
	var inspectorPanel = null;
	var selectedChainIdx = 0;
	function formatPropsHtml(obj, depth) {
		if (obj === null) return "<span style=\"color:#d97706;\">null</span>";
		if (obj === void 0) return "<span style=\"color:#d97706;\">undefined</span>";
		const t = typeof obj;
		if (t === "number" || t === "boolean") return `<span style="color:#d97706;">${obj}</span>`;
		if (t === "string") {
			const s = obj;
			if (s.startsWith("[") && s.endsWith("]") || s === "…") return `<span style="color:#888;">${escapeHtml(s)}</span>`;
			return `<span style="color:#16a34a;">"${escapeHtml(s)}"</span>`;
		}
		if (Array.isArray(obj)) {
			if (obj.length === 0) return "<span style=\"color:#888;\">[]</span>";
			return `[${obj.map((v) => formatPropsHtml(v, depth + 1)).join(", ")}]`;
		}
		if (t === "object") {
			const entries = Object.entries(obj);
			if (entries.length === 0) return "<span style=\"color:#888;\">{}</span>";
			if (depth > 3) return "<span style=\"color:#888;\">{…}</span>";
			const pad = "  ".repeat(depth + 1);
			const closePad = "  ".repeat(depth);
			return `{\n${entries.map(([k, v]) => `${pad}<span style="color:#9333ea;">${escapeHtml(k)}</span>: ${formatPropsHtml(v, depth + 1)}`).join(",\n")}\n${closePad}}`;
		}
		return escapeHtml(String(obj));
	}
	function renderInspectorContent(chain) {
		if (!inspectorPanel) return;
		const prevScroll = inspectorPanel.querySelector("#i18n-inspector-scroll")?.scrollTop ?? 0;
		const selected = chain[selectedChainIdx] ?? chain[chain.length - 1];
		const editorName = EDITORS.find((e) => e.key === getPreferredEditor())?.label ?? "VS Code";
		const chainHtml = chain.map((info, i) => {
			const shortFile = info.fileName.split("/").slice(-2).join("/");
			const isSel = i === selectedChainIdx;
			const bg = isSel ? "#f0f0ff" : "transparent";
			const border = isSel ? "1px solid #c7d2fe" : "1px solid transparent";
			return `
      <div class="i18n-chain-item" data-idx="${i}" style="padding:5px 8px;margin:1px 0;margin-left:${i * 12}px;background:${bg};border:${border};border-radius:6px;cursor:pointer;display:flex;align-items:center;gap:6px;transition:background 0.15s;">
        <span style="color:#ccc;font-size:10px;">${i < chain.length - 1 ? "├" : "└"}</span>
        <div style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
          <span style="color:#8b5cf6;font-weight:${isSel ? "700" : "500"};font-size:12px;">&lt;${escapeHtml(info.componentName)}&gt;</span>
          <span style="color:#bbb;font-size:10px;margin-left:4px;">${escapeHtml(shortFile)}:${info.lineNumber}</span>
        </div>
        <div class="i18n-chain-jump" data-idx="${i}" style="flex-shrink:0;color:#2563eb;font-size:10px;cursor:pointer;padding:2px 6px;border-radius:3px;background:#eff6ff;border:1px solid #bfdbfe;" title="在 ${editorName} 中打开">→ IDE</div>
      </div>`;
		}).join("");
		const propsHtml = Object.entries(selected.props).length === 0 ? "<div style=\"color:#999;padding:4px 0;font-size:12px;\">无 Props</div>" : `<pre style="margin:0;padding:8px;background:#fafafa;border:1px solid #eee;border-radius:6px;font-size:11px;line-height:1.5;max-height:240px;overflow:auto;white-space:pre-wrap;word-break:break-all;">${formatPropsHtml(selected.props, 0)}</pre>`;
		let hooksTitle;
		let hooksHtml;
		if (selected.classState !== null && selected.classState !== void 0) {
			hooksTitle = "State";
			hooksHtml = `<pre style="margin:0;padding:8px;background:#fafafa;border:1px solid #eee;border-radius:6px;font-size:11px;line-height:1.5;max-height:200px;overflow:auto;white-space:pre-wrap;word-break:break-all;">${formatPropsHtml(selected.classState, 0)}</pre>`;
		} else if (selected.hooks && selected.hooks.length > 0) {
			hooksTitle = `Hooks (${selected.hooks.length})`;
			const hcMap = {
				useState: ["#2563eb", "#eff6ff"],
				useReducer: ["#2563eb", "#eff6ff"],
				state: ["#2563eb", "#eff6ff"],
				useRef: ["#16a34a", "#f0fdf4"],
				ref: ["#16a34a", "#f0fdf4"],
				useMemo: ["#9333ea", "#faf5ff"],
				useCallback: ["#9333ea", "#faf5ff"],
				memo: ["#9333ea", "#faf5ff"],
				useContext: ["#d97706", "#fffbeb"],
				useTransition: ["#0891b2", "#ecfeff"]
			};
			hooksHtml = `<div style="padding:4px 8px;background:#fafafa;border:1px solid #eee;border-radius:6px;max-height:200px;overflow:auto;">${selected.hooks.map((h) => {
				const [c, bg] = hcMap[h.type] ?? ["#888", "#f5f5f5"];
				return `<div style="padding:3px 0;font-size:11px;display:flex;align-items:flex-start;gap:6px;">
        <span style="color:#bbb;font-size:10px;flex-shrink:0;min-width:14px;text-align:right;">${h.idx}</span>
        <span style="color:${c};background:${bg};padding:0 6px;border-radius:3px;font-size:10px;flex-shrink:0;line-height:18px;">${escapeHtml(h.type)}</span>
        <span style="flex:1;min-width:0;word-break:break-all;line-height:18px;">${formatPropsHtml(h.value, 1)}</span>
      </div>`;
			}).join("")}</div>`;
		} else {
			hooksTitle = "Hooks";
			hooksHtml = "<div style=\"color:#999;padding:4px 0;font-size:12px;\">无 Hooks</div>";
		}
		inspectorPanel.innerHTML = `
    <div style="padding:10px 16px;border-bottom:1px solid #eee;display:flex;align-items:center;flex-shrink:0;">
      <div style="font-weight:600;color:#333;font-size:13px;">组件检查器</div>
      <div style="flex:1;"></div>
      <span style="font-size:10px;color:#bbb;margin-right:8px;">${editorName} · ${chain.length} 层</span>
      <div id="i18n-inspector-close" style="cursor:pointer;color:#999;font-size:18px;padding:0 4px;line-height:1;">&times;</div>
    </div>
    <div id="i18n-inspector-scroll" style="flex:1;overflow-y:auto;">
      <div style="padding:8px 12px 4px;">
        <div style="font-size:11px;color:#888;margin-bottom:4px;">组件链路 <span style="color:#bbb;">· 点击查看 Props / Hooks</span></div>
        ${chainHtml}
      </div>
      <div style="padding:4px 12px 4px;">
        <div style="font-size:11px;color:#888;margin-bottom:4px;">Props · <span style="color:#8b5cf6;">&lt;${escapeHtml(selected.componentName)}&gt;</span></div>
        ${propsHtml}
      </div>
      <div style="padding:4px 12px 12px;">
        <div style="font-size:11px;color:#888;margin-bottom:4px;">${hooksTitle} · <span style="color:#8b5cf6;">&lt;${escapeHtml(selected.componentName)}&gt;</span></div>
        ${hooksHtml}
      </div>
    </div>
  `;
		const scrollEl = inspectorPanel.querySelector("#i18n-inspector-scroll");
		if (scrollEl) scrollEl.scrollTop = prevScroll;
		inspectorPanel.querySelector("#i18n-inspector-close").addEventListener("click", hideInspectorPanel);
		inspectorPanel.querySelectorAll(".i18n-chain-item").forEach((item) => {
			item.addEventListener("click", (e) => {
				if (e.target.closest(".i18n-chain-jump")) return;
				selectedChainIdx = parseInt(item.dataset.idx, 10);
				renderInspectorContent(chain);
			});
		});
		inspectorPanel.querySelectorAll(".i18n-chain-jump").forEach((btn) => {
			btn.addEventListener("click", () => {
				const info = chain[parseInt(btn.dataset.idx, 10)];
				if (info) {
					openInEditor(info);
					showToast(`跳转: <${info.componentName}>`);
				}
			});
		});
	}
	function showInspectorPanel(chain, x, y) {
		selectedChainIdx = chain.length - 1;
		if (!inspectorPanel || !document.body.contains(inspectorPanel)) {
			inspectorPanel = document.createElement("div");
			inspectorPanel.id = "i18n-inspector-panel";
			inspectorPanel.style.cssText = `
      position:fixed;z-index:2147483647;width:440px;max-height:560px;
      background:#fff;border:1px solid #e0e0e0;border-radius:10px;
      box-shadow:0 8px 30px rgba(0,0,0,0.15);font-family:'SF Mono',Monaco,'Cascadia Code',monospace;
      font-size:13px;display:none;overflow:hidden;flex-direction:column;
    `;
			document.body.appendChild(inspectorPanel);
		}
		renderInspectorContent(chain);
		const pw = 440;
		let left = x + 15;
		let top = y + 15;
		if (left + pw > window.innerWidth - 10) left = x - pw - 15;
		if (top + 560 > window.innerHeight - 10) top = Math.max(10, window.innerHeight - 570);
		inspectorPanel.style.left = `${Math.max(10, left)}px`;
		inspectorPanel.style.top = `${Math.max(10, top)}px`;
		inspectorPanel.style.right = "auto";
		inspectorPanel.style.bottom = "auto";
		inspectorPanel.style.display = "flex";
	}
	function hideInspectorPanel() {
		if (inspectorPanel) inspectorPanel.style.display = "none";
	}
	function createTooltip() {
		const el = document.createElement("div");
		el.id = "i18n-finder-tooltip";
		el.style.cssText = `
    position: fixed;
    z-index: 2147483647;
    background: #fff;
    color: #333;
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    padding: 12px 16px;
    font-size: 13px;
    font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
    max-width: 600px;
    max-height: 400px;
    overflow-y: auto;
    box-shadow: 0 4px 20px rgba(0,0,0,0.12);
    display: none;
    line-height: 1.6;
    word-break: break-all;
  `;
		document.body.appendChild(el);
		return el;
	}
	var tooltip = null;
	var clickEnabled = false;
	var escHeld = false;
	function getTooltip() {
		if (!tooltip || !document.body.contains(tooltip)) tooltip = createTooltip();
		return tooltip;
	}
	function showTooltip(x, y, html) {
		const tip = getTooltip();
		tip.innerHTML = html;
		tip.style.display = "block";
		const rect = tip.getBoundingClientRect();
		const left = Math.min(x + 10, window.innerWidth - rect.width - 10);
		const top = y + rect.height + 20 > window.innerHeight ? y - rect.height - 10 : y + 10;
		tip.style.left = `${Math.max(0, left)}px`;
		tip.style.top = `${Math.max(0, top)}px`;
	}
	function hideTooltip() {
		if (tooltip) tooltip.style.display = "none";
	}
	function copyToClipboard(text, toastMsg) {
		navigator.clipboard.writeText(text).catch(() => {
			const ta = document.createElement("textarea");
			ta.value = text;
			ta.style.position = "fixed";
			ta.style.opacity = "0";
			document.body.appendChild(ta);
			ta.select();
			document.execCommand("copy");
			document.body.removeChild(ta);
		});
		showToast(toastMsg ?? `已复制: ${text}`);
	}
	function renderResult(text, result) {
		const escaped = escapeHtml(text.length > 50 ? text.slice(0, 50) + "..." : text);
		const { used, other, fuzzy } = result;
		if (used.length + other.length === 0) return `<div style="color:#e53e3e;">未找到匹配: "${escaped}"</div>`;
		const keyHtml = (k, green) => {
			const src = keySourceMap?.get(k);
			const srcTag = src ? `<span style="color:#999;font-size:10px;margin-left:6px;">[${escapeHtml(src)}]</span>` : "";
			const templateVal = fuzzy && flatMap ? flatMap.get(k) ?? "" : "";
			const templateTag = templateVal ? `<div style="color:#999;font-size:11px;margin-top:2px;">模板: ${escapeHtml(templateVal.length > 60 ? templateVal.slice(0, 60) + "..." : templateVal)}</div>` : "";
			const bg = green ? "#f0fdf4" : "#f5f7fa";
			const bc = green ? "#86efac" : "#e8ecf1";
			const fg = green ? "#16a34a" : "#2563eb";
			const ek = escapeHtml(k);
			return `<div style="padding:4px 8px;margin:4px 0;background:${bg};border:1px solid ${bc};border-radius:4px;cursor:pointer;" class="i18n-key-item" data-key="${ek}">
        <span style="color:${fg};">${ek}</span>${srcTag}${`<span class="i18n-highlight-btn" data-highlight-key="${ek}" style="color:#f59e0b;font-size:10px;margin-left:6px;cursor:pointer;text-decoration:underline;">高亮</span>`}${`<span class="i18n-source-btn" data-source-key="${ek}" style="color:#8b5cf6;font-size:10px;margin-left:6px;cursor:pointer;text-decoration:underline;">源码</span>`}${templateTag}${renderMultiLang(k)}
      </div>`;
		};
		let html = `<div style="margin-bottom:6px;color:#888;font-size:12px;">文本: "${escaped}"</div>`;
		if (fuzzy) html += `<div style="margin-bottom:6px;color:#f59e0b;font-size:11px;">变量模糊匹配</div>`;
		if (used.length > 0) {
			html += `<div style="margin-bottom:4px;color:#16a34a;font-size:12px;font-weight:600;">当前页面使用(点击复制):</div>`;
			html += used.map((k) => keyHtml(k, true)).join("");
		}
		if (other.length > 0) if (used.length > 0) {
			html += `<div style="margin:8px 0 4px;color:#bbb;font-size:11px;cursor:pointer;user-select:none;" class="i18n-toggle-other">其他匹配(${other.length})▸</div>`;
			html += `<div class="i18n-other-keys" style="display:none;">${other.map((k) => keyHtml(k, false)).join("")}</div>`;
		} else {
			html += `<div style="margin-bottom:4px;color:#888;font-size:12px;">找到 ${other.length} 个 key(点击复制):</div>`;
			html += other.map((k) => keyHtml(k, false)).join("");
		}
		return html;
	}
	function getClickedText(e) {
		const el = e.target;
		if (!el || el.id === "i18n-finder-tooltip" || el.closest("#i18n-finder-tooltip")) return "";
		if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
			if (!el.value && el.placeholder) return el.placeholder;
		}
		const selection = window.getSelection();
		if (selection && selection.toString().trim()) return selection.toString();
		const range = document.caretRangeFromPoint(e.clientX, e.clientY);
		if (range?.startContainer.nodeType === Node.TEXT_NODE) return range.startContainer.textContent ?? "";
		if (el.childNodes.length === 1 && el.childNodes[0].nodeType === Node.TEXT_NODE) return el.textContent ?? "";
		return "";
	}
	async function handleClick(e) {
		if (devMode && e.metaKey && !e.altKey) {
			const target = e.target;
			if (!target || target.closest("#i18n-finder-tooltip, #i18n-finder-panel, #i18n-finder-toolbar, #i18n-finder-settings, #i18n-react-overlay, #i18n-react-label, #i18n-inspector-panel")) return;
			e.preventDefault();
			e.stopPropagation();
			e.stopImmediatePropagation();
			const chain = lastInspectorChain.length > 0 ? lastInspectorChain : findReactComponentChain(target);
			if (chain.length > 0) {
				hideInspectorOverlay();
				showInspectorPanel(chain, e.clientX, e.clientY);
			} else showTooltip(e.clientX, e.clientY, "<div style=\"color:#e53e3e;\">未找到 React 组件源码(需要开发模式)</div>");
			return;
		}
		if (devMode && e.altKey) {
			const target = e.target;
			if (!target || target.closest("#i18n-finder-tooltip, #i18n-finder-panel, #i18n-finder-toolbar, #i18n-finder-settings, #i18n-react-overlay, #i18n-react-label, #i18n-inspector-panel")) return;
			e.preventDefault();
			e.stopPropagation();
			e.stopImmediatePropagation();
			const source = findReactSource(target);
			if (source) {
				openInEditor(source);
				const overlay = getInspectorOverlay();
				overlay.style.borderColor = "#22c55e";
				overlay.style.background = "rgba(34, 197, 94, 0.15)";
				setTimeout(hideInspectorOverlay, 300);
			} else showTooltip(e.clientX, e.clientY, "<div style=\"color:#e53e3e;\">未找到 React 组件源码(需要开发模式)</div>");
			return;
		}
		if (!clickEnabled) return;
		const target = e.target;
		if (!target) return;
		if (target.closest("#i18n-finder-tooltip, #i18n-finder-panel, #i18n-finder-toolbar, #i18n-finder-settings")) return;
		if (escHeld) {
			e.preventDefault();
			e.stopPropagation();
			e.stopImmediatePropagation();
		}
		const rawText = getClickedText(e);
		if (!rawText) {
			hideTooltip();
			return;
		}
		const rawExactKey = extractExactKey(rawText);
		const text = stripMarkers(rawText).trim();
		if (!text) {
			hideTooltip();
			return;
		}
		const exactKey = rawExactKey && /^[a-zA-Z][\w.]*$/.test(rawExactKey) ? rawExactKey : null;
		if (exactKey) {
			const src = keySourceMap?.get(exactKey);
			const srcTag = src ? `<span style="color:#999;font-size:10px;margin-left:6px;">[${escapeHtml(src)}]</span>` : "";
			const escaped = escapeHtml(text.length > 50 ? text.slice(0, 50) + "..." : text);
			const hlBtn = `<span class="i18n-highlight-btn" data-highlight-key="${exactKey}" style="color:#f59e0b;font-size:10px;margin-left:6px;cursor:pointer;text-decoration:underline;">高亮</span>`;
			const srcBtn = `<span class="i18n-source-btn" data-source-key="${exactKey}" style="color:#8b5cf6;font-size:10px;margin-left:6px;cursor:pointer;text-decoration:underline;">源码</span>`;
			showTooltip(e.clientX, e.clientY, `
      <div style="margin-bottom:6px;color:#888;font-size:12px;">文本: "${escaped}"</div>
      <div style="margin-bottom:4px;color:#16a34a;font-size:12px;font-weight:600;">精确匹配:</div>
      <div style="margin-bottom:6px;color:#e53e3e;font-size:11px;line-height:1.4;">此 key 可能在网站多处使用,修改前请全局搜索确认影响范围</div>
      <div style="padding:4px 8px;margin:4px 0;background:#f0fdf4;border:1px solid #86efac;border-radius:4px;cursor:pointer;" class="i18n-key-item" data-key="${exactKey}">
        <span style="color:#16a34a;">${exactKey}</span>${srcTag}${hlBtn}${srcBtn}${renderMultiLang(exactKey)}
      </div>
    `);
			return;
		}
		showTooltip(e.clientX, e.clientY, "<div style=\"color:#888;\">加载中...</div>");
		try {
			const { reverse } = await fetchI18nData();
			const result = findKeys(reverse, text);
			showTooltip(e.clientX, e.clientY, renderResult(text, result));
		} catch {
			showTooltip(e.clientX, e.clientY, "<div style=\"color:#e53e3e;\">加载 i18n 数据失败</div>");
		}
	}
	function hideAllPanels() {
		if (searchPanel) searchPanel.style.display = "none";
		if (settingsPanel) settingsPanel.style.display = "none";
		if (helpPanel) helpPanel.style.display = "none";
		hideInspectorPanel();
	}
	function getToolbarPos() {
		const bar = document.getElementById("i18n-finder-toolbar");
		if (bar) return {
			right: parseInt(bar.style.right) || 20,
			bottom: parseInt(bar.style.bottom) || 160
		};
		return {
			right: 20,
			bottom: 160
		};
	}
	function clampPanelInViewport(panel) {
		requestAnimationFrame(() => {
			const rect = panel.getBoundingClientRect();
			if (rect.top < 10) panel.style.bottom = `${window.innerHeight - panel.offsetHeight - 10}px`;
			if (rect.left < 10) panel.style.right = `${window.innerWidth - panel.offsetWidth - 10}px`;
		});
	}
	var searchPanel = null;
	function createSearchPanel() {
		const pos = getToolbarPos();
		const panel = document.createElement("div");
		panel.id = "i18n-finder-panel";
		panel.style.cssText = `
    position: fixed;
    right: ${pos.right + 60}px;
    bottom: ${pos.bottom}px;
    z-index: 2147483647;
    width: 420px;
    max-height: 500px;
    background: #fff;
    border: 1px solid #e0e0e0;
    border-radius: 10px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.12);
    font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
    font-size: 13px;
    display: none;
    overflow: hidden;
  `;
		const tabStyle = (active) => `padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;transition:all 0.2s;` + (active ? "background:#2563eb;color:#fff;" : "background:#f0f2f5;color:#666;");
		panel.innerHTML = `
    <div style="padding:12px 16px;border-bottom:1px solid #eee;">
      <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
        <div style="display:flex;gap:4px;">
          <div id="i18n-tab-key" style="${tabStyle(true)}">按 Key</div>
          <div id="i18n-tab-value" style="${tabStyle(false)}">按翻译值</div>
        </div>
        <div style="flex:1;"></div>
        <div id="i18n-search-close" style="cursor:pointer;color:#999;font-size:18px;padding:0 4px;line-height:1;">&times;</div>
      </div>
      <div style="display:flex;align-items:center;gap:8px;">
        <input id="i18n-search-input" type="text" placeholder="输入 key 搜索..."
          style="flex:1;min-width:0;box-sizing:border-box;background:#f5f7fa;border:1px solid #e0e0e0;border-radius:6px;padding:8px 12px;
          color:#333;font-size:13px;font-family:inherit;outline:none;" />
        <label id="i18n-exact-toggle" style="display:none;align-items:center;gap:4px;cursor:pointer;white-space:nowrap;user-select:none;font-size:12px;color:#999;">
          <div id="i18n-exact-switch" style="width:32px;height:18px;border-radius:9px;background:#ddd;position:relative;transition:background 0.2s;flex-shrink:0;">
            <div style="width:14px;height:14px;border-radius:50%;background:#fff;position:absolute;top:2px;left:2px;transition:left 0.2s;box-shadow:0 1px 2px rgba(0,0,0,0.15);"></div>
          </div>
          精准
        </label>
      </div>
    </div>
    <div id="i18n-search-results" style="max-height:420px;overflow-y:auto;padding:8px 16px;"></div>
  `;
		document.body.appendChild(panel);
		return panel;
	}
	var searchMode = "key";
	var exactMatchMap = {
		key: false,
		value: false
	};
	function getSearchPanel() {
		if (!searchPanel || !document.body.contains(searchPanel)) {
			searchPanel = createSearchPanel();
			const input = searchPanel.querySelector("#i18n-search-input");
			const results = searchPanel.querySelector("#i18n-search-results");
			const close = searchPanel.querySelector("#i18n-search-close");
			const tabKey = searchPanel.querySelector("#i18n-tab-key");
			const tabValue = searchPanel.querySelector("#i18n-tab-value");
			const activeStyle = "padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;transition:all 0.2s;background:#2563eb;color:#fff;";
			const inactiveStyle = "padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;transition:all 0.2s;background:#f0f2f5;color:#666;";
			const exactToggle = searchPanel.querySelector("#i18n-exact-toggle");
			const exactSwitch = searchPanel.querySelector("#i18n-exact-switch");
			const exactDot = exactSwitch.querySelector("div");
			function updateExactUI() {
				exactToggle.style.display = searchMode === "value" ? "flex" : "none";
				const on = exactMatchMap[searchMode];
				exactSwitch.style.background = on ? "#2563eb" : "#ddd";
				exactDot.style.left = on ? "16px" : "2px";
				exactToggle.style.color = on ? "#2563eb" : "#999";
			}
			function setTab(mode) {
				searchMode = mode;
				tabKey.style.cssText = mode === "key" ? activeStyle : inactiveStyle;
				tabValue.style.cssText = mode === "value" ? activeStyle : inactiveStyle;
				input.placeholder = mode === "key" ? "输入 key 搜索..." : "输入翻译值搜索...";
				updateExactUI();
				if (input.value.trim()) doSearch(input.value.trim(), results);
			}
			tabKey.addEventListener("click", () => setTab("key"));
			tabValue.addEventListener("click", () => setTab("value"));
			exactToggle.addEventListener("click", () => {
				exactMatchMap[searchMode] = !exactMatchMap[searchMode];
				updateExactUI();
				if (input.value.trim()) doSearch(input.value.trim(), results);
			});
			let debounceTimer;
			input.addEventListener("input", () => {
				clearTimeout(debounceTimer);
				debounceTimer = setTimeout(() => {
					doSearch(input.value.trim(), results);
				}, 200);
			});
			close.addEventListener("click", () => {
				searchPanel.style.display = "none";
			});
			input.addEventListener("keydown", (e) => {
				e.stopPropagation();
			});
			results.addEventListener("click", (e) => {
				const target = e.target;
				if (target.classList.contains("i18n-highlight-btn")) {
					e.stopPropagation();
					const key = target.dataset.highlightKey;
					if (key) highlightKeyOnPage(key);
					return;
				}
				if (target.classList.contains("i18n-source-btn")) {
					e.stopPropagation();
					const key = target.dataset.sourceKey;
					if (key) jumpToKeySource(key);
					return;
				}
				if (target.classList.contains("i18n-toggle-multilang")) {
					const box = target.nextElementSibling;
					if (box) {
						const show = box.style.display === "none";
						box.style.display = show ? "block" : "none";
						target.textContent = target.textContent.replace(show ? "▸" : "▾", show ? "▾" : "▸");
					}
					return;
				}
				const item = target.closest(".i18n-search-item");
				if (item?.dataset.key) {
					copyToClipboard(item.dataset.key);
					item.style.background = "#e6f9ee";
					item.style.borderColor = "#34d399";
					setTimeout(() => {
						item.style.background = "#f5f7fa";
						item.style.borderColor = "#e8ecf1";
					}, 600);
				}
			});
		}
		return searchPanel;
	}
	function highlightText(text, query) {
		if (!query) return escapeHtml(text);
		const idx = text.toLowerCase().indexOf(query.toLowerCase());
		if (idx < 0) return escapeHtml(text);
		const before = text.slice(0, idx);
		const match = text.slice(idx, idx + query.length);
		const after = text.slice(idx + query.length);
		return `${escapeHtml(before)}<mark style="background:#fef08a;color:inherit;padding:0;border-radius:2px;">${escapeHtml(match)}</mark>${escapeHtml(after)}`;
	}
	async function doSearch(query, container) {
		if (!query) {
			container.innerHTML = "<div style=\"color:#999;padding:8px 0;\">输入关键词开始搜索</div>";
			return;
		}
		try {
			await fetchI18nData();
		} catch {
			container.innerHTML = "<div style=\"color:#e53e3e;\">加载 i18n 数据失败</div>";
			return;
		}
		const matches = [];
		const lowerQuery = query.toLowerCase();
		for (const item of searchIndex) {
			const target = searchMode === "key" ? item.keyLower : item.valueLower;
			if (exactMatchMap[searchMode] ? target === lowerQuery : target.includes(lowerQuery)) {
				matches.push({
					key: item.key,
					value: item.value
				});
				if (matches.length >= 50) break;
			}
		}
		if (matches.length === 0) {
			container.innerHTML = `<div style="color:#e53e3e;padding:8px 0;">未找到匹配 "${escapeHtml(query)}" 的结果</div>`;
			return;
		}
		container.innerHTML = `
    <div style="color:#999;padding:4px 0 8px;font-size:12px;">找到 ${matches.length}${matches.length >= 50 ? "+" : ""} 个结果(点击 key 复制)</div>
    ${matches.map((m) => {
			const ek = escapeHtml(m.key);
			const displayKey = searchMode === "key" ? highlightText(m.key, query) : `<span style="color:#2563eb;">${ek}</span>`;
			const rawVal = m.value.length > 80 ? m.value.slice(0, 80) + "..." : m.value;
			const highlightedVal = searchMode === "value" ? highlightText(rawVal, query) : escapeHtml(rawVal);
			const srcLabel = keySourceMap?.get(m.key);
			const hlBtn = `<span class="i18n-highlight-btn" data-highlight-key="${ek}" style="color:#f59e0b;font-size:10px;margin-left:6px;cursor:pointer;text-decoration:underline;">高亮</span>`;
			const srcBtn = `<span class="i18n-source-btn" data-source-key="${ek}" style="color:#8b5cf6;font-size:10px;margin-left:6px;cursor:pointer;text-decoration:underline;">源码</span>`;
			return `
      <div class="i18n-search-item" data-key="${ek}" style="padding:8px;margin:4px 0;background:#f5f7fa;border:1px solid #e8ecf1;border-radius:6px;cursor:pointer;">
        <div style="font-size:12px;margin-bottom:4px;">${searchMode === "key" ? displayKey : `<span style="color:#2563eb;">${ek}</span>`}${srcLabel ? `<span style="color:#999;font-size:10px;margin-left:6px;">[${escapeHtml(srcLabel)}]</span>` : ""}${isKeyUsedOnPage(m.key) ? `<span style="color:#16a34a;font-size:10px;margin-left:6px;">● 当前页面</span>` : ""}${hlBtn}${srcBtn}</div>
        <div style="color:#555;font-size:13px;">${highlightedVal}</div>
        ${renderMultiLang(m.key)}
      </div>`;
		}).join("")}
  `;
	}
	function toggleSearchPanel() {
		const panel = getSearchPanel();
		const visible = panel.style.display !== "none";
		hideAllPanels();
		if (visible) return;
		else {
			const pos = getToolbarPos();
			panel.style.right = `${pos.right + 60}px`;
			panel.style.bottom = `${pos.bottom}px`;
			panel.style.display = "block";
			clampPanelInViewport(panel);
			panel.querySelector("#i18n-search-input").focus();
			if (!flatMap) fetchI18nData().catch(() => {});
		}
	}
	var settingsPanel = null;
	function renderSourceList() {
		if (sources.length === 0) return "<div style=\"color:#999;padding:8px 0;font-size:12px;\">暂无数据源,等待页面请求自动捕获...</div>";
		return sources.map((s, i) => `
    <div class="i18n-source-item" style="display:flex;align-items:center;gap:8px;padding:8px;margin:4px 0;background:#f5f7fa;border:1px solid #e8ecf1;border-radius:6px;">
      <div style="flex:1;min-width:0;">
        <div style="font-size:12px;font-weight:600;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${s.name}</div>
        <div style="font-size:11px;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:2px;">${s.url}</div>
      </div>
      <div class="i18n-source-del" data-idx="${i}" style="font-size:14px;color:#ccc;cursor:pointer;padding:0 2px;">&times;</div>
    </div>`).join("");
	}
	function createSettingsPanel() {
		const pos = getToolbarPos();
		const panel = document.createElement("div");
		panel.id = "i18n-finder-settings";
		panel.style.cssText = `
    position: fixed;
    right: ${pos.right + 60}px;
    bottom: ${pos.bottom}px;
    z-index: 2147483647;
    width: 420px;
    max-height: 520px;
    background: #fff;
    border: 1px solid #e0e0e0;
    border-radius: 10px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.12);
    font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
    font-size: 13px;
    display: none;
    overflow: hidden;
  `;
		panel.innerHTML = `
    <div style="padding:12px 16px;border-bottom:1px solid #eee;display:flex;align-items:center;">
      <div style="font-weight:600;color:#333;font-size:14px;">数据源管理</div>
      <div style="flex:1;"></div>
      <div style="font-size:11px;color:#999;margin-right:8px;">共 ${sources.length} 个,已合并加载</div>
      <div id="i18n-settings-close" style="cursor:pointer;color:#999;font-size:18px;padding:0 4px;line-height:1;">&times;</div>
    </div>
    <div style="padding:8px 16px;border-bottom:1px solid #eee;display:flex;align-items:center;gap:8px;">
      <div style="font-size:12px;color:#666;white-space:nowrap;">Option+Click 编辑器:</div>
      <div id="i18n-editor-selector" style="display:flex;gap:4px;flex-wrap:wrap;"></div>
    </div>
    <div style="padding:12px 16px;border-bottom:1px solid #eee;">
      <input id="i18n-source-name" type="text" placeholder="名称(如:smart-push prod)"
        style="width:100%;box-sizing:border-box;background:#f5f7fa;border:1px solid #e0e0e0;border-radius:6px;padding:8px 12px;
        color:#333;font-size:13px;font-family:inherit;outline:none;margin-bottom:8px;" />
      <input id="i18n-source-url" type="text" placeholder="JSON URL"
        style="width:100%;box-sizing:border-box;background:#f5f7fa;border:1px solid #e0e0e0;border-radius:6px;padding:8px 12px;
        color:#333;font-size:13px;font-family:inherit;outline:none;margin-bottom:8px;" />
      <div id="i18n-source-add" style="display:inline-block;padding:6px 16px;background:#2563eb;color:#fff;border-radius:6px;cursor:pointer;font-size:12px;">添加</div>
    </div>
    <div style="padding:8px 16px;border-bottom:1px solid #eee;">
      <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
        <div id="i18n-hc-btn" style="display:inline-flex;align-items:center;gap:6px;padding:6px 16px;background:#fff;border:1px solid #ddd;border-radius:6px;cursor:pointer;font-size:12px;color:#666;transition:all 0.2s;">
          <span style="font-size:14px;">&#128270;</span> 硬编码检测
        </div>
        <div id="i18n-missing-btn" style="display:inline-flex;align-items:center;gap:6px;padding:6px 16px;background:#fff;border:1px solid #ddd;border-radius:6px;cursor:pointer;font-size:12px;color:#666;transition:all 0.2s;">
          <span style="font-size:14px;">&#127760;</span> 多语言缺失
        </div>
      </div>
      <span id="i18n-hc-hint" style="font-size:11px;color:#999;display:block;margin-top:6px;">硬编码检测需精确匹配 · 多语言缺失需同项目 ≥2 种语言</span>
    </div>
    <div id="i18n-source-list" style="max-height:300px;overflow-y:auto;padding:8px 16px;"></div>
  `;
		document.body.appendChild(panel);
		return panel;
	}
	function refreshSettingsPanel() {
		if (!settingsPanel) return;
		const list = settingsPanel.querySelector("#i18n-source-list");
		list.innerHTML = renderSourceList();
		bindSourceEvents(list);
	}
	function bindSourceEvents(list) {
		list.querySelectorAll(".i18n-source-del").forEach((btn) => {
			btn.addEventListener("click", () => {
				const idx = parseInt(btn.dataset.idx, 10);
				sources.splice(idx, 1);
				saveSources(sources);
				clearCache();
				refreshSettingsPanel();
			});
		});
	}
	function getSettingsPanel() {
		if (!settingsPanel || !document.body.contains(settingsPanel)) {
			settingsPanel = createSettingsPanel();
			const close = settingsPanel.querySelector("#i18n-settings-close");
			const addBtn = settingsPanel.querySelector("#i18n-source-add");
			const nameInput = settingsPanel.querySelector("#i18n-source-name");
			const urlInput = settingsPanel.querySelector("#i18n-source-url");
			close.addEventListener("click", () => {
				settingsPanel.style.display = "none";
			});
			const editorSel = settingsPanel.querySelector("#i18n-editor-selector");
			function renderEditorButtons() {
				const current = getPreferredEditor();
				editorSel.innerHTML = EDITORS.map((e) => `<div class="i18n-editor-opt" data-editor="${e.key}" style="padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px;transition:all 0.2s;${e.key === current ? "background:#8b5cf6;color:#fff;" : "background:#f0f2f5;color:#666;"}">${e.label}</div>`).join("");
			}
			renderEditorButtons();
			editorSel.addEventListener("click", (e) => {
				const btn = e.target.closest(".i18n-editor-opt");
				if (!btn?.dataset.editor) return;
				setPreferredEditor(btn.dataset.editor);
				renderEditorButtons();
			});
			addBtn.addEventListener("click", () => {
				const name = nameInput.value.trim();
				const url = urlInput.value.trim();
				if (!name || !url) return;
				if (sources.some((s) => s.url === url)) return;
				sources.push({
					name,
					url
				});
				saveSources(sources);
				clearCache();
				nameInput.value = "";
				urlInput.value = "";
				refreshSettingsPanel();
			});
			[nameInput, urlInput].forEach((el) => {
				el.addEventListener("keydown", (e) => e.stopPropagation());
			});
			hcButton = settingsPanel.querySelector("#i18n-hc-btn");
			const hcHint = settingsPanel.querySelector("#i18n-hc-hint");
			hcButton.addEventListener("click", () => {
				detectHardcoded();
				hcButton.style.background = hardcodeActive ? "#ef4444" : "#fff";
				hcButton.style.color = hardcodeActive ? "#fff" : "#666";
				hcButton.style.borderColor = hardcodeActive ? "#ef4444" : "#ddd";
				hcHint.textContent = hardcodeActive ? "再次点击「硬编码检测」清除标记" : "硬编码检测需精确匹配 · 多语言缺失需同项目 ≥2 种语言";
			});
			settingsPanel.querySelector("#i18n-missing-btn").addEventListener("click", () => {
				detectMissingTranslations();
			});
			refreshSettingsPanel();
		}
		return settingsPanel;
	}
	function toggleSettingsPanel() {
		const panel = getSettingsPanel();
		const visible = panel.style.display !== "none";
		hideAllPanels();
		if (visible) return;
		else {
			const pos = getToolbarPos();
			panel.style.right = `${pos.right + 60}px`;
			panel.style.bottom = `${pos.bottom}px`;
			panel.style.display = "block";
			clampPanelInViewport(panel);
			refreshSettingsPanel();
		}
	}
	function createToolbar() {
		const saved = _GM_getValue("i18n_toolbar_pos", "");
		let posRight = 20, posBottom = 160;
		if (saved) try {
			const p = JSON.parse(saved);
			posRight = p.right;
			posBottom = p.bottom;
		} catch {}
		const bar = document.createElement("div");
		bar.id = "i18n-finder-toolbar";
		bar.style.cssText = `
    position: fixed;
    right: ${posRight}px;
    bottom: ${posBottom}px;
    z-index: 2147483647;
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: center;
    transition: gap 0.3s;
  `;
		const handle = document.createElement("div");
		handle.style.cssText = `
    width: 48px;
    height: 18px;
    border-radius: 9px;
    background: #e5e7eb;
    cursor: grab;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    user-select: none;
    transition: background 0.2s;
  `;
		handle.innerHTML = `
    <div style="display:flex;flex-direction:column;gap:3px;">
      <div style="display:flex;gap:4px;"><div style="width:3px;height:3px;border-radius:50%;background:#999;"></div><div style="width:3px;height:3px;border-radius:50%;background:#999;"></div></div>
      <div style="display:flex;gap:4px;"><div style="width:3px;height:3px;border-radius:50%;background:#999;"></div><div style="width:3px;height:3px;border-radius:50%;background:#999;"></div></div>
    </div>`;
		handle.addEventListener("mouseenter", () => {
			if (!dragging) handle.style.background = "#d1d5db";
		});
		handle.addEventListener("mouseleave", () => {
			if (!dragging) handle.style.background = "#e5e7eb";
		});
		let dragging = false;
		let dragMoved = false;
		let startX = 0, startY = 0, origRight = posRight, origBottom = posBottom;
		handle.addEventListener("mousedown", (e) => {
			hideAllPanels();
			dragging = true;
			dragMoved = false;
			startX = e.clientX;
			startY = e.clientY;
			origRight = parseInt(bar.style.right) || 0;
			origBottom = parseInt(bar.style.bottom) || 0;
			handle.style.cursor = "grabbing";
			e.preventDefault();
		});
		document.addEventListener("mousemove", (e) => {
			if (!dragging) return;
			dragMoved = true;
			const dx = e.clientX - startX;
			const dy = e.clientY - startY;
			const newRight = Math.max(0, Math.min(origRight - dx, window.innerWidth - bar.offsetWidth - 10));
			const newBottom = Math.max(0, Math.min(origBottom - dy, window.innerHeight - bar.offsetHeight - 10));
			bar.style.right = `${newRight}px`;
			bar.style.bottom = `${newBottom}px`;
		});
		let collapsed = _GM_getValue("i18n_toolbar_collapsed", false);
		const buttons = [];
		function applyCollapsed() {
			for (const btn of buttons) {
				btn.style.maxHeight = collapsed ? "0" : "48px";
				btn.style.opacity = collapsed ? "0" : "1";
				btn.style.margin = collapsed ? "0" : "";
				btn.style.pointerEvents = collapsed ? "none" : "";
				btn.style.overflow = "hidden";
			}
			bar.style.gap = collapsed ? "0" : "8px";
			handle.title = collapsed ? "点击展开工具栏" : "点击收起 · 拖拽移动";
		}
		document.addEventListener("mouseup", () => {
			if (!dragging) return;
			dragging = false;
			handle.style.cursor = "grab";
			if (dragMoved) _GM_setValue("i18n_toolbar_pos", JSON.stringify({
				right: parseInt(bar.style.right) || 0,
				bottom: parseInt(bar.style.bottom) || 0
			}));
			else {
				collapsed = !collapsed;
				_GM_setValue("i18n_toolbar_collapsed", collapsed);
				if (collapsed) hideAllPanels();
				applyCollapsed();
				if (!collapsed) setTimeout(() => {
					const maxB = Math.max(0, window.innerHeight - bar.offsetHeight - 10);
					if ((parseInt(bar.style.bottom) || 0) > maxB) {
						bar.style.bottom = `${maxB}px`;
						_GM_setValue("i18n_toolbar_pos", JSON.stringify({
							right: parseInt(bar.style.right) || 0,
							bottom: maxB
						}));
					}
				}, 320);
			}
		});
		window.addEventListener("resize", () => {
			const maxR = Math.max(0, window.innerWidth - bar.offsetWidth - 10);
			const maxB = Math.max(0, window.innerHeight - bar.offsetHeight - 10);
			bar.style.right = `${Math.max(0, Math.min(parseInt(bar.style.right) || 0, maxR))}px`;
			bar.style.bottom = `${Math.max(0, Math.min(parseInt(bar.style.bottom) || 0, maxB))}px`;
		});
		const btnStyle = `
    width: 48px;
    height: 48px;
    max-height: 48px;
    border-radius: 50%;
    background: #fff;
    color: #888;
    border: 1px solid #ddd;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    font-weight: bold;
    font-family: 'SF Mono', Monaco, monospace;
    user-select: none;
    transition: all 0.2s, max-height 0.3s, opacity 0.3s, margin 0.3s;
    box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  `;
		const settingsBtn = document.createElement("div");
		settingsBtn.innerHTML = "&#9881;";
		settingsBtn.style.cssText = btnStyle;
		settingsBtn.style.fontSize = "18px";
		settingsBtn.addEventListener("click", toggleSettingsPanel);
		const searchBtn = document.createElement("div");
		searchBtn.textContent = "Key";
		searchBtn.style.cssText = btnStyle;
		searchBtn.addEventListener("click", toggleSearchPanel);
		const clickBtn = document.createElement("div");
		clickBtn.textContent = "i18n";
		clickBtn.style.cssText = btnStyle;
		clickBtn.style.position = "relative";
		const exactDot = document.createElement("div");
		exactDot.style.cssText = `position:absolute;top:5px;right:5px;width:7px;height:7px;border-radius:50%;background:#cbd5e1;box-shadow:0 0 0 1.5px #fff;transition:background 0.3s;`;
		clickBtn.appendChild(exactDot);
		const applyExactStatus = (ready) => {
			exactDot.style.background = ready ? "#22c55e" : "#cbd5e1";
			clickBtn.title = ready ? "i18n 精确匹配已就绪" : "精确匹配不可用,当前仅反查匹配";
		};
		let exactPolls = 0;
		const exactTimer = setInterval(() => {
			const ready = isExactMatchReady();
			if (ready || ++exactPolls >= 12) {
				applyExactStatus(ready);
				clearInterval(exactTimer);
			}
		}, 1200);
		clickBtn.addEventListener("click", () => {
			clickEnabled = !clickEnabled;
			clickBtn.style.background = clickEnabled ? "#2563eb" : "#fff";
			clickBtn.style.color = clickEnabled ? "#fff" : "#888";
			clickBtn.style.borderColor = clickEnabled ? "#2563eb" : "#ddd";
			if (!clickEnabled) hideTooltip();
			if (clickEnabled && !i18nMap) fetchI18nData().catch(() => {});
			if (clickEnabled) applyExactStatus(isExactMatchReady());
		});
		const helpBtn = document.createElement("div");
		helpBtn.textContent = "?";
		helpBtn.style.cssText = btnStyle;
		helpBtn.style.fontSize = "18px";
		helpBtn.addEventListener("click", toggleHelpPanel);
		buttons.push(helpBtn, settingsBtn, searchBtn, clickBtn);
		bar.appendChild(handle);
		bar.appendChild(helpBtn);
		bar.appendChild(settingsBtn);
		bar.appendChild(searchBtn);
		bar.appendChild(clickBtn);
		document.body.appendChild(bar);
		applyCollapsed();
	}
	var helpPanel = null;
	function toggleHelpPanel() {
		if (!helpPanel || !document.body.contains(helpPanel)) {
			helpPanel = document.createElement("div");
			helpPanel.id = "i18n-finder-help";
			const pos = getToolbarPos();
			helpPanel.style.cssText = `
      position:fixed;right:${pos.right + 60}px;bottom:${pos.bottom}px;z-index:2147483647;width:380px;
      background:#fff;border:1px solid #e0e0e0;border-radius:10px;max-height:80vh;
      box-shadow:0 4px 20px rgba(0,0,0,0.12);font-size:13px;display:none;overflow:hidden;
      font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;flex-direction:column;
    `;
			helpPanel.innerHTML = `
      <div style="padding:12px 16px;border-bottom:1px solid #eee;display:flex;align-items:center;">
        <div style="font-weight:600;color:#333;font-size:14px;">i18n Key Finder 使用说明</div>
        <div style="flex:1;"></div>
        <div id="i18n-help-close" style="cursor:pointer;color:#999;font-size:18px;padding:0 4px;line-height:1;">&times;</div>
      </div>
      <div style="padding:12px 16px;line-height:1.8;color:#444;overflow-y:auto;flex:1;">
        <div style="font-weight:600;color:#2563eb;margin-bottom:4px;">工具栏按钮</div>
        <div><b style="color:#333;">?</b> — 本说明</div>
        <div><b style="color:#333;">&#9881;</b> — 数据源管理(自动捕获 i18n JSON 请求)</div>
        <div><b style="color:#333;">Key</b> — 搜索面板(按 key 或翻译值搜索)</div>
        <div><b style="color:#333;">i18n</b> — 点击取词模式(开启后点击页面文字查 key)</div>
        <div style="font-weight:600;color:#2563eb;margin:8px 0 4px;">点击取词</div>
        <div>1. 点击 <b style="color:#2563eb;">i18n</b> 按钮开启(变蓝)</div>
        <div>2. 点击页面上的任意翻译文字</div>
        <div>3. 弹出匹配结果,点击 key 即可复制</div>
        <div>4. 如果文字是链接,按住 <kbd style="background:#f0f0f0;padding:1px 6px;border:1px solid #ddd;border-radius:3px;font-size:12px;">Esc</kbd> 再点击可阻止跳转</div>
        <div style="font-weight:600;color:#2563eb;margin:8px 0 4px;">匹配模式</div>
        <div><span style="color:#16a34a;font-weight:600;">精确匹配</span> — 需要在项目的 i18n 初始化文件中添加一行代码将实例暴露到 window:</div>
        <div style="margin:4px 0;padding:6px 8px;background:#1e1e1e;color:#d4d4d4;border-radius:4px;font-family:monospace;font-size:11px;line-height:1.5;white-space:pre;overflow-x:auto;"><span style="color:#c586c0;">import</span> i18n <span style="color:#c586c0;">from</span> <span style="color:#ce9178;">'i18next'</span>;\n;(<span style="color:#4ec9b0;">window</span> <span style="color:#c586c0;">as</span> <span style="color:#4ec9b0;">any</span>).__i18n__ = i18n\n<span style="color:#c586c0;">export default</span> i18n;</div>
        <div style="color:#888;font-size:11px;">添加后重新部署,脚本会自动 hook t() 精确定位每段文本的 key</div>
        <div style="color:#888;font-size:11px;"><b style="color:#16a34a;">i18n</b> 按钮右上角圆点:<span style="color:#16a34a;">绿色</span>=精确匹配就绪,<span style="color:#94a3b8;">灰色</span>=仅反查匹配</div>
        <div><span style="color:#2563eb;font-weight:600;">反查匹配</span> — 通过翻译值反查所有可能的 key,按页面使用情况过滤排序</div>
        <div><span style="color:#f59e0b;font-weight:600;">变量模糊匹配</span> — 当精确和反查均无结果时,自动对含变量占位符(<code style="background:#f0f0f0;padding:1px 4px;border-radius:3px;font-size:11px;">\${...}</code>、<code style="background:#f0f0f0;padding:1px 4px;border-radius:3px;font-size:11px;">{{...}}</code>)的翻译值去除变量后做子串匹配</div>
        <div style="font-weight:600;color:#f59e0b;margin:8px 0 4px;">页面高亮 / 源码跳转</div>
        <div>查找到 key 后,结果项提供两个按钮:</div>
        <div><span style="color:#f59e0b;text-decoration:underline;">高亮</span> — 页面上所有使用该 key 的元素加黄框并滚动到第一处(右上角徽章可清除)</div>
        <div><span style="color:#8b5cf6;text-decoration:underline;">源码</span> — 跳转到调用该 key 的 React 组件源码位置</div>
        <div style="color:#888;font-size:11px;">两者均需精确匹配模式;源码跳转还需 React 开发模式</div>
        <div style="font-weight:600;color:#8b5cf6;margin:8px 0 4px;">多语言对照</div>
        <div>当存在多个数据源时,key 下方显示 <span style="color:#8b5cf6;">多语言对照</span> 展开项</div>
        <div style="color:#888;font-size:11px;">点击可查看该 key 在各数据源中的翻译值</div>
        <div style="font-weight:600;color:#ef4444;margin:8px 0 4px;">硬编码检测</div>
        <div>在 <b style="color:#333;">&#9881;</b> 数据源管理中点击「硬编码检测」按钮</div>
        <div>自动扫描页面,用红色虚线框标出未经 t() 翻译的文本</div>
        <div style="color:#888;font-size:11px;">需要精确匹配模式(window.__i18n__ 已暴露)</div>
        <div style="font-weight:600;color:#8b5cf6;margin:8px 0 4px;">多语言缺失检测</div>
        <div>在 <b style="color:#333;">&#9881;</b> 数据源管理中点击「多语言缺失」按钮</div>
        <div>对比同一项目的多个语言数据源,列出某些语言缺失(或为空)的 key</div>
        <div style="color:#888;font-size:11px;">需加载同项目 ≥2 种语言文件,可一键复制全部缺失记录</div>
        <div style="font-weight:600;color:#8b5cf6;margin:8px 0 4px;">React 源码跳转</div>
        <div>按住 <kbd style="background:#f0f0f0;padding:1px 6px;border:1px solid #ddd;border-radius:3px;font-size:12px;">Option</kbd>(Alt) 悬停查看组件面包屑链路 + 源码位置</div>
        <div><kbd style="background:#f0f0f0;padding:1px 6px;border:1px solid #ddd;border-radius:3px;font-size:12px;">Option</kbd> + 点击 → 直接跳转编辑器</div>
        <div style="color:#888;font-size:11px;">支持 VS Code、Cursor、Zed、WebStorm、GoLand、IDEA,可在设置中切换</div>
        <div style="color:#888;font-size:11px;">打开时会激活并置顶编辑器窗口;JetBrains 系首次需在浏览器允许打开协议</div>
        <div style="font-weight:600;color:#8b5cf6;margin:8px 0 4px;">组件检查器</div>
        <div>按住 <kbd style="background:#f0f0f0;padding:1px 6px;border:1px solid #ddd;border-radius:3px;font-size:12px;">⌘ Command</kbd> 悬停查看组件链路(同 Option)</div>
        <div><kbd style="background:#f0f0f0;padding:1px 6px;border:1px solid #ddd;border-radius:3px;font-size:12px;">⌘ Command</kbd> + 点击 → 打开组件检查器面板</div>
        <div style="margin:4px 0 2px;font-size:12px;">面板包含三个区域:</div>
        <div style="padding-left:12px;font-size:12px;"><b style="color:#333;">组件链路</b> — 从根组件到当前元素的完整组件树,每层可独立跳转 IDE</div>
        <div style="padding-left:12px;font-size:12px;"><b style="color:#333;">Props</b> — 当前选中组件的属性,语法高亮显示</div>
        <div style="padding-left:12px;font-size:12px;"><b style="color:#333;">Hooks / State</b> — 函数组件显示 Hooks 列表(useState、useRef、useMemo 等),类组件显示 State 对象</div>
        <div style="color:#888;font-size:11px;">点击链路中的不同组件可切换查看其 Props 和 Hooks</div>
        <div style="color:#888;font-size:11px;">以上功能均需 React 开发模式(_debugSource / _debugHookTypes 信息)</div>
        <div style="font-weight:600;color:#e53e3e;margin:8px 0 4px;">注意</div>
        <div style="color:#e53e3e;">同一个 key 可能在网站多处使用,修改翻译前请全局搜索确认影响范围</div>
      </div>
    `;
			document.body.appendChild(helpPanel);
			helpPanel.querySelector("#i18n-help-close").addEventListener("click", () => {
				helpPanel.style.display = "none";
			});
		}
		const visible = helpPanel.style.display !== "none";
		hideAllPanels();
		if (!visible) {
			const pos = getToolbarPos();
			helpPanel.style.right = `${pos.right + 60}px`;
			helpPanel.style.bottom = `${pos.bottom}px`;
			helpPanel.style.display = "flex";
			clampPanelInViewport(helpPanel);
		}
	}
	function initUI() {
		detectInlineScriptBlocked();
		createToolbar();
		tryHookI18n();
		document.addEventListener("click", (e) => {
			const t = e.target;
			if (t && !t.closest("#i18n-finder-panel, #i18n-finder-settings, #i18n-finder-help, #i18n-finder-toolbar, #i18n-hardcode-panel, #i18n-inspector-panel")) hideAllPanels();
			handleClick(e);
		}, true);
		getTooltip().addEventListener("click", (e) => {
			const target = e.target;
			if (target.classList.contains("i18n-toggle-other")) {
				const box = target.nextElementSibling;
				if (box) {
					const show = box.style.display === "none";
					box.style.display = show ? "block" : "none";
					target.textContent = target.textContent.replace(show ? "▸" : "▾", show ? "▾" : "▸");
				}
				return;
			}
			if (target.classList.contains("i18n-highlight-btn")) {
				e.stopPropagation();
				const key = target.dataset.highlightKey;
				if (key) highlightKeyOnPage(key);
				return;
			}
			if (target.classList.contains("i18n-source-btn")) {
				e.stopPropagation();
				const key = target.dataset.sourceKey;
				if (key) jumpToKeySource(key);
				return;
			}
			if (target.classList.contains("i18n-toggle-multilang")) {
				const box = target.nextElementSibling;
				if (box) {
					const show = box.style.display === "none";
					box.style.display = show ? "block" : "none";
					target.textContent = target.textContent.replace(show ? "▸" : "▾", show ? "▾" : "▸");
				}
				return;
			}
			const item = target.closest(".i18n-key-item");
			if (item?.dataset.key) {
				copyToClipboard(item.dataset.key);
				item.style.background = "#e6f9ee";
				item.style.borderColor = "#34d399";
				setTimeout(() => {
					const inOther = item.closest(".i18n-other-keys");
					item.style.background = inOther ? "#f5f7fa" : "#f0fdf4";
					item.style.borderColor = inOther ? "#e8ecf1" : "#86efac";
				}, 600);
			}
		});
		document.addEventListener("keydown", (e) => {
			if (e.key === "Escape") escHeld = true;
			if (!devMode) return;
			if (e.key === "Alt") {
				altHeld = true;
				document.documentElement.classList.add("i18n-inspector-active");
			}
			if (e.key === "Meta") {
				if (metaTimer) clearTimeout(metaTimer);
				metaTimer = setTimeout(() => {
					metaHeld = true;
					document.documentElement.classList.add("i18n-inspector-active");
				}, 200);
			}
			if (e.metaKey && e.key !== "Meta") {
				if (metaTimer) {
					clearTimeout(metaTimer);
					metaTimer = null;
				}
				metaHeld = false;
				document.documentElement.classList.remove("i18n-inspector-active");
				hideInspectorOverlay();
			}
		}, true);
		document.addEventListener("keyup", (e) => {
			if (e.key === "Escape") escHeld = false;
			if (!devMode) return;
			if (e.key === "Alt") {
				altHeld = false;
				if (!metaHeld) {
					document.documentElement.classList.remove("i18n-inspector-active");
					hideInspectorOverlay();
				}
			}
			if (e.key === "Meta") {
				if (metaTimer) {
					clearTimeout(metaTimer);
					metaTimer = null;
				}
				metaHeld = false;
				if (!altHeld) {
					document.documentElement.classList.remove("i18n-inspector-active");
					hideInspectorOverlay();
				}
			}
		}, true);
		if (devMode) {
			let inspectorRafId = 0;
			document.addEventListener("mousemove", (e) => {
				if (!altHeld && !metaHeld) return;
				cancelAnimationFrame(inspectorRafId);
				inspectorRafId = requestAnimationFrame(() => {
					const el = document.elementFromPoint(e.clientX, e.clientY);
					if (!el || el === lastHoverEl) return;
					if (el.closest("#i18n-react-overlay, #i18n-react-label, #i18n-finder-toolbar, #i18n-inspector-panel")) return;
					lastHoverEl = el;
					updateInspectorOverlay(el);
				});
			}, true);
			window.addEventListener("blur", () => {
				if (altHeld || metaHeld) {
					altHeld = false;
					metaHeld = false;
					document.documentElement.classList.remove("i18n-inspector-active");
					hideInspectorOverlay();
				}
			});
			const inspectorStyle = document.createElement("style");
			inspectorStyle.textContent = ".i18n-inspector-active, .i18n-inspector-active * { cursor: crosshair !important; }";
			document.head.appendChild(inspectorStyle);
		}
	}
	setupRequestCapture();
	if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", initUI);
	else initUI();
})();