diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9ad68d7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*bak**
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5b68ae7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,54 @@
+# 30 分钟,使用 cursor 开发的一个极简低配版"沉浸式翻译"chrome 插件
+
+## 说明
+
+- 这个插件是使用 cursor 的 composer 功能开发的,我本人是完全不懂 chrome 插件开发的。
+- 翻译功能是基于调用在线服务大模型 API 实现的,所以需要使用者有可用的平台地址和 ak。
+ - 因为是调用大模型API进行翻译,可能完全翻译完会比较慢。
+ - 可以试用下我写在代码里的是硅基流动中免费的模型 `Qwen/Qwen2.5-7B-Instruct`。
+ - 当然,如果对硅基流动平台感兴趣,还能用下我的邀请码注册,那就更好了:
+ - https://cloud.siliconflow.cn/i/tRIcST68
+- 整页翻译的内容会保存在缓存中,1小时内同一个网站不会重复调用API进行翻译。
+ - 如果需要强制重新翻译,可以点击对应的清除缓存按钮后,重新翻译。
+
+## 先看看效果
+
+- 安装插件后,点击插件图标,右上角会显示出功能弹窗:
+
+
+
+- 点击“设置”按钮,配置大模型平台地址、模型名、和 AK,**记得首次使用要保存设置才生效**。
+
+
+
+- 整页翻译:对比翻译的效果
+
+
+
+- 整页翻译:替换翻译的效果
+
+
+
+- 划词翻译:对只需要翻译网页中部分文本,在选中文本(划词)后,会出现一个小的“翻译”按钮,点击之后就会弹窗显示翻译结果,目标语言在右上角的配置面板中指定。
+
+
+
+- 如果是阅读 pdf 文件,或者也是一般网页,右键选择“AI 翻译助手-翻译选中文本”,会弹出独立翻译窗口。
+
+
+
+- 这个独立窗口可以当成个简单的翻译工具,复制需要翻译的内容,选择目标语言,然后随意翻译即可。
+
+
+
+## 其他补充
+
+- 翻译效果和大模型质量相关
+- 网页内容过大,可能翻译比较慢,只会翻译点击翻译时已经加载的内容
+- 嵌入式(对比翻译)效果不一定好看
+
+## 安装使用
+
+下载这个项目,解压后,打开 chrome 或 edge浏览器,进入 `chrome://extensions/` 或`edge://extensions/`页面,点击“加载已解压的扩展程序”,选择解压后的文件夹即可。
+
+首次使用一定点击“设置”按钮或者插件图标右键选“选项”,去配置 API 地址、AK 和模型名称,点击“保存设置”。
\ No newline at end of file
diff --git a/background/background.js b/background/background.js
new file mode 100644
index 0000000..6d28cf4
--- /dev/null
+++ b/background/background.js
@@ -0,0 +1,194 @@
+// 获取特定标签页的设置
+const getTabSettings = async (tabId) => {
+ return new Promise((resolve) => {
+ chrome.storage.local.get(
+ {
+ [`targetLang_${tabId}`]: 'zh' // 获取特定标签页的目标语言
+ },
+ async (tabItems) => {
+ // 获取全局API设置
+ const globalSettings = await chrome.storage.sync.get({
+ apiEndpoint: '', // 移除默认值
+ apiKey: '',
+ model: ''
+ });
+
+ resolve({
+ ...globalSettings,
+ targetLang: tabItems[`targetLang_${tabId}`]
+ });
+ }
+ );
+ });
+};
+
+// 调用API进行翻译
+const translateWithAPI = async (text, targetLang, apiKey, model, apiEndpoint) => {
+ // 检查必要的API设置
+ if (!apiEndpoint || !apiKey || !model) {
+ throw new Error('请先在设置页面配置API信息');
+ }
+
+ const response = await fetch(apiEndpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${apiKey}`
+ },
+ body: JSON.stringify({
+ model: model,
+ messages: [
+ {
+ role: "system",
+ content: `你是一个翻译助手。请将用户输入的文本翻译成${targetLang},只返回翻译结果,不需要解释。`
+ },
+ {
+ role: "user",
+ content: text
+ }
+ ],
+ temperature: 0.3
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('翻译请求失败');
+ }
+
+ const data = await response.json();
+ return data.choices[0].message.content.trim();
+};
+
+// 监听来自content script的消息
+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ if (request.action === 'translate') {
+ (async () => {
+ try {
+ let settings;
+ if (request.isPopupWindow) {
+ // 如果是独立窗口的请求,获取全局API设置
+ settings = await chrome.storage.sync.get({
+ apiEndpoint: '',
+ apiKey: '',
+ model: ''
+ });
+ settings.targetLang = request.targetLang;
+ } else {
+ // 否则使用标签页的设置
+ settings = await getTabSettings(request.tabId);
+ }
+
+ const translation = await translateWithAPI(
+ request.text,
+ settings.targetLang,
+ settings.apiKey,
+ settings.model,
+ settings.apiEndpoint
+ );
+ sendResponse({ translation });
+ } catch (error) {
+ sendResponse({ error: error.message });
+ }
+ })();
+ return true;
+ } else if (request.action === 'getCurrentTabId') {
+ sendResponse({ tabId: sender.tab?.id });
+ return false;
+ }
+});
+
+// 存储面板状态(使用对象而不是 Map)
+let panelStates = {};
+
+// 添加新的消息处理
+chrome.action.onClicked.addListener(async (tab) => {
+ const tabId = tab.id;
+
+ try {
+ // 检查当前标签页是否已有面板
+ const [result] = await chrome.scripting.executeScript({
+ target: { tabId: tabId },
+ func: togglePanel,
+ args: [tabId, !!panelStates[tabId]] // 转换为布尔值
+ });
+
+ // 更新面板状态
+ panelStates[tabId] = result.result;
+ } catch (error) {
+ console.error('面板切换失败:', error);
+ }
+});
+
+// 监听标签页关闭事件
+chrome.tabs.onRemoved.addListener((tabId) => {
+ delete panelStates[tabId];
+});
+
+// 面板切换函数
+function togglePanel(tabId, isVisible) {
+ let panel = document.querySelector(`.translator-panel[data-tab-id="${tabId}"]`);
+
+ if (panel) {
+ panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
+ return panel.style.display === 'block';
+ } else {
+ const iframe = document.createElement('iframe');
+ iframe.className = 'translator-panel';
+ iframe.setAttribute('data-tab-id', tabId);
+ iframe.src = chrome.runtime.getURL('content/panel.html');
+ iframe.style.display = 'block';
+ document.body.appendChild(iframe);
+ return true;
+ }
+}
+
+let translateWindow = null;
+
+// 创建右键菜单
+chrome.runtime.onInstalled.addListener(() => {
+ chrome.contextMenus.create({
+ id: 'translateSelection',
+ title: 'AI翻译助手 - 翻译选中文本',
+ contexts: ['selection']
+ });
+});
+
+// 处理右键菜单点击
+chrome.contextMenus.onClicked.addListener(async (info, tab) => {
+ if (info.menuItemId === 'translateSelection') {
+ const selectedText = info.selectionText;
+
+ if (!translateWindow) {
+ // 创建新窗口
+ const window = await chrome.windows.create({
+ url: chrome.runtime.getURL('translate/translate.html'),
+ type: 'popup',
+ width: 800,
+ height: 600
+ });
+ translateWindow = window.id;
+
+ // 等待窗口加载完成
+ setTimeout(() => {
+ chrome.runtime.sendMessage({
+ action: 'updateText',
+ text: selectedText
+ });
+ }, 1000);
+ } else {
+ // 更新已存在的窗口
+ chrome.windows.update(translateWindow, { focused: true });
+ chrome.runtime.sendMessage({
+ action: 'updateText',
+ text: selectedText
+ });
+ }
+ }
+});
+
+// 监听窗口关闭
+chrome.windows.onRemoved.addListener((windowId) => {
+ if (windowId === translateWindow) {
+ translateWindow = null;
+ }
+});
\ No newline at end of file
diff --git a/content/cache-manager.js b/content/cache-manager.js
new file mode 100644
index 0000000..c5b6916
--- /dev/null
+++ b/content/cache-manager.js
@@ -0,0 +1,169 @@
+// 缓存管理器
+const CacheManager = {
+ // 缓存有效期(1小时)
+ CACHE_DURATION: 3600000,
+
+ // 初始化缓存
+ async init() {
+ const result = await chrome.storage.local.get('translationCache');
+ if (!result.translationCache) {
+ await chrome.storage.local.set({ translationCache: {} });
+ } else {
+ // 清理过期缓存
+ await this.cleanExpiredCache();
+ }
+ },
+
+ // 清理过期缓存
+ async cleanExpiredCache() {
+ const result = await chrome.storage.local.get('translationCache');
+ const translationCache = result.translationCache;
+ let hasExpired = false;
+
+ if (translationCache) {
+ const now = Date.now();
+ for (const key of Object.keys(translationCache)) {
+ if (now - translationCache[key].timestamp > this.CACHE_DURATION) {
+ delete translationCache[key];
+ hasExpired = true;
+ }
+ }
+
+ if (hasExpired) {
+ await chrome.storage.local.set({ translationCache });
+ }
+ }
+ },
+
+ // 生成缓存键 - 简化缓存键的生成方式
+ generateCacheKey(url, text, targetLang, type) {
+ // 使用文本内容的哈希作为缓存键的一部分
+ const textHash = this.hashString(text);
+ return `${url}_${targetLang}_${type}_${textHash}`;
+ },
+
+ // 字符串哈希函数
+ hashString(str) {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // Convert to 32-bit integer
+ }
+ return Math.abs(hash).toString(16);
+ },
+
+ // 获取缓存
+ async getCache(url, text, targetLang, type) {
+ const cacheKey = this.generateCacheKey(url, text, targetLang, type);
+ const result = await chrome.storage.local.get('translationCache');
+ const cache = result.translationCache[cacheKey];
+
+ if (!cache) {
+ console.log(`[缓存未命中] ${text.slice(0, 30)}...`);
+ return null;
+ }
+
+ // 检查缓存是否过期
+ if (Date.now() - cache.timestamp > this.CACHE_DURATION) {
+ console.log(`[缓存过期] ${text.slice(0, 30)}...`);
+ await this.removeCache(cacheKey);
+ return null;
+ }
+
+ console.log(`[缓存命中] ${text.slice(0, 30)}...`);
+ return cache;
+ },
+
+ // 设置缓存
+ async setCache(url, text, translation, targetLang, type) {
+ const cacheKey = this.generateCacheKey(url, text, targetLang, type);
+ const result = await chrome.storage.local.get('translationCache');
+ const translationCache = result.translationCache || {};
+
+ translationCache[cacheKey] = {
+ url,
+ text,
+ translation,
+ targetLang,
+ type,
+ timestamp: Date.now()
+ };
+
+ await chrome.storage.local.set({ translationCache });
+ console.log(`[缓存已保存] ${text.slice(0, 30)}...`);
+ },
+
+ // 移除缓存
+ async removeCache(cacheKey) {
+ const result = await chrome.storage.local.get('translationCache');
+ const translationCache = result.translationCache;
+ if (translationCache && translationCache[cacheKey]) {
+ delete translationCache[cacheKey];
+ await chrome.storage.local.set({ translationCache });
+ }
+ },
+
+ // 清除特定网页和目标语言的缓存
+ async clearTypeCache(url, targetLang, type) {
+ const result = await chrome.storage.local.get('translationCache');
+ const translationCache = result.translationCache;
+ let hasCache = false;
+
+ if (translationCache) {
+ const prefix = `${url}_${targetLang}_${type}_`;
+ for (const key of Object.keys(translationCache)) {
+ if (key.startsWith(prefix)) {
+ delete translationCache[key];
+ hasCache = true;
+ }
+ }
+ if (hasCache) {
+ await chrome.storage.local.set({ translationCache });
+ console.log(`[缓存已清除] ${url} ${type}`);
+ }
+ }
+
+ return { success: hasCache, empty: !hasCache };
+ },
+
+ // 检查特定网页和目标语言是否有缓存
+ async hasCache(url, targetLang, type) {
+ const result = await chrome.storage.local.get('translationCache');
+ const translationCache = result.translationCache;
+
+ if (!translationCache) return false;
+
+ const prefix = `${url}_${targetLang}_${type}_`;
+ const now = Date.now();
+
+ return Object.keys(translationCache).some(key =>
+ key.startsWith(prefix) &&
+ now - translationCache[key].timestamp <= this.CACHE_DURATION
+ );
+ },
+
+ // 获取页面的所有缓存翻译
+ async getPageCache(url, targetLang, type) {
+ const result = await chrome.storage.local.get('translationCache');
+ const translationCache = result.translationCache;
+ const pageCache = {};
+
+ if (translationCache) {
+ const prefix = `${url}_${targetLang}_${type}_`;
+ const now = Date.now();
+
+ for (const [key, cache] of Object.entries(translationCache)) {
+ if (key.startsWith(prefix) &&
+ now - cache.timestamp <= this.CACHE_DURATION) {
+ pageCache[cache.text] = cache.translation;
+ }
+ }
+ }
+
+ return pageCache;
+ }
+};
+
+// 初始化缓存管理器
+CacheManager.init();
\ No newline at end of file
diff --git a/content/content.css b/content/content.css
new file mode 100644
index 0000000..477419c
--- /dev/null
+++ b/content/content.css
@@ -0,0 +1,115 @@
+.ai-translator-popup {
+ position: absolute;
+ z-index: 10000;
+ background: rgba(255, 255, 255, 0.95);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ padding: 12px;
+ max-width: 300px;
+ display: none;
+ backdrop-filter: blur(5px);
+ color: #333;
+}
+
+/* 深色模式支持 */
+@media (prefers-color-scheme: dark) {
+ .ai-translator-popup {
+ background: rgba(40, 40, 40, 0.95);
+ border-color: rgba(255, 255, 255, 0.1);
+ color: #fff;
+ }
+}
+
+.ai-translator-content {
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.ai-translator-loading {
+ color: inherit;
+ opacity: 0.7;
+ font-style: italic;
+}
+
+.ai-translation {
+ color: #666;
+ margin-left: 4px;
+}
+
+.translate-button {
+ position: absolute;
+ z-index: 10000;
+ background: rgba(76, 175, 80, 0.9);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 6px 12px;
+ cursor: pointer;
+ font-size: 13px;
+ backdrop-filter: blur(5px);
+ transition: background-color 0.2s;
+}
+
+.translate-button:hover {
+ background: rgba(69, 160, 73, 0.95);
+}
+
+.translator-panel {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ width: 256px;
+ min-height: 300px;
+ max-height: 80vh;
+ height: fit-content;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ z-index: 10000;
+ display: none;
+}
+
+.panel-container {
+ padding: 15px;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+.ai-translation-container {
+ margin-top: 8px;
+ margin-bottom: 8px;
+ padding-left: 12px;
+ border-left: 2px solid var(--theme-color, #4CAF50);
+ opacity: 0.9;
+ color: inherit;
+}
+
+.language-info {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.arrow {
+ color: #666;
+ margin: 0 8px;
+}
+
+.ai-translation-inline {
+ color: inherit;
+ opacity: 0.8;
+ margin-left: 4px;
+ font-size: 0.95em;
+}
+
+/* 只有当内容超过最大高度时才显示滚动条 */
+@media (max-height: 500px) {
+ .translator-panel {
+ overflow-y: auto;
+ }
+}
+
\ No newline at end of file
diff --git a/content/content.js b/content/content.js
new file mode 100644
index 0000000..1085d10
--- /dev/null
+++ b/content/content.js
@@ -0,0 +1,866 @@
+let isTranslated = false;
+let translatedNodes = new Map();
+
+// 存储检测到的语言
+let detectedLanguage = null;
+
+// 检测文本语言
+const detectLanguage = async (text) => {
+ try {
+ const hasChineseChars = /[\u4e00-\u9fa5]/.test(text);
+ const hasJapaneseChars = /[\u3040-\u30ff]/.test(text);
+ const hasKoreanChars = /[\uac00-\ud7af]/.test(text);
+
+ if (hasChineseChars) return 'zh';
+ if (hasJapaneseChars) return 'ja';
+ if (hasKoreanChars) return 'ko';
+ return 'en';
+ } catch (error) {
+ console.error('语言检测失败:', error);
+ return 'unknown';
+ }
+};
+
+// 判断是否是文本内容节点
+const isTextContentNode = (node) => {
+ // 检查节点是否有父元素
+ if (!node.parentElement) {
+ return false;
+ }
+
+ const textTags = ['p', 'article', 'section', 'li', 'dd', 'div'];
+ const parent = node.parentElement;
+
+ // 检查标签名
+ if (textTags.includes(parent.tagName.toLowerCase())) {
+ // 检查是否是列表项
+ if (parent.tagName.toLowerCase() === 'li') return true;
+
+ // 检查是否是主要文本内容
+ const text = parent.textContent.trim();
+ return text.length > 20 || text.includes('。') || text.includes('.');
+ }
+
+ return false;
+};
+
+// 创建翻译容器
+const createTranslationContainer = (originalNode) => {
+ // 检查节点是否有父元素
+ if (!originalNode.parentElement) {
+ return null;
+ }
+
+ const container = document.createElement('div');
+ container.className = 'ai-translation-container';
+
+ // 复制原始节点的样式
+ const originalStyle = window.getComputedStyle(originalNode.parentElement);
+ container.style.fontFamily = originalStyle.fontFamily;
+ container.style.fontSize = originalStyle.fontSize;
+ container.style.lineHeight = originalStyle.lineHeight;
+ container.style.color = originalStyle.color;
+
+ return container;
+};
+
+// 创建内联翻译元素
+const createInlineTranslation = (originalNode) => {
+ const span = document.createElement('span');
+ span.className = 'ai-translation-inline';
+ return span;
+};
+
+// 获取当前标签页ID
+const getCurrentTabId = async () => {
+ try {
+ // 如果是在content script中运行
+ if (chrome.runtime?.id) {
+ const response = await chrome.runtime.sendMessage({ action: 'getCurrentTabId' });
+ return response.tabId;
+ }
+ // 如果是在独立窗口中运行
+ const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
+ return tabs[0].id;
+ } catch (error) {
+ console.error('获取标签页ID失败:', error);
+ return null;
+ }
+};
+
+// 检测页面主要语言
+const detectPageLanguage = async () => {
+ const mainContent = document.body.innerText.slice(0, 1000);
+ detectedLanguage = await detectLanguage(mainContent);
+
+ // 发送检测结果
+ const tabId = await getCurrentTabId();
+ if (tabId) {
+ chrome.runtime.sendMessage({
+ action: 'updateSourceLanguage',
+ language: detectedLanguage,
+ tabId: tabId
+ });
+ }
+};
+
+// 监听面板创建事件
+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ if (request.action === 'panelCreated') {
+ // 如果已经检测到语言,立即发送
+ if (detectedLanguage) {
+ chrome.runtime.sendMessage({
+ action: 'updateSourceLanguage',
+ language: detectedLanguage,
+ tabId: request.tabId
+ });
+ sendResponse({}); // 立即发送响应
+ } else {
+ // 如果还没有检测结果,立即进行检测
+ detectPageLanguage().then(() => {
+ sendResponse({}); // 检测完成后发送响应
+ });
+ return true; // 表示将异步发送响应
+ }
+ } else if (request.action === 'translatePage') {
+ translatePage().then(() => {
+ sendResponse({}); // 翻译完成后发送响应
+ });
+ return true; // 表示将异步发送响应
+ } else if (request.action === 'replaceTranslate') {
+ replaceTranslate().then(() => {
+ sendResponse({}); // 替换翻译完成后发送响应
+ });
+ return true; // 表示将异步发送响应
+ } else if (request.action === 'restoreOriginal') {
+ restoreOriginal();
+ sendResponse({}); // 立即发送响应
+ } else if (request.action === 'detectChineseVariant') {
+ const isTraditional = detectChineseVariant(document.body.innerText);
+ sendResponse({ isTraditional });
+ }
+});
+
+// 在页面加载完成后立即开始检测
+document.addEventListener('DOMContentLoaded', () => {
+ detectPageLanguage();
+});
+
+// 在页面内容变化时重新检测
+const observer = new MutationObserver(() => {
+ if (document.body) {
+ detectPageLanguage();
+ observer.disconnect();
+ }
+});
+
+observer.observe(document.documentElement, {
+ childList: true,
+ subtree: true
+});
+
+// 创建翻译按钮
+const createTranslateButton = () => {
+ const button = document.createElement('button');
+ button.className = 'translate-button';
+ button.textContent = '翻译';
+ return button;
+};
+
+// 创建翻译弹窗
+const createTranslationPopup = () => {
+ const popup = document.createElement('div');
+ popup.className = 'ai-translator-popup';
+ popup.innerHTML = `
+
+ `;
+ document.body.appendChild(popup);
+ return popup;
+};
+
+// 获取选中的文本
+const getSelectedText = () => {
+ const selection = window.getSelection();
+ return selection.toString().trim();
+};
+
+// 定位弹窗
+const positionPopup = (element, rect) => {
+ const top = rect.bottom + window.scrollY;
+ const left = rect.left + window.scrollX;
+
+ element.style.top = `${top}px`;
+ element.style.left = `${left}px`;
+};
+
+// 添加翻译缓存和控制变量
+let translationCache = {
+ compare: new Map(),
+ replace: new Map()
+};
+let isTranslating = {
+ compare: false,
+ replace: false
+};
+let shouldStopTranslation = {
+ compare: false,
+ replace: false
+};
+
+// 缓存翻译结果
+const cacheTranslation = (type, text, translation) => {
+ const cacheKey = `${type}_${text}`;
+ const cacheData = {
+ translation,
+ timestamp: Date.now()
+ };
+ translationCache[type].set(cacheKey, cacheData);
+};
+
+// 获取缓存的翻译
+const getCachedTranslation = (type, text) => {
+ const cacheKey = `${type}_${text}`;
+ const cached = translationCache[type].get(cacheKey);
+ if (!cached) return null;
+
+ // 检查缓存是否过期(1小时)
+ if (Date.now() - cached.timestamp > 3600000) {
+ translationCache[type].delete(cacheKey);
+ return null;
+ }
+
+ return cached.translation;
+};
+
+// 修改清除缓存函数
+const clearCache = async (type) => {
+ if (isTranslating[type]) {
+ return { success: false, message: '翻译进行中,无法清除缓存' };
+ }
+
+ const url = window.location.href;
+ const targetLang = await getCurrentTargetLang();
+ return await CacheManager.clearTypeCache(url, targetLang, type);
+};
+
+// 修改检查缓存状态函数
+const checkCache = async () => {
+ const url = window.location.href;
+ const targetLang = await getCurrentTargetLang();
+ return {
+ hasCompareCache: await CacheManager.hasCache(url, targetLang, 'compare'),
+ hasReplaceCache: await CacheManager.hasCache(url, targetLang, 'replace')
+ };
+};
+
+// 监听消息
+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ if (request.action === 'stopTranslation') {
+ const type = request.translationType;
+ if (type) {
+ stopTranslation(type);
+ }
+ sendResponse({});
+ } else if (request.action === 'clearCache') {
+ // 修改为异步处理
+ (async () => {
+ const result = await clearCache(request.cacheType);
+ sendResponse(result);
+ })();
+ return true; // 保持消息通道打开以等待异步响应
+ } else if (request.action === 'checkCache') {
+ // 修改为异步处理
+ (async () => {
+ const result = await checkCache();
+ sendResponse(result);
+ })();
+ return true; // 保持消息通道打开以等待异步响应
+ }
+ // ... 其他消息处理保持不变 ...
+});
+
+// 获取节点的唯一标识
+const getNodeIdentifier = (node) => {
+ // 获取节点在其父元素中的索引
+ const getNodeIndex = (node) => {
+ let index = 0;
+ let sibling = node;
+ while (sibling.previousSibling) {
+ if (sibling.previousSibling.nodeType === Node.TEXT_NODE) {
+ index++;
+ }
+ sibling = sibling.previousSibling;
+ }
+ return index;
+ };
+
+ // 安全地获取类名
+ const getClassNames = (element) => {
+ if (!element) return '';
+ if (typeof element.className === 'string') {
+ return element.className;
+ }
+ if (element.className && element.className.baseVal) {
+ // 处理 SVG 元素的类名
+ return element.className.baseVal;
+ }
+ // 如果都不是,返回空字符串
+ return '';
+ };
+
+ // 构建节点路径
+ const buildNodePath = (node) => {
+ const path = [];
+ let current = node;
+
+ while (current && current.parentElement) {
+ const parent = current.parentElement;
+ const index = getNodeIndex(current);
+ const tag = parent.tagName.toLowerCase();
+ const className = getClassNames(parent);
+ const classes = className ? `.${className.split(' ').join('.')}` : '';
+ const id = parent.id ? `#${parent.id}` : '';
+ path.unshift(`${tag}${id}${classes}:nth-text(${index})`);
+ current = parent;
+ }
+
+ return path.join(' > ');
+ };
+
+ try {
+ // 返回节点的唯一标识
+ return `${buildNodePath(node)}_${node.textContent.trim().length}`;
+ } catch (error) {
+ console.error('生成节点标识失败:', error);
+ // 返回一个基于时间戳的备用标识
+ return `node_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+ }
+};
+
+// 修改翻译函数,添加节点标识
+const translateText = async (text, type = 'compare', nodeId = '') => {
+ const url = window.location.href;
+ const targetLang = await getCurrentTargetLang();
+
+ // 生成缓存键时包含节点标识
+ const cacheKey = `${url}_${targetLang}_${type}_${nodeId}`;
+
+ // 检查缓存
+ const cached = await CacheManager.getCache(url, text, targetLang, type, nodeId);
+ if (cached) {
+ console.log(`[缓存] 使用缓存的翻译结果: ${text.slice(0, 30)}...`);
+ return cached.translation;
+ }
+
+ // 如果没有缓存,调用API翻译
+ console.log(`[API] 调用API翻译: ${text.slice(0, 30)}...`);
+
+ const response = await chrome.runtime.sendMessage({
+ action: 'translate',
+ text: text,
+ tabId: await getCurrentTabId()
+ });
+
+ // 缓存翻译结果时包含节点标识
+ if (response.translation) {
+ await CacheManager.setCache(url, text, response.translation, targetLang, type, nodeId);
+ }
+
+ return response.translation;
+};
+
+// 获取当前目标语言
+const getCurrentTargetLang = async () => {
+ const tabId = await getCurrentTabId();
+ const result = await chrome.storage.local.get(`targetLang_${tabId}`);
+ return result[`targetLang_${tabId}`] || 'zh-CN';
+};
+
+// 检查是否是PDF查看器
+const isPDFViewer = () => {
+ return window.location.pathname.endsWith('.pdf') ||
+ document.querySelector('embed[type="application/pdf"]') !== null;
+};
+
+// 处理选中文本事件
+let translateButton = null;
+let popup = null;
+
+document.addEventListener('mouseup', async () => {
+ const selectedText = getSelectedText();
+
+ if (!selectedText) {
+ if (translateButton) {
+ translateButton.style.display = 'none';
+ }
+ if (popup) {
+ popup.style.display = 'none';
+ }
+ return;
+ }
+
+ // 特殊处理PDF查看器
+ if (isPDFViewer()) {
+ if (!popup) {
+ popup = createTranslationPopup();
+ }
+
+ const selection = window.getSelection();
+ const range = selection.getRangeAt(0);
+ const rect = range.getBoundingClientRect();
+
+ popup.style.display = 'block';
+ popup.querySelector('.ai-translator-text').textContent = '';
+ popup.querySelector('.ai-translator-loading').style.display = 'block';
+ positionPopup(popup, rect);
+
+ try {
+ const translation = await translateText(selectedText);
+ popup.querySelector('.ai-translator-loading').style.display = 'none';
+ popup.querySelector('.ai-translator-text').textContent = translation;
+ } catch (error) {
+ popup.querySelector('.ai-translator-loading').style.display = 'none';
+ popup.querySelector('.ai-translator-text').textContent = '翻译失败,请重试';
+ }
+ return;
+ }
+
+ // 普通网页的理逻辑保持不变
+ if (!translateButton) {
+ translateButton = createTranslateButton();
+ document.body.appendChild(translateButton);
+ }
+
+ const selection = window.getSelection();
+ const range = selection.getRangeAt(0);
+ const rect = range.getBoundingClientRect();
+
+ translateButton.style.display = 'block';
+ positionPopup(translateButton, rect);
+
+ translateButton.onclick = async () => {
+ if (!popup) {
+ popup = createTranslationPopup();
+ }
+
+ popup.style.display = 'block';
+ popup.querySelector('.ai-translator-text').textContent = '';
+ popup.querySelector('.ai-translator-loading').style.display = 'block';
+ positionPopup(popup, rect);
+
+ try {
+ const translation = await translateText(selectedText);
+ popup.querySelector('.ai-translator-loading').style.display = 'none';
+ popup.querySelector('.ai-translator-text').textContent = translation;
+ } catch (error) {
+ popup.querySelector('.ai-translator-loading').style.display = 'none';
+ popup.querySelector('.ai-translator-text').textContent = '翻译失败,请重试';
+ }
+ };
+});
+
+// 更新翻译进度
+const updateProgress = (progress) => {
+ chrome.runtime.sendMessage({
+ action: 'updateProgress',
+ progress: progress
+ });
+};
+
+// 判断是否是有效的文本内容
+const isValidText = (text, node) => {
+ // 移除空白字符
+ text = text.trim();
+ if (!text || text.length < 2) return false;
+
+ // 检查父元素是否是需要排除的元素
+ const invalidParents = ['script', 'style', 'noscript', 'code', 'pre'];
+ let current = node.parentElement;
+ while (current) {
+ if (invalidParents.includes(current.tagName.toLowerCase())) {
+ return false;
+ }
+ current = current.parentElement;
+ }
+
+ // 检查是否是代码或脚本内容
+ const codePatterns = [
+ /^[{(\[`].*[})\]`]$/, // 代码块
+ /^(function|class|const|let|var)\s/, // 代码声明
+ /^[a-zA-Z_$][a-zA-Z0-9_$]*\.[a-zA-Z_$]/, // 对象属性访问
+ /^self\./, // self引用
+ /^window\./, // window引用
+ /^document\./, // document引用
+ /^[0-9a-f]{32,}$/, // 哈希值
+ /^data:/, // Data URLs
+ /^https?:\/\//, // URLs
+ /^[0-9.]+$/, // 纯数字
+ /^#[0-9a-f]{3,8}$/, // 颜色代码
+ ];
+
+ if (codePatterns.some(pattern => pattern.test(text))) {
+ return false;
+ }
+
+ // 检查是否包含有效的文本特征
+ const validFeatures = [
+ // 标点符号
+ /[,。!?;:""''()【】《》、]/,
+ // 中文字符
+ /[\u4e00-\u9fa5]/,
+ // 英文单词(包括常见缩写)
+ /[A-Za-z]{2,}/,
+ // 日文假名
+ /[\u3040-\u309F\u30A0-\u30FF]/,
+ // 韩文
+ /[\uAC00-\uD7AF]/,
+ // 阿拉伯文
+ /[\u0600-\u06FF]/,
+ // 泰文
+ /[\u0E00-\u0E7F]/,
+ // 越南文
+ /[àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ]/,
+ // 常见标点
+ /[.!?;:]/
+ ];
+
+ // 如果包含任何有效特征,就认为是有效文本
+ if (validFeatures.some(pattern => pattern.test(text))) {
+ return true;
+ }
+
+ // 检查父元素标签
+ const validTags = [
+ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+ 'article', 'section', 'div', 'span', 'a',
+ 'li', 'td', 'th', 'dd', 'dt', 'label',
+ 'blockquote', 'cite', 'figcaption', 'time',
+ 'mark', 'strong', 'em', 'b', 'i', 'small',
+ 'caption', 'title', 'button'
+ ];
+
+ const parentTag = node.parentElement?.tagName.toLowerCase();
+ if (parentTag && validTags.includes(parentTag)) {
+ return true;
+ }
+
+ // 检查节点的角色
+ const validRoles = [
+ 'heading', 'article', 'paragraph', 'text',
+ 'contentinfo', 'button', 'link', 'label',
+ 'listitem', 'cell', 'gridcell', 'tooltip',
+ 'alert', 'status', 'log', 'note'
+ ];
+
+ const role = node.parentElement?.getAttribute('role');
+ if (role && validRoles.includes(role)) {
+ return true;
+ }
+
+ // 检查是否是列表项或表格单元格的内容
+ const isListOrTableContent = node.parentElement?.closest('li, td, th, dt, dd');
+ if (isListOrTableContent) {
+ return true;
+ }
+
+ // 检查是否是链接文本
+ const isLinkText = node.parentElement?.closest('a');
+ if (isLinkText) {
+ return true;
+ }
+
+ // 检查是否是按钮文本
+ const isButtonText = node.parentElement?.closest('button');
+ if (isButtonText) {
+ return true;
+ }
+
+ // 检查是否是表单标签文本
+ const isLabelText = node.parentElement?.closest('label');
+ if (isLabelText) {
+ return true;
+ }
+
+ return false;
+};
+
+// 获取节点的XPath
+const getXPath = (node) => {
+ if (!node) return '';
+ if (node.nodeType !== Node.ELEMENT_NODE) {
+ return getXPath(node.parentNode);
+ }
+
+ let nodeCount = 0;
+ let hasFollowingSibling = false;
+ const siblings = node.parentNode.childNodes;
+
+ for (let i = 0; i < siblings.length; i++) {
+ const sibling = siblings[i];
+ if (sibling === node) {
+ hasFollowingSibling = true;
+ } else if (hasFollowingSibling && sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === node.tagName) {
+ nodeCount++;
+ }
+ }
+
+ const prefix = getXPath(node.parentNode);
+ const tagName = node.tagName.toLowerCase();
+ const position = nodeCount > 0 ? `[${nodeCount + 1}]` : '';
+
+ return `${prefix}/${tagName}${position}`;
+};
+
+// 修改文本节点遍历逻辑
+const getTextNodes = (root) => {
+ const nodes = [];
+ const walker = document.createTreeWalker(
+ root,
+ NodeFilter.SHOW_TEXT,
+ {
+ acceptNode: (node) => {
+ // 基本检查
+ if (!node || !node.parentElement) {
+ return NodeFilter.FILTER_REJECT;
+ }
+
+ // 检查是否是已翻译的内容
+ if (node.parentElement.classList.contains('ai-translation-container') ||
+ node.parentElement.classList.contains('ai-translation-inline')) {
+ return NodeFilter.FILTER_REJECT;
+ }
+
+ // 检查文本内容
+ const text = node.textContent.trim();
+ if (!text || !isValidText(text, node)) {
+ return NodeFilter.FILTER_REJECT;
+ }
+
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ }
+ );
+
+ while (walker.nextNode()) {
+ nodes.push(walker.currentNode);
+ }
+
+ return nodes;
+};
+
+// 修改整页翻译功能
+const translatePage = async () => {
+ isTranslating.compare = true;
+ shouldStopTranslation.compare = false;
+
+ if (translatedNodes.size > 0) {
+ restoreOriginal();
+ }
+
+ const url = window.location.href;
+ const targetLang = await getCurrentTargetLang();
+ const pageCache = await CacheManager.getPageCache(url, targetLang, 'compare');
+
+ const textNodes = getTextNodes(document.body);
+ const totalNodes = textNodes.length;
+ let processedNodes = 0;
+
+ // 批量处理大小
+ const BATCH_SIZE = 10;
+ // 批处理延迟(毫秒)
+ const BATCH_DELAY = 50;
+
+ // 分批处理函数
+ const processBatch = async (nodes, startIndex) => {
+ const batch = nodes.slice(startIndex, startIndex + BATCH_SIZE);
+
+ for (const node of batch) {
+ if (shouldStopTranslation.compare) {
+ isTranslating.compare = false;
+ chrome.runtime.sendMessage({
+ action: 'translationStopped',
+ translationType: 'compare'
+ });
+ return;
+ }
+
+ const text = node.textContent.trim();
+ if (!text) continue;
+
+ try {
+ // 首先检查缓存
+ const nodeId = getNodeIdentifier(node);
+ let translation;
+
+ // 如果有缓存,立即使用
+ if (pageCache[text]) {
+ translation = pageCache[text];
+ } else {
+ // 否则调用API翻译
+ translation = await translateText(text, 'compare', nodeId);
+ }
+
+ if (isTextContentNode(node)) {
+ const container = createTranslationContainer(node);
+ if (container) {
+ container.textContent = translation;
+ container.setAttribute('data-node-id', nodeId);
+
+ const parent = node.parentElement;
+ if (parent && parent.parentNode) {
+ if (parent.nextSibling) {
+ parent.parentNode.insertBefore(container, parent.nextSibling);
+ } else {
+ parent.parentNode.appendChild(container);
+ }
+ translatedNodes.set(node, container);
+ }
+ }
+ } else {
+ const span = createInlineTranslation(node);
+ if (span && node.parentNode) {
+ span.textContent = ` (${translation})`;
+ span.setAttribute('data-node-id', nodeId);
+ if (node.nextSibling) {
+ node.parentNode.insertBefore(span, node.nextSibling);
+ } else {
+ node.parentNode.appendChild(span);
+ }
+ translatedNodes.set(node, span);
+ }
+ }
+
+ processedNodes++;
+ updateProgress(Math.round((processedNodes / totalNodes) * 100));
+ } catch (error) {
+ console.error('翻译失败:', error);
+ }
+ }
+
+ // 如果还有未处理的节点,延迟处理下一批
+ if (startIndex + BATCH_SIZE < nodes.length && !shouldStopTranslation.compare) {
+ await new Promise(resolve => setTimeout(resolve, BATCH_DELAY));
+ await processBatch(nodes, startIndex + BATCH_SIZE);
+ } else if (!shouldStopTranslation.compare) {
+ // 所有节点处理完成
+ isTranslating.compare = false;
+ chrome.runtime.sendMessage({
+ action: 'translationComplete',
+ translationType: 'compare'
+ });
+ }
+ };
+
+ // 开始批量处理
+ await processBatch(textNodes, 0);
+};
+
+// 修改替换翻译功能,使用类似的批处理逻辑
+const replaceTranslate = async () => {
+ isTranslating.replace = true;
+ shouldStopTranslation.replace = false;
+
+ if (translatedNodes.size > 0) {
+ restoreOriginal();
+ }
+
+ const url = window.location.href;
+ const targetLang = await getCurrentTargetLang();
+ const pageCache = await CacheManager.getPageCache(url, targetLang, 'replace');
+
+ const textNodes = getTextNodes(document.body);
+ const totalNodes = textNodes.length;
+ let processedNodes = 0;
+
+ const BATCH_SIZE = 10;
+ const BATCH_DELAY = 50;
+
+ const processBatch = async (nodes, startIndex) => {
+ const batch = nodes.slice(startIndex, startIndex + BATCH_SIZE);
+
+ for (const node of batch) {
+ if (shouldStopTranslation.replace) {
+ isTranslating.replace = false;
+ chrome.runtime.sendMessage({
+ action: 'translationStopped',
+ translationType: 'replace'
+ });
+ return;
+ }
+
+ const text = node.textContent.trim();
+ if (!text) continue;
+
+ try {
+ const nodeId = getNodeIdentifier(node);
+ let translation;
+
+ // 优先使用缓存
+ if (pageCache[text]) {
+ translation = pageCache[text];
+ } else {
+ translation = await translateText(text, 'replace', nodeId);
+ }
+
+ // 保存原始文本
+ translatedNodes.set(node, node.textContent);
+ // 替换文本内容
+ node.textContent = translation;
+
+ processedNodes++;
+ updateProgress(Math.round((processedNodes / totalNodes) * 100));
+ } catch (error) {
+ console.error('翻译失败:', error);
+ }
+ }
+
+ if (startIndex + BATCH_SIZE < nodes.length && !shouldStopTranslation.replace) {
+ await new Promise(resolve => setTimeout(resolve, BATCH_DELAY));
+ await processBatch(nodes, startIndex + BATCH_SIZE);
+ } else if (!shouldStopTranslation.replace) {
+ isTranslating.replace = false;
+ chrome.runtime.sendMessage({
+ action: 'translationComplete',
+ translationType: 'replace'
+ });
+ }
+ };
+
+ await processBatch(textNodes, 0);
+};
+
+// 修改恢复原文函数
+const restoreOriginal = () => {
+ for (const [node, originalText] of translatedNodes) {
+ // 检查节点是否仍然存在于文档中
+ if (node && node.parentElement && document.contains(node)) {
+ if (originalText instanceof Node) {
+ // 如果是注入式翻译,检查节点是否仍然存在于文档中
+ if (originalText.parentElement && document.contains(originalText)) {
+ originalText.remove();
+ }
+ } else {
+ // 如果是替换式翻译,恢复原文
+ node.textContent = originalText;
+ }
+ }
+ }
+ translatedNodes.clear();
+ isTranslated = false;
+ chrome.runtime.sendMessage({ action: 'restorationComplete' });
+};
+
+// 检测中文变体
+const detectChineseVariant = (text) => {
+ const traditionalChars = /[錒-鎛]/;
+ return traditionalChars.test(text);
+};
+
+// 修改停止翻译函数
+const stopTranslation = (type) => {
+ shouldStopTranslation[type] = true;
+};
+
\ No newline at end of file
diff --git a/content/languages.js b/content/languages.js
new file mode 100644
index 0000000..c947334
--- /dev/null
+++ b/content/languages.js
@@ -0,0 +1,222 @@
+// 语言代码和名称的映射
+export const LANGUAGES = {
+ // 常用语言
+ 'common': {
+ 'zh-CN': { name: '简体中文', native: '简体中文' },
+ 'en': { name: '英语', native: 'English' },
+ },
+ // 其他语言
+ 'others': {
+ 'zh-TW': { name: '繁体中文', native: '繁體中文' },
+ 'ja': { name: '日语', native: '日本語' },
+ 'ko': { name: '韩语', native: '한국어' },
+ 'es': { name: '西班牙语', native: 'Español' },
+ 'fr': { name: '法语', native: 'Français' },
+ 'de': { name: '德语', native: 'Deutsch' },
+ 'ru': { name: '俄语', native: 'Русский' },
+ 'it': { name: '意大利语', native: 'Italiano' },
+ 'pt': { name: '葡萄牙语', native: 'Português' },
+ 'vi': { name: '越南语', native: 'Tiếng Việt' },
+ 'th': { name: '泰语', native: 'ไทย' },
+ 'ar': { name: '阿拉伯语', native: 'العربية' },
+ // ... 可以继续添加更多语言
+ }
+};
+
+// 获取浏览器语言
+export const getBrowserLanguage = () => {
+ const lang = navigator.language || navigator.userLanguage;
+ // 处理类似 'zh-CN' 或 'en-US' 的格式
+ const baseLang = lang.split('-')[0];
+ const region = lang.split('-')[1];
+
+ // 特殊处理中文
+ if (baseLang === 'zh') {
+ return region === 'TW' || region === 'HK' ? 'zh-TW' : 'zh-CN';
+ }
+
+ // 检查是否支持该语言
+ const allLanguages = { ...LANGUAGES.common, ...LANGUAGES.others };
+ return allLanguages[baseLang] ? baseLang : 'en';
+};
+
+// 使用 Google Cloud Translation API 进行语言检测
+export const detectLanguage = async (text, apiKey) => {
+ try {
+ const response = await fetch(
+ `https://translation.googleapis.com/language/translate/v2/detect?key=${apiKey}`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ q: text
+ })
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error('Language detection failed');
+ }
+
+ const data = await response.json();
+ return data.data.detections[0][0].language;
+ } catch (error) {
+ console.error('Language detection error:', error);
+ // 降级到基本检测
+ return fallbackDetectLanguage(text);
+ }
+};
+
+// 更完善的语言检测规则
+const LANGUAGE_PATTERNS = {
+ // 东亚语言
+ 'zh-CN': {
+ pattern: /[\u4e00-\u9fa5]/,
+ threshold: 0.1,
+ excludePatterns: [/[ぁ-んァ-ン]/, /[가-힣]/] // 排除日文和韩文
+ },
+ 'zh-TW': {
+ pattern: /[[\u4e00-\u9fa5][\u3105-\u312F]]/,
+ threshold: 0.1,
+ traditionalChars: /[錒-鎛]/
+ },
+ 'ja': {
+ pattern: /[ぁ-んァ-ン]/,
+ threshold: 0.05,
+ extraPatterns: [/[\u4e00-\u9fa5]/, /[\u30A0-\u30FF]/] // 日文中可能包含汉字和片假名
+ },
+ 'ko': {
+ pattern: /[가-힣]/,
+ threshold: 0.1
+ },
+
+ // 欧洲语言
+ 'en': {
+ pattern: /[a-zA-Z]/,
+ threshold: 0.5,
+ wordPatterns: [/\b(the|and|is|in|to|of)\b/i]
+ },
+ 'fr': {
+ pattern: /[a-zA-Z]/,
+ threshold: 0.5,
+ wordPatterns: [/\b(le|la|les|et|est|dans|pour)\b/]
+ },
+ 'de': {
+ pattern: /[a-zA-Z]/,
+ threshold: 0.5,
+ wordPatterns: [/\b(der|die|das|und|ist|in|zu)\b/]
+ },
+ 'es': {
+ pattern: /[a-zA-Z]/,
+ threshold: 0.5,
+ wordPatterns: [/\b(el|la|los|las|es|en|por)\b/]
+ },
+ 'it': {
+ pattern: /[a-zA-Z]/,
+ threshold: 0.5,
+ wordPatterns: [/\b(il|la|le|è|in|per|con)\b/]
+ },
+ 'pt': {
+ pattern: /[a-zA-Z]/,
+ threshold: 0.5,
+ wordPatterns: [/\b(o|a|os|as|é|em|para)\b/]
+ },
+ 'ru': {
+ pattern: /[а-яА-Я]/,
+ threshold: 0.3
+ },
+
+ // 其他语系
+ 'ar': {
+ pattern: /[\u0600-\u06FF]/,
+ threshold: 0.2,
+ rtl: true
+ },
+ 'hi': {
+ pattern: /[\u0900-\u097F]/,
+ threshold: 0.2
+ },
+ 'th': {
+ pattern: /[\u0E00-\u0E7F]/,
+ threshold: 0.2
+ },
+ 'vi': {
+ pattern: /[àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ]/,
+ threshold: 0.1
+ }
+};
+
+// 改进的语言检测函数
+const fallbackDetectLanguage = (text) => {
+ const results = {};
+ const totalLength = text.length;
+
+ // 计算每种语言的匹配度
+ for (const [lang, config] of Object.entries(LANGUAGE_PATTERNS)) {
+ let matches = 0;
+ let isExcluded = false;
+
+ // 检查排除模式
+ if (config.excludePatterns) {
+ for (const excludePattern of config.excludePatterns) {
+ if (excludePattern.test(text)) {
+ isExcluded = true;
+ break;
+ }
+ }
+ }
+
+ if (isExcluded) continue;
+
+ // 基本字符匹配
+ matches = (text.match(config.pattern) || []).length;
+
+ // 检查额外模式
+ if (config.extraPatterns) {
+ for (const pattern of config.extraPatterns) {
+ matches += (text.match(pattern) || []).length;
+ }
+ }
+
+ // 检查特定词汇模式
+ if (config.wordPatterns) {
+ const wordMatches = config.wordPatterns.reduce((acc, pattern) => {
+ return acc + (text.match(pattern) || []).length;
+ }, 0);
+ matches += wordMatches * 2; // 给词汇匹配更高的权重
+ }
+
+ // 特殊处理繁体中文
+ if (lang === 'zh-TW' && config.traditionalChars.test(text)) {
+ matches *= 1.5; // 增加繁体字的权重
+ }
+
+ const ratio = matches / totalLength;
+ if (ratio >= config.threshold) {
+ results[lang] = ratio;
+ }
+ }
+
+ // 如果没有匹配结果,返回英语
+ if (Object.keys(results).length === 0) {
+ return 'en';
+ }
+
+ // 返回匹配度最高的语言
+ return Object.entries(results).reduce((a, b) => a[1] > b[1] ? a : b)[0];
+};
+
+// 格式化语言显示
+export const formatLanguageDisplay = (langCode) => {
+ const allLanguages = { ...LANGUAGES.common, ...LANGUAGES.others };
+ const lang = allLanguages[langCode];
+ return lang ? `${lang.name} (${lang.native})` : langCode;
+};
+
+// 验证语言代码
+export const isValidLanguageCode = (code) => {
+ // 检查是否符合 ISO 639-1 或 ISO 639-2 格式
+ return /^[a-z]{2,3}(-[A-Z]{2,3})?$/.test(code);
+};
\ No newline at end of file
diff --git a/content/panel.css b/content/panel.css
new file mode 100644
index 0000000..b35b33b
--- /dev/null
+++ b/content/panel.css
@@ -0,0 +1,233 @@
+.panel-container {
+ padding: 12px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+.panel-title {
+ margin: 0 0 12px 0;
+ font-size: 16px;
+ color: #333;
+ text-align: center;
+}
+
+.language-section {
+ margin-bottom: 15px;
+}
+
+label {
+ display: block;
+ margin-bottom: 8px;
+ color: #333;
+}
+
+select {
+ width: 100%;
+ padding: 8px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background-color: white;
+ font-size: 14px;
+}
+
+.action-section {
+ margin-top: 15px;
+}
+
+button {
+ width: 100%;
+ padding: 10px;
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ margin-bottom: 15px;
+}
+
+button:hover {
+ background-color: #45a049;
+}
+
+button:disabled {
+ background-color: #cccccc;
+ cursor: not-allowed;
+}
+
+.progress-bar {
+ width: 100%;
+ height: 6px;
+ background-color: #f0f0f0;
+ border-radius: 3px;
+ overflow: hidden;
+ margin-bottom: 6px;
+}
+
+.progress {
+ width: 0;
+ height: 100%;
+ background-color: #4CAF50;
+ transition: width 0.3s ease;
+}
+
+.progress-text {
+ text-align: center;
+ font-size: 12px;
+ color: #666;
+}
+
+.language-select-container {
+ position: relative;
+}
+
+#targetLang {
+ width: 200px;
+ padding: 6px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background-color: white;
+ font-size: 14px;
+}
+
+#customLang {
+ width: 192px;
+ padding: 6px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+ margin-top: 5px;
+}
+
+.target-language {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+
+#customLang.invalid {
+ border-color: #ff4444;
+ background-color: #fff8f8;
+}
+
+#customLang.invalid::placeholder {
+ color: #ff4444;
+}
+
+.language-select-container::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ bottom: -20px;
+ left: 0;
+ font-size: 12px;
+ color: #666;
+ display: none;
+}
+
+.language-select-container:hover::after {
+ display: block;
+}
+
+.button-group {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 12px;
+}
+
+.button-group button {
+ flex: 1;
+ padding: 8px 6px;
+ font-size: 13px;
+ margin: 0;
+}
+
+button.active {
+ background-color: #388E3C;
+}
+
+button:disabled {
+ background-color: #cccccc;
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+.translator-panel {
+ height: auto !important;
+ max-height: none !important;
+}
+
+.cache-controls {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 12px;
+}
+
+.secondary-button {
+ background-color: #757575;
+ font-size: 11px;
+ padding: 4px 6px;
+ height: 24px;
+ border-radius: 3px;
+ opacity: 0.8;
+ transition: opacity 0.2s;
+}
+
+.secondary-button:hover {
+ background-color: #616161;
+ opacity: 1;
+}
+
+.secondary-button:disabled {
+ background-color: #bdbdbd;
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+button.stop-translate {
+ background-color: #f44336;
+}
+
+button.stop-translate:hover {
+ background-color: #d32f2f;
+}
+
+.panel-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.panel-title {
+ margin: 0;
+ font-size: 16px;
+ color: #333;
+}
+
+.settings-link {
+ color: #4CAF50;
+ text-decoration: none;
+ font-size: 13px;
+ padding: 4px 8px;
+ border-radius: 3px;
+ transition: background-color 0.2s;
+}
+
+.settings-link:hover {
+ background-color: rgba(76, 175, 80, 0.1);
+}
+
+/* 深色模式支持 */
+@media (prefers-color-scheme: dark) {
+ .panel-title {
+ color: #fff;
+ }
+
+ .settings-link {
+ color: #81c784;
+ }
+
+ .settings-link:hover {
+ background-color: rgba(129, 199, 132, 0.1);
+ }
+}
+
\ No newline at end of file
diff --git a/content/panel.html b/content/panel.html
new file mode 100644
index 0000000..aa292ec
--- /dev/null
+++ b/content/panel.html
@@ -0,0 +1,46 @@
+
+
+
+
+ AI 网页翻译
+
+
+
+
+
+
+
+
原文语言: 检测中...
+
→
+
+
目标语言:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0%
+
+
+
+
+
\ No newline at end of file
diff --git a/content/panel.js b/content/panel.js
new file mode 100644
index 0000000..636370e
--- /dev/null
+++ b/content/panel.js
@@ -0,0 +1,361 @@
+import { LANGUAGES, getBrowserLanguage, formatLanguageDisplay, isValidLanguageCode } from './languages.js';
+
+let targetLang = 'zh-CN';
+let currentTabId = null;
+
+document.addEventListener('DOMContentLoaded', async () => {
+ const translateButton = document.getElementById('translatePage');
+ const replaceButton = document.getElementById('replaceTranslate');
+ const targetLangSelect = document.getElementById('targetLang');
+ const customLangInput = document.getElementById('customLang');
+ const sourceLanguageSpan = document.getElementById('sourceLanguage');
+ const clearCompareCache = document.getElementById('clearCompareCache');
+ const clearReplaceCache = document.getElementById('clearReplaceCache');
+
+ // 填充语言选项
+ const fillLanguageOptions = () => {
+ // 添加常用语言组
+ const commonGroup = document.createElement('optgroup');
+ commonGroup.label = '常用语言';
+ Object.entries(LANGUAGES.common).forEach(([code, lang]) => {
+ const option = document.createElement('option');
+ option.value = code;
+ option.textContent = `${lang.name} (${lang.native})`;
+ commonGroup.appendChild(option);
+ });
+ targetLangSelect.appendChild(commonGroup);
+
+ // 添加其他语言组
+ const othersGroup = document.createElement('optgroup');
+ othersGroup.label = '其他语言';
+ Object.entries(LANGUAGES.others).forEach(([code, lang]) => {
+ const option = document.createElement('option');
+ option.value = code;
+ option.textContent = `${lang.name} (${lang.native})`;
+ othersGroup.appendChild(option);
+ });
+ targetLangSelect.appendChild(othersGroup);
+
+ // 添加自定义语言选项
+ const customOption = document.createElement('option');
+ customOption.value = 'custom';
+ customOption.textContent = '自定义语言...';
+ targetLangSelect.appendChild(customOption);
+ };
+
+ fillLanguageOptions();
+
+ // 获取当前标签页ID
+ const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
+ currentTabId = tabs[0].id;
+
+ // 为面板添加唯一标识
+ document.body.setAttribute('data-tab-id', currentTabId);
+
+ // 设置默认目标语言为浏览器语言
+ const browserLang = getBrowserLanguage();
+
+ // 加载当前标签页的目标语言设置
+ chrome.storage.local.get({ [`targetLang_${currentTabId}`]: browserLang }, (items) => {
+ const savedLang = items[`targetLang_${currentTabId}`];
+ if (Object.keys({ ...LANGUAGES.common, ...LANGUAGES.others }).includes(savedLang)) {
+ targetLangSelect.value = savedLang;
+ } else {
+ targetLangSelect.value = 'custom';
+ customLangInput.style.display = 'block';
+ customLangInput.value = savedLang;
+ }
+ targetLang = savedLang;
+ });
+
+ // 监听自定义语言输入
+ customLangInput.addEventListener('input', (e) => {
+ const value = e.target.value.trim();
+ if (isValidLanguageCode(value)) {
+ targetLang = value;
+ customLangInput.classList.remove('invalid');
+ chrome.storage.local.set({ [`targetLang_${currentTabId}`]: value });
+ } else {
+ customLangInput.classList.add('invalid');
+ }
+ });
+
+ // 添加自定义语言输入提示
+ customLangInput.placeholder = '输入语言代码 (如: en, zh-CN, ja)';
+ customLangInput.title = '请输入符合 ISO 639-1 或 ISO 639-2 标准的语言代码';
+
+ // 监听语言选择变化
+ targetLangSelect.addEventListener('change', (e) => {
+ if (e.target.value === 'custom') {
+ customLangInput.style.display = 'block';
+ targetLang = customLangInput.value;
+ } else {
+ customLangInput.style.display = 'none';
+ targetLang = e.target.value;
+ }
+ chrome.storage.local.set({ [`targetLang_${currentTabId}`]: targetLang });
+ });
+
+ // 监听源语言更新
+ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ if (request.tabId !== currentTabId) return;
+
+ if (request.action === 'updateSourceLanguage' && request.language) {
+ // 处理中文显示
+ let displayLanguage = request.language;
+ if (displayLanguage === 'zh') {
+ // 检测是否为繁体中文
+ chrome.tabs.sendMessage(currentTabId, {
+ action: 'detectChineseVariant'
+ }, (response) => {
+ const isTraditional = response && response.isTraditional;
+ displayLanguage = isTraditional ? 'zh-TW' : 'zh-CN';
+ sourceLanguageSpan.textContent = formatLanguageDisplay(displayLanguage);
+ });
+ } else {
+ sourceLanguageSpan.textContent = formatLanguageDisplay(displayLanguage);
+ }
+
+ // 自动选择目标语言
+ if (request.language === targetLang) {
+ const defaultTarget = request.language.startsWith('zh') ? 'en' : 'zh-CN';
+ if (Object.keys(LANGUAGES).includes(defaultTarget)) {
+ targetLangSelect.value = defaultTarget;
+ customLangInput.style.display = 'none';
+ }
+ targetLang = defaultTarget;
+ chrome.storage.local.set({ [`targetLang_${currentTabId}`]: defaultTarget });
+ }
+ }
+ });
+
+ // 禁用另一个按钮的函数
+ const disableOtherButton = (activeButton) => {
+ const otherButton = activeButton === translateButton ? replaceButton : translateButton;
+ otherButton.disabled = true;
+ };
+
+ // 启用所有按钮的函数
+ const enableAllButtons = () => {
+ translateButton.disabled = false;
+ replaceButton.disabled = false;
+ };
+
+ // 禁用缓存清除按钮的函数
+ const disableCacheButtons = () => {
+ clearCompareCache.disabled = true;
+ clearReplaceCache.disabled = true;
+ };
+
+ // 启用缓存清除按钮的函数
+ const enableCacheButtons = () => {
+ clearCompareCache.disabled = false;
+ clearReplaceCache.disabled = false;
+ };
+
+ // 修改翻译按钮点击事件
+ translateButton.addEventListener('click', () => {
+ if (translateButton.textContent === '对比翻译') {
+ translateButton.textContent = '停止翻译';
+ translateButton.classList.add('stop-translate');
+ translateButton.disabled = false;
+ replaceButton.disabled = true;
+ disableCacheButtons();
+ chrome.tabs.sendMessage(currentTabId, {
+ action: 'translatePage',
+ targetLang: targetLang
+ }).catch(error => {
+ console.error('翻译请求失败:', error);
+ translateButton.textContent = '对比翻译';
+ translateButton.classList.remove('stop-translate');
+ enableAllButtons();
+ enableCacheButtons();
+ });
+ } else if (translateButton.textContent === '停止翻译') {
+ chrome.tabs.sendMessage(currentTabId, {
+ action: 'stopTranslation',
+ translationType: 'compare'
+ });
+ } else if (translateButton.textContent === '显示原文') {
+ translateButton.textContent = '对比翻译';
+ replaceButton.textContent = '替换翻译';
+ enableAllButtons();
+ enableCacheButtons();
+ chrome.tabs.sendMessage(currentTabId, {
+ action: 'restoreOriginal'
+ });
+ }
+ });
+
+ // 替换翻译按钮点击事件
+ replaceButton.addEventListener('click', () => {
+ if (replaceButton.textContent === '替换翻译') {
+ replaceButton.textContent = '停止翻译';
+ replaceButton.classList.add('stop-translate');
+ replaceButton.disabled = false;
+ translateButton.disabled = true;
+ disableCacheButtons();
+ chrome.tabs.sendMessage(currentTabId, {
+ action: 'replaceTranslate',
+ targetLang: targetLang
+ }).catch(error => {
+ console.error('替换翻译失败:', error);
+ replaceButton.textContent = '替换翻译';
+ replaceButton.classList.remove('stop-translate');
+ enableAllButtons();
+ enableCacheButtons();
+ });
+ } else if (replaceButton.textContent === '停止翻译') {
+ chrome.tabs.sendMessage(currentTabId, {
+ action: 'stopTranslation',
+ translationType: 'replace'
+ });
+ } else if (replaceButton.textContent === '显示原文') {
+ replaceButton.textContent = '替换翻译';
+ translateButton.textContent = '对比翻译';
+ enableAllButtons();
+ enableCacheButtons();
+ chrome.tabs.sendMessage(currentTabId, {
+ action: 'restoreOriginal'
+ });
+ }
+ });
+
+ // 清除缓存按钮点击事件
+ clearCompareCache.addEventListener('click', () => {
+ chrome.tabs.sendMessage(currentTabId, {
+ action: 'clearCache',
+ cacheType: 'compare'
+ }, (response) => {
+ if (response.success) {
+ showCacheClearMessage('对比翻译缓存已清除');
+ clearCompareCache.disabled = true;
+ } else if (response.empty) {
+ showCacheClearMessage('没有可清除的对比翻译缓存');
+ clearCompareCache.disabled = true;
+ }
+ });
+ });
+
+ clearReplaceCache.addEventListener('click', () => {
+ chrome.tabs.sendMessage(currentTabId, {
+ action: 'clearCache',
+ cacheType: 'replace'
+ }, (response) => {
+ if (response.success) {
+ showCacheClearMessage('替换翻译缓存已清除');
+ clearReplaceCache.disabled = true;
+ } else if (response.empty) {
+ showCacheClearMessage('没有可清除的替换翻译缓存');
+ clearReplaceCache.disabled = true;
+ }
+ });
+ });
+
+ // 显示缓存清除消息
+ const showCacheClearMessage = (message) => {
+ const progressText = document.querySelector('.progress-text');
+ progressText.textContent = message;
+ setTimeout(() => {
+ progressText.textContent = '';
+ }, 2000);
+ };
+
+ // 检查缓存状态并更新按钮
+ const updateCacheButtons = () => {
+ chrome.tabs.sendMessage(currentTabId, { action: 'checkCache' }, (response) => {
+ clearCompareCache.disabled = !response.hasCompareCache;
+ clearReplaceCache.disabled = !response.hasReplaceCache;
+ });
+ };
+
+ // 在面板创建和翻译完成时检查缓存状态
+ document.addEventListener('DOMContentLoaded', () => {
+ updateCacheButtons();
+ });
+
+ // 修改消息监听器
+ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ if (sender.tab && sender.tab.id !== currentTabId) return;
+
+ const progressBar = document.querySelector('.progress');
+ const progressText = document.querySelector('.progress-text');
+
+ if (request.action === 'updateProgress') {
+ progressBar.style.width = `${request.progress}%`;
+ progressText.textContent = `${request.progress}%`;
+ } else if (request.action === 'translationComplete') {
+ const translationType = request.translationType;
+ const activeButton = translationType === 'compare' ? translateButton : replaceButton;
+ const inactiveButton = translationType === 'compare' ? replaceButton : translateButton;
+
+ activeButton.textContent = '显示原文';
+ activeButton.classList.remove('stop-translate');
+ activeButton.disabled = false;
+
+ inactiveButton.disabled = true;
+ inactiveButton.textContent = translationType === 'compare' ? '替换翻译' : '对比翻译';
+ inactiveButton.classList.remove('stop-translate');
+
+ progressBar.style.width = '100%';
+ progressText.textContent = '100%';
+ enableCacheButtons();
+ } else if (request.action === 'translationStopped') {
+ const translationType = request.translationType;
+ const activeButton = translationType === 'compare' ? translateButton : replaceButton;
+ const inactiveButton = translationType === 'compare' ? replaceButton : translateButton;
+
+ activeButton.textContent = translationType === 'compare' ? '对比翻译' : '替换翻译';
+ activeButton.classList.remove('stop-translate');
+ inactiveButton.textContent = translationType === 'compare' ? '替换翻译' : '对比翻译';
+ inactiveButton.classList.remove('stop-translate');
+
+ progressBar.style.width = '0%';
+ progressText.textContent = '已停止';
+ enableAllButtons();
+ enableCacheButtons();
+ }
+ });
+
+ // 通知 content script 面板已创建,请求语言检测结果
+ chrome.tabs.sendMessage(currentTabId, {
+ action: 'panelCreated',
+ tabId: currentTabId
+ });
+
+ // 添加设置按钮点击事件
+ document.getElementById('openOptions').addEventListener('click', (e) => {
+ e.preventDefault();
+ chrome.runtime.openOptionsPage();
+ });
+});
+
+// 监听来自content script的消息
+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ // 只处理当前标签页的消息
+ if (sender.tab && sender.tab.id !== currentTabId) return;
+
+ const translateButton = document.getElementById('translatePage');
+ const progressBar = document.querySelector('.progress');
+ const progressText = document.querySelector('.progress-text');
+
+ if (request.action === 'updateProgress') {
+ progressBar.style.width = `${request.progress}%`;
+ progressText.textContent = `${request.progress}%`;
+ } else if (request.action === 'translationComplete') {
+ translateButton.textContent = '显示原文';
+ translateButton.disabled = false;
+ progressBar.style.width = '100%';
+ progressText.textContent = '100%';
+ } else if (request.action === 'restorationComplete') {
+ progressBar.style.width = '0%';
+ progressText.textContent = '0%';
+ }
+});
+
+// 监听标签页关闭事件,清理存储
+chrome.tabs.onRemoved.addListener((tabId) => {
+ if (tabId === currentTabId) {
+ chrome.storage.local.remove(`targetLang_${tabId}`);
+ }
+});
\ No newline at end of file
diff --git a/icons/icon.svg b/icons/icon.svg
new file mode 100644
index 0000000..c4432ef
--- /dev/null
+++ b/icons/icon.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..ad72c36
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,46 @@
+{
+ "manifest_version": 3,
+ "name": "AI 极简翻译助手",
+ "version": "1.0",
+ "description": "使用在线服务大模型的 API 进行智能网页翻译",
+ "permissions": [
+ "storage",
+ "activeTab",
+ "scripting",
+ "contextMenus",
+ "tabs"
+ ],
+ "action": {},
+ "options_page": "options/options.html",
+ "background": {
+ "service_worker": "background/background.js"
+ },
+ "content_scripts": [
+ {
+ "matches": [""],
+ "js": [
+ "content/cache-manager.js",
+ "content/content.js"
+ ],
+ "css": ["content/content.css"]
+ }
+ ],
+ "web_accessible_resources": [{
+ "resources": [
+ "content/panel.html",
+ "content/panel.css",
+ "content/panel.js",
+ "content/languages.js",
+ "translate/translate.html",
+ "translate/translate.css",
+ "translate/translate.js",
+ "options/options.html",
+ "options/options.css",
+ "options/options.js"
+ ],
+ "matches": [""]
+ }],
+ "host_permissions": [
+ "https://api.siliconflow.cn/*"
+ ]
+}
\ No newline at end of file
diff --git a/options/options.css b/options/options.css
new file mode 100644
index 0000000..88f27f8
--- /dev/null
+++ b/options/options.css
@@ -0,0 +1,39 @@
+.container {
+ width: 80%;
+ max-width: 600px;
+ margin: 20px auto;
+ padding: 20px;
+}
+
+.form-group {
+ margin-bottom: 15px;
+}
+
+label {
+ display: block;
+ margin-bottom: 5px;
+}
+
+input, select {
+ width: 100%;
+ padding: 8px;
+ margin-bottom: 10px;
+}
+
+button {
+ background-color: #4CAF50;
+ color: white;
+ padding: 10px 20px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+button:hover {
+ background-color: #45a049;
+}
+
+#status {
+ margin-top: 10px;
+ padding: 10px;
+}
\ No newline at end of file
diff --git a/options/options.html b/options/options.html
new file mode 100644
index 0000000..300c929
--- /dev/null
+++ b/options/options.html
@@ -0,0 +1,28 @@
+
+
+
+
+ AI 翻译助手设置
+
+
+
+
+
AI 翻译助手设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/options/options.js b/options/options.js
new file mode 100644
index 0000000..804a01b
--- /dev/null
+++ b/options/options.js
@@ -0,0 +1,38 @@
+document.addEventListener('DOMContentLoaded', () => {
+ // 加载保存的设置
+ chrome.storage.sync.get(
+ {
+ apiEndpoint: 'https://api.siliconflow.cn/v1/chat/completions',
+ apiKey: 'sk-giizgegutggzuhelpcwgqsbfpvmlpjfdxszikbmdzwtpuovu',
+ model: 'Qwen/Qwen2.5-7B-Instruct'
+ },
+ (items) => {
+ document.getElementById('apiEndpoint').value = items.apiEndpoint;
+ document.getElementById('apiKey').value = items.apiKey;
+ document.getElementById('model').value = items.model;
+ }
+ );
+
+ // 保存设置
+ document.getElementById('save').addEventListener('click', () => {
+ const apiEndpoint = document.getElementById('apiEndpoint').value;
+ const apiKey = document.getElementById('apiKey').value;
+ const model = document.getElementById('model').value;
+
+ chrome.storage.sync.set(
+ {
+ apiEndpoint,
+ apiKey,
+ model
+ },
+ () => {
+ const status = document.getElementById('status');
+ status.textContent = '设置已保存';
+ status.style.color = 'green';
+ setTimeout(() => {
+ status.textContent = '';
+ }, 2000);
+ }
+ );
+ });
+});
\ No newline at end of file
diff --git a/panel.js b/panel.js
new file mode 100644
index 0000000..fe90c5c
--- /dev/null
+++ b/panel.js
@@ -0,0 +1,29 @@
+document.addEventListener('DOMContentLoaded', async () => {
+ // ... 其他初始化代码 ...
+
+ // 获取当前标签页ID
+ const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
+ currentTabId = tabs[0].id;
+
+ // 为面板添加唯一标识
+ document.body.setAttribute('data-tab-id', currentTabId);
+
+ // 从存储中获取特定标签页的目标语言设置
+ chrome.storage.sync.get({ [`targetLang_${currentTabId}`]: 'zh' }, (items) => {
+ targetLangSelect.value = items[`targetLang_${currentTabId}`];
+ targetLang = items[`targetLang_${currentTabId}`];
+ });
+
+ // 监听语言选择变化
+ targetLangSelect.addEventListener('change', (e) => {
+ targetLang = e.target.value;
+ // 保存特定标签页的目标语言设置
+ chrome.storage.sync.set({ [`targetLang_${currentTabId}`]: targetLang });
+ });
+
+ // 通知content script面板已创建
+ chrome.tabs.sendMessage(currentTabId, {
+ action: 'panelCreated',
+ tabId: currentTabId
+ });
+});
\ No newline at end of file
diff --git a/popup/popup.css b/popup/popup.css
new file mode 100644
index 0000000..7b901db
--- /dev/null
+++ b/popup/popup.css
@@ -0,0 +1,43 @@
+.container {
+ width: 256px;
+ padding: 15px;
+}
+
+button {
+ width: 100%;
+ padding: 10px;
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+button:hover {
+ background-color: #45a049;
+}
+
+.divider {
+ margin: 15px 0;
+ border-top: 1px solid #ddd;
+}
+
+.info {
+ font-size: 12px;
+}
+
+.info ul {
+ padding-left: 20px;
+ margin: 5px 0;
+}
+
+.footer {
+ margin-top: 15px;
+ text-align: right;
+}
+
+.footer a {
+ color: #666;
+ text-decoration: none;
+ font-size: 12px;
+}
\ No newline at end of file
diff --git a/popup/popup.html b/popup/popup.html
new file mode 100644
index 0000000..eb4bab9
--- /dev/null
+++ b/popup/popup.html
@@ -0,0 +1,25 @@
+
+
+
+
+ AI 翻译助手
+
+
+
+
+
+
+
+
提示:
+
+ - 选中文本后自动显示翻译
+ - 点击上方按钮翻译整页
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/popup/popup.js b/popup/popup.js
new file mode 100644
index 0000000..4491c90
--- /dev/null
+++ b/popup/popup.js
@@ -0,0 +1,13 @@
+document.addEventListener('DOMContentLoaded', () => {
+ // 打开设置页面
+ document.getElementById('openOptions').addEventListener('click', () => {
+ chrome.runtime.openOptionsPage();
+ });
+
+ // 翻译整个页面
+ document.getElementById('translatePage').addEventListener('click', () => {
+ chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
+ chrome.tabs.sendMessage(tabs[0].id, {action: 'translatePage'});
+ });
+ });
+});
\ No newline at end of file
diff --git "a/screenshots/pdf\345\217\263\351\224\256.png" "b/screenshots/pdf\345\217\263\351\224\256.png"
new file mode 100755
index 0000000..29eb48f
Binary files /dev/null and "b/screenshots/pdf\345\217\263\351\224\256.png" differ
diff --git "a/screenshots/\345\210\222\350\257\215\347\277\273\350\257\221.png" "b/screenshots/\345\210\222\350\257\215\347\277\273\350\257\221.png"
new file mode 100755
index 0000000..5cf1f3f
Binary files /dev/null and "b/screenshots/\345\210\222\350\257\215\347\277\273\350\257\221.png" differ
diff --git "a/screenshots/\345\257\271\346\257\224\347\277\273\350\257\221\347\244\272\344\276\213\351\241\265\351\235\242.png" "b/screenshots/\345\257\271\346\257\224\347\277\273\350\257\221\347\244\272\344\276\213\351\241\265\351\235\242.png"
new file mode 100755
index 0000000..495ad24
Binary files /dev/null and "b/screenshots/\345\257\271\346\257\224\347\277\273\350\257\221\347\244\272\344\276\213\351\241\265\351\235\242.png" differ
diff --git "a/screenshots/\346\233\277\346\215\242\347\277\273\350\257\221\347\244\272\344\276\213\351\241\265\351\235\242.png" "b/screenshots/\346\233\277\346\215\242\347\277\273\350\257\221\347\244\272\344\276\213\351\241\265\351\235\242.png"
new file mode 100755
index 0000000..a3b7e9d
Binary files /dev/null and "b/screenshots/\346\233\277\346\215\242\347\277\273\350\257\221\347\244\272\344\276\213\351\241\265\351\235\242.png" differ
diff --git "a/screenshots/\347\202\271\345\207\273\346\217\222\344\273\266\346\214\211\351\222\256\345\207\272\347\216\260\347\232\204\345\265\214\345\205\245\345\274\217\351\241\265\351\235\242.png" "b/screenshots/\347\202\271\345\207\273\346\217\222\344\273\266\346\214\211\351\222\256\345\207\272\347\216\260\347\232\204\345\265\214\345\205\245\345\274\217\351\241\265\351\235\242.png"
new file mode 100755
index 0000000..ca88c4a
Binary files /dev/null and "b/screenshots/\347\202\271\345\207\273\346\217\222\344\273\266\346\214\211\351\222\256\345\207\272\347\216\260\347\232\204\345\265\214\345\205\245\345\274\217\351\241\265\351\235\242.png" differ
diff --git "a/screenshots/\347\213\254\347\253\213\347\277\273\350\257\221\345\274\271\347\252\227.png" "b/screenshots/\347\213\254\347\253\213\347\277\273\350\257\221\345\274\271\347\252\227.png"
new file mode 100755
index 0000000..46b7dc7
Binary files /dev/null and "b/screenshots/\347\213\254\347\253\213\347\277\273\350\257\221\345\274\271\347\252\227.png" differ
diff --git "a/screenshots/\350\207\252\345\256\232\344\271\211\345\244\247\346\250\241\345\236\213API\345\234\260\345\235\200\345\222\214ak.png" "b/screenshots/\350\207\252\345\256\232\344\271\211\345\244\247\346\250\241\345\236\213API\345\234\260\345\235\200\345\222\214ak.png"
new file mode 100755
index 0000000..12501f0
Binary files /dev/null and "b/screenshots/\350\207\252\345\256\232\344\271\211\345\244\247\346\250\241\345\236\213API\345\234\260\345\235\200\345\222\214ak.png" differ
diff --git a/translate/translate.css b/translate/translate.css
new file mode 100644
index 0000000..24563ff
--- /dev/null
+++ b/translate/translate.css
@@ -0,0 +1,266 @@
+body {
+ margin: 0;
+ padding: 0 20px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ min-width: 500px;
+ min-height: 300px;
+}
+
+.container {
+ display: grid;
+ grid-template-rows: auto auto 1fr;
+ gap: 15px;
+ height: 100%;
+}
+
+.source-text,
+.translated-text {
+ min-height: 0;
+}
+
+h3 {
+ margin: 0 0 10px 0;
+ color: #333;
+ font-size: 14px;
+}
+
+.content {
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background: #f9f9f9;
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.translation-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 0;
+ border-top: 1px solid #eee;
+ border-bottom: 1px solid #eee;
+}
+
+.language-select {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+select {
+ padding: 6px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+ min-width: 200px;
+}
+
+button {
+ padding: 6px 12px;
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+button:hover {
+ background-color: #45a049;
+}
+
+.loading {
+ color: #666;
+ font-style: italic;
+}
+
+.edit-hint {
+ font-size: 12px;
+ color: #666;
+ font-weight: normal;
+}
+
+textarea.content {
+ width: 100%;
+ min-height: 100px;
+ resize: vertical;
+ font-family: inherit;
+ box-sizing: border-box;
+ border: 1px solid #ddd;
+ outline: none;
+}
+
+textarea.content:focus {
+ border-color: #4CAF50;
+}
+
+/* 深色模式支持 */
+@media (prefers-color-scheme: dark) {
+ body {
+ background: #1e1e1e;
+ color: #fff;
+ }
+
+ h3 {
+ color: #fff;
+ }
+
+ .content {
+ background: #2d2d2d;
+ border-color: #444;
+ color: #fff;
+ }
+
+ select {
+ background: #2d2d2d;
+ border-color: #444;
+ color: #fff;
+ }
+
+ .translation-controls {
+ border-color: #444;
+ }
+
+ .edit-hint {
+ color: #999;
+ }
+
+ textarea.content {
+ background: #2d2d2d;
+ border-color: #444;
+ color: #fff;
+ }
+
+ textarea.content:focus {
+ border-color: #81c784;
+ }
+}
+
+.main-container {
+ display: flex;
+ height: 100vh;
+ gap: 20px;
+}
+
+.translate-container {
+ flex: 2;
+ display: grid;
+ grid-template-rows: auto auto 1fr;
+ gap: 15px;
+}
+
+.history-container {
+ flex: 1;
+ min-width: 256px;
+ border-left: 1px solid #ddd;
+ padding-left: 20px;
+}
+
+.history-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.clear-btn {
+ font-size: 12px;
+ padding: 4px 8px;
+ background-color: #f44336;
+}
+
+.clear-btn:hover {
+ background-color: #d32f2f;
+}
+
+.history-list {
+ overflow-y: auto;
+ max-height: calc(100vh - 100px);
+}
+
+.history-item {
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ margin-bottom: 10px;
+ cursor: pointer;
+ position: relative;
+}
+
+.history-item:hover {
+ background-color: #f5f5f5;
+}
+
+.history-item .time {
+ font-size: 12px;
+ color: #666;
+ margin-bottom: 5px;
+}
+
+.history-item .source {
+ font-size: 13px;
+ margin-bottom: 5px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.history-item .target-lang {
+ font-size: 12px;
+ color: #4CAF50;
+ margin-bottom: 5px;
+}
+
+.history-item .translation {
+ font-size: 13px;
+ color: #666;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.history-item .delete-btn {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ padding: 2px 6px;
+ font-size: 10px;
+ background-color: transparent;
+ color: #f44336;
+ display: none;
+}
+
+.history-item:hover .delete-btn {
+ display: block;
+}
+
+/* 深色模式支持 */
+@media (prefers-color-scheme: dark) {
+ .history-container {
+ border-left-color: #444;
+ }
+
+ .history-item {
+ border-color: #444;
+ background-color: #2d2d2d;
+ }
+
+ .history-item:hover {
+ background-color: #383838;
+ }
+
+ .history-item .time {
+ color: #999;
+ }
+
+ .history-item .translation {
+ color: #bbb;
+ }
+
+ .history-item .delete-btn {
+ color: #ff6b6b;
+ }
+}
\ No newline at end of file
diff --git a/translate/translate.html b/translate/translate.html
new file mode 100644
index 0000000..a6a4fed
--- /dev/null
+++ b/translate/translate.html
@@ -0,0 +1,42 @@
+
+
+
+
+ AI 翻译结果
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/translate/translate.js b/translate/translate.js
new file mode 100644
index 0000000..c97c77f
--- /dev/null
+++ b/translate/translate.js
@@ -0,0 +1,197 @@
+import { LANGUAGES, getBrowserLanguage, formatLanguageDisplay, isValidLanguageCode } from '../content/languages.js';
+
+let currentText = '';
+
+// 历史记录管理
+class TranslationHistory {
+ constructor() {
+ this.maxItems = 100; // 最大保存记录数
+ }
+
+ async getAll() {
+ const result = await chrome.storage.local.get('translationHistory');
+ return result.translationHistory || [];
+ }
+
+ async add(item) {
+ const history = await this.getAll();
+ history.unshift({
+ ...item,
+ id: Date.now(),
+ time: new Date().toLocaleString()
+ });
+
+ // 限制最大记录数
+ if (history.length > this.maxItems) {
+ history.pop();
+ }
+
+ await chrome.storage.local.set({ translationHistory: history });
+ return history;
+ }
+
+ async remove(id) {
+ const history = await this.getAll();
+ const newHistory = history.filter(item => item.id !== id);
+ await chrome.storage.local.set({ translationHistory: newHistory });
+ return newHistory;
+ }
+
+ async clear() {
+ await chrome.storage.local.set({ translationHistory: [] });
+ return [];
+ }
+}
+
+document.addEventListener('DOMContentLoaded', async () => {
+ const history = new TranslationHistory();
+ const sourceContent = document.getElementById('sourceContent');
+ const translatedContent = document.getElementById('translatedContent');
+ const targetLangSelect = document.getElementById('targetLang');
+ const translateBtn = document.getElementById('translateBtn');
+ const historyList = document.getElementById('historyList');
+ const clearHistoryBtn = document.getElementById('clearHistory');
+ const loading = translatedContent.querySelector('.loading');
+ const result = translatedContent.querySelector('.result');
+
+ // 填充语言选项
+ const fillLanguageOptions = () => {
+ // 常用语言组
+ const commonGroup = document.createElement('optgroup');
+ commonGroup.label = '常用语言';
+ Object.entries(LANGUAGES.common).forEach(([code, lang]) => {
+ const option = document.createElement('option');
+ option.value = code;
+ option.textContent = `${lang.name} (${lang.native})`;
+ commonGroup.appendChild(option);
+ });
+ targetLangSelect.appendChild(commonGroup);
+
+ // 其他语言组
+ const othersGroup = document.createElement('optgroup');
+ othersGroup.label = '其他语言';
+ Object.entries(LANGUAGES.others).forEach(([code, lang]) => {
+ const option = document.createElement('option');
+ option.value = code;
+ option.textContent = `${lang.name} (${lang.native})`;
+ othersGroup.appendChild(option);
+ });
+ targetLangSelect.appendChild(othersGroup);
+ };
+
+ fillLanguageOptions();
+
+ // 获取存储的目标语言
+ chrome.storage.local.get({ 'translateWindow_targetLang': 'zh-CN' }, (items) => {
+ targetLangSelect.value = items.translateWindow_targetLang;
+ });
+
+ // 监听语��选择变化
+ targetLangSelect.addEventListener('change', (e) => {
+ chrome.storage.local.set({ 'translateWindow_targetLang': e.target.value });
+ });
+
+ // 监听文本输入
+ sourceContent.addEventListener('input', () => {
+ currentText = sourceContent.value;
+ });
+
+ // 渲染历史记录
+ const renderHistory = async () => {
+ const records = await history.getAll();
+ historyList.innerHTML = records.map(record => `
+
+
${record.time}
+
${record.sourceText}
+
翻译为:${formatLanguageDisplay(record.targetLang)}
+
${record.translation}
+
+
+ `).join('');
+
+ // 添加点击事件
+ historyList.querySelectorAll('.history-item').forEach(item => {
+ item.addEventListener('click', (e) => {
+ if (e.target.classList.contains('delete-btn')) {
+ e.stopPropagation();
+ const id = parseInt(item.dataset.id);
+ history.remove(id).then(renderHistory);
+ } else {
+ const id = parseInt(item.dataset.id);
+ const record = records.find(r => r.id === id);
+ if (record) {
+ sourceContent.value = record.sourceText;
+ targetLangSelect.value = record.targetLang;
+ result.textContent = record.translation;
+ currentText = record.sourceText;
+ }
+ }
+ });
+ });
+ };
+
+ // 清除所有历史记录
+ clearHistoryBtn.addEventListener('click', async () => {
+ if (confirm('确定要清除所有历史记录吗?')) {
+ await history.clear();
+ renderHistory();
+ }
+ });
+
+ // 修改翻译按钮点击事件
+ translateBtn.addEventListener('click', async () => {
+ currentText = sourceContent.value.trim();
+ if (!currentText) {
+ result.textContent = '请输入要翻译的文本';
+ return;
+ }
+
+ loading.style.display = 'block';
+ result.textContent = '';
+
+ try {
+ const currentTargetLang = targetLangSelect.value;
+ const response = await chrome.runtime.sendMessage({
+ action: 'translate',
+ text: currentText,
+ targetLang: currentTargetLang,
+ isPopupWindow: true
+ });
+
+ loading.style.display = 'none';
+ const translation = response.translation || '翻译失败';
+ result.textContent = translation;
+
+ // 添加到历史记录
+ await history.add({
+ sourceText: currentText,
+ targetLang: currentTargetLang,
+ translation: translation
+ });
+ renderHistory();
+ } catch (error) {
+ loading.style.display = 'none';
+ result.textContent = '翻译失败,请重试';
+ }
+ });
+
+ // 监听来自background的消息
+ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ if (request.action === 'updateText') {
+ currentText = request.text;
+ sourceContent.value = currentText;
+ translateBtn.click(); // 自动开始翻译
+ }
+ });
+
+ // 初始加载历史记录
+ renderHistory();
+
+ // 支持快捷键翻译
+ sourceContent.addEventListener('keydown', (e) => {
+ // Ctrl+Enter 或 Command+Enter 触发翻译
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
+ translateBtn.click();
+ }
+ });
+});
\ No newline at end of file