i18n Key Finder

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Advertisement:

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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();
})();