// ==UserScript==
// @name Yandex/微软自动翻译
// @namespace https://viayoo.com/
// @version 2.24.1
// @description Yandex/微软自动翻译小部件 | 自动翻译整个页面 | 简化界面 | 使用Ctrl+Shift+Y切换 | 增强多引擎支持 | 支持颜色自定义 | 网站专用引擎 | 立即翻译
// @author You
// @run-at document-start
// @match https://*/*
// @match http://*/*
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect api-edge.cognitive.microsofttranslator.com
// @connect edge.microsoft.com
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 脚本配置
const CONFIG = {
widgetId: 'ytWidget',
storageKey: 'yandex_widget_enabled',
whitelistKey: 'yandex_whitelist',
translationEngineKey: 'translation_engine',
translationLanguageKey: 'translation_language',
translationModeKey: 'translation_mode',
translationColorKey: 'translation_color',
translationBgColorKey: 'translation_bg_color',
siteSpecificEnginesKey: 'site_specific_engines',
showTranslationIndicatorKey: 'show_translation_indicator', // 新增:控制提示显示
defaultEnabled: true,
hotkey: 'Ctrl+Shift+Y',
ENGINES: {
YANDEX: 'yandex',
MICROSOFT: 'microsoft'
},
MODES: {
REPLACE: 'replace',
DUAL: 'dual'
},
defaultEngine: 'microsoft',
defaultTargetLanguage: 'zh-Hans',
defaultMode: 'replace',
defaultTextColor: '#0066cc',
defaultBgColor: 'rgba(0,102,204,0.1)',
russianDetectionThreshold: 0.2,
LANGUAGES: {
'zh-Hans': {code: 'zh-Hans', name: '简体中文', icon: '🇨🇳'},
'en': {code: 'en', name: '英语', icon: '🇺🇸'},
'ru': {code: 'ru', name: '俄语', icon: '🇷🇺'},
'ja': {code: 'ja', name: '日语', icon: '🇯🇵'},
'ko': {code: 'ko', name: '韩语', icon: '🇰🇷'},
'fr': {code: 'fr', name: '法语', icon: '🇫🇷'},
'de': {code: 'de', name: '德语', icon: '🇩🇪'},
'es': {code: 'es', name: '西班牙语', icon: '🇪🇸'},
'pt': {code: 'pt', name: '葡萄牙语', icon: '🇵🇹'},
'ar': {code: 'ar', name: '阿拉伯语', icon: '🇸🇦'},
'it': {code: 'it', name: '意大利语', icon: '🇮🇹'},
'vi': {code: 'vi', name: '越南语', icon: '🇻🇳'},
'th': {code: 'th', name: '泰语', icon: '🇹🇭'}
},
COLOR_PRESETS: [
{name: '默认蓝色', value: '#0066cc', bg: 'rgba(0,102,204,0.1)'},
{name: '绿色', value: '#00b894', bg: 'rgba(0,184,148,0.1)'},
{name: '红色', value: '#ff4757', bg: 'rgba(255,71,87,0.1)'},
{name: '紫色', value: '#6c5ce7', bg: 'rgba(108,92,231,0.1)'},
{name: '橙色', value: '#ff9f43', bg: 'rgba(255,159,67,0.1)'},
{name: '深色', value: '#2d3436', bg: 'rgba(45,52,54,0.1)'},
{name: '粉色', value: '#fd79a8', bg: 'rgba(253,121,168,0.1)'},
{name: '青色', value: '#00cec9', bg: 'rgba(0,206,201,0.1)'}
],
russianDomains: ['.ru', '.рф', '.su', '.by', '.kz', '.ua', '.com.ru'],
russianKeywords: [
'программы', 'андроид', 'скачать', 'русский', 'приложение',
'софт', 'игры', 'утилиты', 'бесплатно', 'android', 'программу',
'для', 'на', 'телефон', 'смартфон', 'установить', 'загрузить'
],
russianSiteOptimizations: {
'programmy-dlya-android.ru': {
textExtraction: 'enhanced',
mergeShortTexts: true,
prioritizeLongTexts: true,
forceRussianSource: true,
excludeSelectors: ['code', '.code', 'pre', '.pre', 'textarea', 'input'],
alwaysUseYandex: true
},
'rutracker.org': {
textExtraction: 'simple',
excludeSelectors: ['.post_body', '.sp-wrap', '.code'],
alwaysUseYandex: true
}
},
MAX_TEXT_LENGTH: 5000,
BATCH_SIZE: 50,
AUTO_TRANSLATE_DELAY: 100,
DOM_CHANGE_DELAY: 800,
SCROLL_CHECK_DELAY: 2000,
MAX_RETRY_COUNT: 2,
YANDEX_CHECK_INTERVAL: 3000,
YANDEX_TRANSLATE_TIMEOUT: 5000,
TRANSLATING_INDICATOR_DURATION: 3000, // 统一提示显示时间:3秒
TRANSLATING_INDICATOR_FADE_OUT: 1000
};
// 脚本状态
let widgetEnabled = CONFIG.defaultEnabled;
let isWhitelistedDomain = false;
let mainMenuCommandId = null;
let whitelistMenuCommandId = null;
let engineMenuCommandId = null;
let settingsMenuCommandId = null;
let widgetContainer = null;
let yandexScript = null;
let isModalOpen = false;
let cachedWhitelist = null;
let currentDomain = '';
let isRussianSite = false;
let isRussianContent = false;
let currentRussianSiteConfig = null;
let currentEngine = CONFIG.ENGINES.MICROSOFT;
let targetLanguage = CONFIG.defaultTargetLanguage;
let translationMode = CONFIG.defaultMode;
let translationTextColor = CONFIG.defaultTextColor;
let translationBgColor = CONFIG.defaultBgColor;
let siteSpecificEngines = {};
let microsoftAuthToken = null;
let microsoftTranslatorActive = false;
let pageTranslating = false;
let translationCache = new Map();
let autoTranslateTimeout = null;
let originalTextMap = new Map();
let domObserver = null;
let domChangeTimer = null;
let isObservingDOM = false;
let scrollCheckTimer = null;
let lastScrollY = 0;
let scrollCheckCount = 0;
let continuousTranslationTimer = null;
let translationRetryCount = 0;
let lastTranslationTime = 0;
let isUsingSiteSpecificEngine = false;
let siteSpecificEngineName = '';
let russianContentRatio = 0;
let russianDetectionSamples = 0;
let russianDetectionTimer = null;
let isLeavingRussianSite = false;
let isPageLoaded = false;
let pageLoadedTimer = null;
let immediateTranslationScheduled = false;
let menuTranslationFixApplied = false;
let yandexTranslationActive = false;
let yandexCheckInterval = null;
let yandexTranslationAttempts = 0;
let yandexMaxAttempts = 5;
let currentTranslatingIndicator = null;
let indicatorHideTimeout = null;
let isTranslatingIndicatorVisible = false;
let currentIndicatorText = '';
let showTranslationIndicator = true; // 新增:控制提示显示
// 获取当前域名
function getCurrentDomain() {
return location.hostname;
}
// 检测俄语字符
function containsRussianText(text) {
const russianRegex = /[\u0400-\u04FF]/;
return russianRegex.test(text);
}
// 计算文本中俄语字符的比例
function getRussianCharacterRatio(text) {
if (!text || text.length === 0) return 0;
const russianChars = text.match(/[\u0400-\u04FF]/g) || [];
return russianChars.length / text.length;
}
// 增强的俄语内容检测
function detectRussianByContent() {
if (!document.body) return false;
try {
// 检查特定网站配置是否强制使用Yandex
if (currentRussianSiteConfig && currentRussianSiteConfig.alwaysUseYandex) {
console.log('检测到强制使用Yandex的俄语网站');
return true;
}
// 获取页面主要内容的文本
const mainContentSelectors = [
'main', 'article', '.content', '#content', '.main-content',
'.post-content', '.entry-content', '.text', '.article',
'body'
];
let contentText = '';
// 尝试从主要选择器获取内容
for (const selector of mainContentSelectors) {
const element = document.querySelector(selector);
if (element && element.textContent && element.textContent.length > 100) {
contentText = element.textContent;
break;
}
}
// 如果没有找到足够的内容,使用body
if (!contentText || contentText.length < 100) {
contentText = document.body.textContent || '';
}
const visibleText = contentText.replace(/\s+/g, ' ').trim();
// 如果文本太短,无法准确判断
if (visibleText.length < 50) return false;
const russianRatio = getRussianCharacterRatio(visibleText);
russianContentRatio = russianRatio;
// 记录检测样本
russianDetectionSamples++;
console.log(`俄语内容检测: 字符数 ${visibleText.length}, 俄语比例 ${(russianRatio * 100).toFixed(1)}%, 样本数 ${russianDetectionSamples}`);
// 如果俄语字符比例超过阈值,认为是俄语内容
if (russianRatio >= CONFIG.russianDetectionThreshold) {
console.log(`检测到俄语内容: 俄语字符比例 ${(russianRatio * 100).toFixed(1)}%`);
// 检查是否有俄语关键词
const textLower = visibleText.toLowerCase();
let russianKeywordCount = 0;
for (const keyword of CONFIG.russianKeywords) {
if (textLower.includes(keyword.toLowerCase())) {
russianKeywordCount++;
console.log(`发现俄语关键词: ${keyword}`);
}
}
// 如果有多个俄语关键词,确认是俄语网站
if (russianKeywordCount >= 2) {
console.log(`检测到 ${russianKeywordCount} 个俄语关键词,确认为俄语网站`);
return true;
}
// 如果俄语比例较高,也认为是俄语网站
if (russianRatio >= 0.25) {
console.log(`俄语比例较高(${(russianRatio * 100).toFixed(1)}%),确认为俄语网站`);
return true;
}
return false;
} else {
console.log(`俄语比例不足(${(russianRatio * 100).toFixed(1)}%),非俄语网站`);
return false;
}
} catch (error) {
console.error('检测俄语内容时出错:', error);
return false;
}
}
// 快速检测是否为俄语网站
function quickDetectRussianSite() {
currentDomain = getCurrentDomain();
let isRussianDomain = false;
for (const suffix of CONFIG.russianDomains) {
if (currentDomain.includes(suffix)) {
isRussianDomain = true;
console.log(`检测到俄语域名后缀: ${suffix}`);
break;
}
}
const htmlLang = document.documentElement.lang;
const isRussianLang = htmlLang && (htmlLang.startsWith('ru') || htmlLang.startsWith('RU'));
if (isRussianLang) console.log('检测到HTML语言标签为俄语');
const metaLang = document.querySelector('meta[http-equiv="Content-Language"]');
const isRussianMeta = metaLang && (metaLang.content.startsWith('ru') || metaLang.content.startsWith('RU'));
if (isRussianMeta) console.log('检测到meta语言标签为俄语');
return isRussianDomain || isRussianLang || isRussianMeta;
}
// 获取网站专用引擎配置
function getSiteSpecificEngines() {
try {
const enginesStr = GM_getValue(CONFIG.siteSpecificEnginesKey, '{}');
return JSON.parse(enginesStr);
} catch (e) {
console.error('读取网站专用引擎配置失败:', e);
return {};
}
}
// 保存网站专用引擎配置
function saveSiteSpecificEngines(engines) {
try {
siteSpecificEngines = engines;
GM_setValue(CONFIG.siteSpecificEnginesKey, JSON.stringify(engines));
} catch (e) {
console.error('保存网站专用引擎配置失败:', e);
}
}
// 检查当前网站是否有专用引擎
function checkSiteSpecificEngine() {
const domain = getCurrentDomain();
isUsingSiteSpecificEngine = false;
siteSpecificEngineName = '';
if (siteSpecificEngines[domain]) {
isUsingSiteSpecificEngine = true;
siteSpecificEngineName = siteSpecificEngines[domain];
console.log(`当前网站有专用引擎: ${siteSpecificEngineName}`);
return siteSpecificEngines[domain];
}
return null;
}
// 为当前网站设置专用引擎
function setSiteSpecificEngine(engine) {
const domain = getCurrentDomain();
siteSpecificEngines[domain] = engine;
saveSiteSpecificEngines(siteSpecificEngines);
isUsingSiteSpecificEngine = true;
siteSpecificEngineName = engine;
console.log(`为网站 ${domain} 设置专用引擎: ${engine}`);
}
// 清除当前网站的专用引擎
function clearSiteSpecificEngine() {
const domain = getCurrentDomain();
if (siteSpecificEngines[domain]) {
delete siteSpecificEngines[domain];
saveSiteSpecificEngines(siteSpecificEngines);
isUsingSiteSpecificEngine = false;
siteSpecificEngineName = '';
console.log(`清除网站 ${domain} 的专用引擎`);
}
}
// 获取当前应使用的引擎(考虑网站专用引擎)
function getCurrentEngine() {
const siteEngine = checkSiteSpecificEngine();
if (siteEngine) {
return siteEngine;
}
return currentEngine;
}
// 智能引擎检测
function smartEngineDetection() {
const domain = getCurrentDomain();
// 清除之前的检测计时器
if (russianDetectionTimer) {
clearTimeout(russianDetectionTimer);
russianDetectionTimer = null;
}
// 延迟检测,确保页面内容加载完成
russianDetectionTimer = setTimeout(() => {
isRussianContent = detectRussianByContent();
console.log(`俄语内容检测结果: ${isRussianContent ? '是俄语网站' : '非俄语网站'}, 比例: ${(russianContentRatio * 100).toFixed(1)}%`);
// 更新菜单命令
registerMenuCommands();
}, 1500);
}
// 获取俄语网站配置
function getRussianSiteConfig() {
if (!isRussianSite) return null;
const domain = getCurrentDomain();
for (const [sitePattern, config] of Object.entries(CONFIG.russianSiteOptimizations)) {
if (domain.includes(sitePattern)) {
console.log(`找到俄语网站优化配置: ${sitePattern}`);
return config;
}
}
// 返回默认的俄语网站配置
return {
textExtraction: 'enhanced',
mergeShortTexts: true,
prioritizeLongTexts: true,
forceRussianSource: true,
excludeSelectors: ['code', '.code', 'pre', '.pre', 'textarea', 'input', '.mce-content-body']
};
}
// 获取白名单
function getWhitelist() {
if (cachedWhitelist !== null) {
return cachedWhitelist;
}
const whitelistStr = GM_getValue(CONFIG.whitelistKey, '[]');
try {
cachedWhitelist = JSON.parse(whitelistStr);
} catch (e) {
cachedWhitelist = [];
}
return cachedWhitelist;
}
// 保存白名单
function saveWhitelist(whitelist) {
cachedWhitelist = whitelist;
GM_setValue(CONFIG.whitelistKey, JSON.stringify(whitelist));
}
// 检查当前域名是否在白名单中
function checkWhitelist() {
const currentDomain = getCurrentDomain();
const whitelist = getWhitelist();
isWhitelistedDomain = whitelist.includes(currentDomain);
return isWhitelistedDomain;
}
// 切换当前域名的白名单状态
function toggleWhitelist() {
const currentDomain = getCurrentDomain();
let whitelist = getWhitelist();
if (whitelist.includes(currentDomain)) {
whitelist = whitelist.filter(domain => domain !== currentDomain);
showStatusMessage(`已从白名单中移除: ${currentDomain}`, 2000);
isWhitelistedDomain = false;
} else {
whitelist.push(currentDomain);
showStatusMessage(`已添加到白名单: ${currentDomain}`, 2000);
isWhitelistedDomain = true;
}
saveWhitelist(whitelist);
// 立即更新翻译状态
if (widgetEnabled || isWhitelistedDomain) {
console.log('白名单变更,立即更新翻译状态');
updateWidgetBasedOnSettings();
// 如果是微软翻译且已启用,立即开始翻译
const engineToUse = getCurrentEngine();
if (engineToUse === CONFIG.ENGINES.MICROSOFT && microsoftTranslatorActive) {
console.log('白名单开启,立即开始翻译');
scheduleImmediateTranslation();
}
} else {
deactivateAllTranslators();
}
registerMenuCommands();
}
// 立即翻译调度
function scheduleImmediateTranslation() {
if (immediateTranslationScheduled) return;
immediateTranslationScheduled = true;
setTimeout(() => {
immediateTranslationScheduled = false;
if (microsoftTranslatorActive && !pageTranslating) {
console.log('调度立即翻译');
translateWholePage();
}
}, 100);
}
// 创建防护层,防止翻译脚本自身内容
function createTranslationProtectionLayer() {
const style = document.createElement('style');
style.id = 'yandex-translation-protection';
style.setAttribute('data-no-translate', 'true');
style.setAttribute('translate', 'no');
style.className = 'no-translate yandex-script-element';
style.textContent = `
/* 强制保护脚本创建的UI元素 */
body * {
transition: none !important;
}
/* 防护引擎选择器中的文本 */
.engine-btn,
.engine-btn *,
.translation-engine-option,
.translation-engine-option * {
translate: no !important;
-webkit-translate: no !important;
font-language-override: normal !important;
unicode-bidi: isolate !important;
}
/* 保护特定关键词不被翻译 */
.protect-yandex::before {
content: "Yandex";
speak: none;
}
/* 为所有脚本元素添加额外防护 */
.yandex-translation-protected {
unicode-bidi: isolate !important;
font-language-override: normal !important;
}
`;
document.head.appendChild(style);
console.log('已创建翻译防护层');
}
// 初始化函数
function init() {
console.log('Yandex/微软自动翻译脚本开始初始化...');
const savedState = GM_getValue(CONFIG.storageKey, CONFIG.defaultEnabled);
widgetEnabled = savedState;
// 读取用户设置
currentEngine = GM_getValue(CONFIG.translationEngineKey, CONFIG.defaultEngine);
targetLanguage = GM_getValue(CONFIG.translationLanguageKey, CONFIG.defaultTargetLanguage);
translationMode = GM_getValue(CONFIG.translationModeKey, CONFIG.defaultMode);
translationTextColor = GM_getValue(CONFIG.translationColorKey, CONFIG.defaultTextColor);
translationBgColor = GM_getValue(CONFIG.translationBgColorKey, CONFIG.defaultBgColor);
// 新增:读取翻译提示显示设置
showTranslationIndicator = GM_getValue(CONFIG.showTranslationIndicatorKey, true);
// 读取网站专用引擎配置
siteSpecificEngines = getSiteSpecificEngines();
// 检测俄语网站(基于域名)
isRussianSite = quickDetectRussianSite();
currentRussianSiteConfig = getRussianSiteConfig();
checkWhitelist();
currentDomain = getCurrentDomain();
// 检查当前网站是否有专用引擎
checkSiteSpecificEngine();
// 重置检测计数器
russianDetectionSamples = 0;
russianContentRatio = 0;
// 添加样式
addStyles();
updateColorStyles();
// 创建翻译防护层
createTranslationProtectionLayer();
// 设置页面加载完成标记
if (document.readyState === 'complete' || document.readyState === 'interactive') {
isPageLoaded = true;
pageLoaded();
} else {
window.addEventListener('load', function() {
isPageLoaded = true;
pageLoaded();
}, { once: true });
}
// 设置快捷键
setupHotkey();
// 延迟检测俄语内容
setTimeout(() => {
smartEngineDetection();
}, 1000);
// 立即注册菜单命令(不等待页面加载)
registerMenuCommands();
// 监听页面卸载事件
window.addEventListener('beforeunload', function() {
console.log('页面卸载');
if (russianDetectionTimer) {
clearTimeout(russianDetectionTimer);
russianDetectionTimer = null;
}
if (pageLoadedTimer) {
clearTimeout(pageLoadedTimer);
pageLoadedTimer = null;
}
// 停止Yandex检查
if (yandexCheckInterval) {
clearInterval(yandexCheckInterval);
yandexCheckInterval = null;
}
// 清除状态提示定时器
if (indicatorHideTimeout) {
clearTimeout(indicatorHideTimeout);
indicatorHideTimeout = null;
}
});
}
// 页面加载完成处理
function pageLoaded() {
console.log('页面加载完成,准备开始翻译');
// 再次注册菜单命令确保显示
setTimeout(() => {
registerMenuCommands();
}, 500);
// 根据当前设置立即启动翻译
const shouldTranslate = isWhitelistedDomain || widgetEnabled;
if (shouldTranslate) {
console.log('页面加载完成,立即启动翻译');
updateWidgetBasedOnSettings();
// 显示首次运行提示
if (GM_getValue('firstRun', true)) {
setTimeout(function() {
const engineToUse = getCurrentEngine();
const engineName = engineToUse === CONFIG.ENGINES.MICROSOFT ? '微软' : 'Yandex';
const languageName = CONFIG.LANGUAGES[targetLanguage]?.name || targetLanguage;
let message = `${engineName}自动翻译已启动,目标语言: ${languageName}`;
if (isUsingSiteSpecificEngine) {
message += ` (网站专用引擎)`;
}
message += `。按Ctrl+Shift+Y关闭`;
showStatusMessage(message, 2500);
GM_setValue('firstRun', false);
}, 1500);
}
}
}
// 添加CSS样式 - 修改翻译状态提示框样式为缩小版
function addStyles() {
const css = `
/* 微软翻译文本样式 - 修复文字颜色问题 */
.microsoft-translated {
position: relative;
border-radius: 3px;
transition: all 0.2s;
}
.microsoft-translated.replace-mode {
color: var(--translation-text-color, #0066cc) !important;
font-weight: 500;
}
.microsoft-translated.replace-mode:hover {
background-color: var(--translation-bg-color, rgba(0,102,204,0.1)) !important;
cursor: pointer;
}
/* 移除"译"字角标 */
.microsoft-translated.replace-mode::after {
display: none !important;
}
.microsoft-translated.dual-mode {
display: inline-flex;
flex-direction: column;
}
.microsoft-translated.dual-mode .original-text {
color: #666;
font-size: 0.9em;
text-decoration: line-through;
opacity: 0.7;
}
.microsoft-translated.dual-mode .translated-text {
color: var(--translation-text-color, #0066cc) !important;
font-weight: 500;
}
/* Yandex翻译小部件样式 */
.yandex-translate-widget {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 10000;
}
/* 翻译状态指示器 - 缩小版,与文字一样大 */
.translating-indicator {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.7); /* 半透明背景 */
color: #00ffcc; /* 青色文字 */
padding: 4px 8px; /* 减小内边距 */
border-radius: 8px; /* 减小圆角 */
z-index: 2147483647;
font-family: Arial, sans-serif;
font-size: 12px; /* 减小字体大小 */
display: flex;
align-items: center;
gap: 6px; /* 减小间隙 */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); /* 减小阴影 */
backdrop-filter: blur(10px); /* 毛玻璃效果 */
border: 1px solid rgba(0, 255, 204, 0.3); /* 青色边框 */
min-width: 100px; /* 减小最小宽度 */
pointer-events: none;
height: auto;
box-sizing: border-box;
opacity: 0.9;
transition: opacity 0.3s, background 0.3s, color 0.3s;
}
.translating-indicator .spinner {
width: 12px; /* 减小spinner大小 */
height: 12px; /* 减小spinner大小 */
border: 2px solid rgba(0, 255, 204, 0.5);
border-top: 2px solid #00ffcc;
border-radius: 50%;
animation: spin 1.2s linear infinite;
flex-shrink: 0;
}
.translating-indicator span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
font-size: 12px; /* 确保文字大小与指示器一致 */
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 状态消息 */
#yandex-status-msg {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 12px 20px;
border-radius: 10px;
z-index: 2147483646;
font-family: Arial, sans-serif;
font-size: 14px;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
opacity: 1;
transform: translateY(0);
max-width: 300px;
word-wrap: break-word;
}
#yandex-status-msg.fade-out {
opacity: 0;
transform: translateY(-20px);
}
/* 设置项样式 */
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 8px;
background-color: #f9f9f9;
border-radius: 6px;
}
.setting-label {
flex: 1;
}
.setting-title {
font-weight: bold;
color: #333;
margin-bottom: 2px;
font-size: 13px;
}
.setting-description {
color: #666;
font-size: 11px;
line-height: 1.3;
}
/* 开关样式 */
.switch {
position: relative;
display: inline-block;
width: 45px;
height: 22px;
flex-shrink: 0;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 22px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #0066cc;
}
input:checked + .slider:before {
transform: translateX(23px);
}
input:focus + .slider {
box-shadow: 0 0 1px #0066cc;
}
/* 网站专用引擎标签 */
.site-engine-badge {
display: inline-block;
background-color: #00b894;
color: white;
font-size: 9px;
padding: 1px 4px;
border-radius: 8px;
margin-left: 5px;
font-weight: bold;
vertical-align: middle;
}
/* 引擎选择按钮 */
.engine-btn {
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
gap: 5px;
transition: all 0.2s;
flex: 1;
justify-content: center;
}
.engine-btn.active {
border-color: #0066cc;
background-color: #f0f7ff;
}
.engine-btn:hover:not(.active) {
border-color: #999;
background-color: #f9f9f9;
}
/* 紧凑的设置项 */
.compact-setting {
margin-bottom: 10px;
padding: 8px;
background-color: #f8f9fa;
border-radius: 5px;
border-left: 3px solid #6c5ce7;
}
.compact-setting-title {
font-weight: bold;
color: #333;
margin-bottom: 5px;
font-size: 13px;
}
.compact-setting-desc {
color: #666;
font-size: 11px;
margin-bottom: 8px;
line-height: 1.3;
}
/* 紧凑按钮 */
.compact-btn {
padding: 5px 8px;
font-size: 11px;
border-radius: 4px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.compact-btn:hover {
border-color: #999;
background-color: #f5f5f5;
}
.compact-btn-primary {
background-color: #0066cc;
color: white;
border-color: #0066cc;
}
.compact-btn-primary:hover {
background-color: #0052a3;
}
/* 紧凑输入框 */
.compact-input {
padding: 5px 8px;
font-size: 12px;
border: 1px solid #ddd;
border-radius: 4px;
width: 100%;
box-sizing: border-box;
}
/* 增强防护:确保所有脚本相关元素不被翻译 */
[data-no-translate],
.no-translate,
.yandex-no-translate,
.yandex-translate-widget,
.yandex-translate-widget *,
.microsoft-translated,
.microsoft-translated *,
.translating-indicator,
.translating-indicator *,
#yandex-status-msg,
#yandex-status-msg *,
.translation-settings-modal,
.translation-settings-modal *,
.tm-script-menu,
.tm-script-menu *,
.vm-script-menu,
.vm-script-menu *,
.gm-script-menu,
.gm-script-menu *,
[class*="userscript"],
[class*="userscript"] *,
[id*="userscript"],
[id*="userscript"] *,
.yandex-script-element,
.yandex-script-element * {
translate: no !important;
-webkit-translate: no !important;
-moz-translate: no !important;
-ms-translate: no !important;
-o-translate: no !important;
font-language-override: normal !important;
text-orientation: mixed !important;
}
/* 防止特定文本被翻译 */
.engine-btn,
.engine-btn *,
.site-engine-badge,
.site-engine-badge * {
translate: no !important;
font-language-override: normal !important;
}
/* 确保"Yandex"文本不被翻译 */
.yandex-protected-text,
.yandex-protected-text * {
translate: no !important;
-webkit-translate: no !important;
font-language-override: normal !important;
unicode-bidi: isolate !important;
}
/* 为特定元素添加额外的防护 */
[data-protect-translation="true"],
[data-protect-translation="true"] * {
translate: no !important;
-webkit-translate: no !important;
font-language-override: normal !important;
}
`;
GM_addStyle(css);
}
// 更新颜色样式 - 修复文字颜色功能
function updateColorStyles() {
const oldStyle = document.querySelector('#translation-color-styles');
if (oldStyle) oldStyle.remove();
const style = document.createElement('style');
style.id = 'translation-color-styles';
style.textContent = `
:root {
--translation-text-color: ${translationTextColor};
--translation-bg-color: ${translationBgColor};
}
/* 直接应用颜色到翻译元素 */
.microsoft-translated.replace-mode {
color: ${translationTextColor} !important;
}
.microsoft-translated.replace-mode:hover {
background-color: ${translationBgColor} !important;
}
.microsoft-translated.dual-mode .translated-text {
color: ${translationTextColor} !important;
}
/* 更新现有翻译元素的颜色 */
.microsoft-translated.replace-mode,
.microsoft-translated.dual-mode .translated-text {
color: ${translationTextColor} !important;
}
`;
document.head.appendChild(style);
console.log('已更新翻译颜色样式:', translationTextColor);
}
// 根据设置更新翻译功能
function updateWidgetBasedOnSettings() {
const engineToUse = getCurrentEngine();
console.log('更新翻译设置,当前状态:', {
widgetEnabled,
isWhitelistedDomain,
mainEngine: currentEngine,
siteSpecificEngine: engineToUse !== currentEngine ? engineToUse : '无',
isUsingSiteSpecificEngine,
isRussianContent,
russianContentRatio: (russianContentRatio * 100).toFixed(1) + '%'
});
if (autoTranslateTimeout) {
clearTimeout(autoTranslateTimeout);
autoTranslateTimeout = null;
}
stopAllObservations();
// 停止Yandex检查
if (yandexCheckInterval) {
clearInterval(yandexCheckInterval);
yandexCheckInterval = null;
}
const shouldShowWidget = isWhitelistedDomain || widgetEnabled;
if (shouldShowWidget) {
if (engineToUse === CONFIG.ENGINES.YANDEX) {
console.log('启用Yandex翻译器');
initYandexTranslator();
} else if (engineToUse === CONFIG.ENGINES.MICROSOFT) {
console.log('启用微软翻译器');
initMicrosoftTranslator();
}
} else {
console.log('翻译已禁用');
deactivateAllTranslators();
}
// 更新菜单命令
registerMenuCommands();
}
// 初始化Yandex翻译器 - 增强版,确保持续工作
function initYandexTranslator() {
if (microsoftTranslatorActive) {
deactivateMicrosoftTranslator();
}
if (!widgetContainer) {
createYandexWidget();
}
// 启动Yandex翻译状态检查
startYandexTranslationCheck();
yandexTranslationActive = true;
yandexTranslationAttempts = 0;
// Yandex翻译时也显示状态提示(根据设置)
if (showTranslationIndicator) {
showTranslatingIndicator('Yandex翻译已启动');
}
}
// 启动Yandex翻译状态检查
function startYandexTranslationCheck() {
if (yandexCheckInterval) {
clearInterval(yandexCheckInterval);
}
console.log('启动Yandex翻译状态检查');
// 立即检查一次
checkAndActivateYandexTranslation();
// 设置定时检查
yandexCheckInterval = setInterval(() => {
checkAndActivateYandexTranslation();
}, CONFIG.YANDEX_CHECK_INTERVAL);
}
// 检查和激活Yandex翻译
function checkAndActivateYandexTranslation() {
const engineToUse = getCurrentEngine();
const shouldShowWidget = isWhitelistedDomain || widgetEnabled;
// 如果当前不应该显示翻译,或者不是Yandex引擎,则停止检查
if (!shouldShowWidget || engineToUse !== CONFIG.ENGINES.YANDEX) {
if (yandexCheckInterval) {
clearInterval(yandexCheckInterval);
yandexCheckInterval = null;
}
return;
}
// 检查Yandex翻译是否正常工作
if (!isYandexTranslatorActive()) {
console.log('Yandex翻译器未激活,尝试激活...');
yandexTranslationAttempts++;
if (yandexTranslationAttempts > yandexMaxAttempts) {
console.log('Yandex翻译激活尝试次数过多,停止尝试');
if (yandexCheckInterval) {
clearInterval(yandexCheckInterval);
yandexCheckInterval = null;
}
return;
}
// 尝试重新激活Yandex翻译
reactivateYandexTranslator();
} else {
// 重置尝试次数
yandexTranslationAttempts = 0;
}
}
// 检查Yandex翻译是否激活
function isYandexTranslatorActive() {
if (!widgetContainer) return false;
// 检查是否在页面上看到翻译相关的元素
// Yandex翻译会在页面上添加特定的类或属性
const yandexElements = document.querySelectorAll('[data-translate], .translated-ltr, .translated-rtl, .goog-te-banner');
// 检查页面文本是否包含翻译
const pageText = document.body ? document.body.textContent || '' : '';
const hasTranslatedContent = pageText.includes('翻译') ||
pageText.length > 1000; // 假设有内容就是翻译了
return yandexElements.length > 0 || hasTranslatedContent;
}
// 重新激活Yandex翻译器
function reactivateYandexTranslator() {
console.log('重新激活Yandex翻译器');
// 如果Yandex小部件不存在,重新创建
if (!widgetContainer || !document.body.contains(widgetContainer)) {
console.log('Yandex小部件不存在,重新创建');
removeYandexWidget();
createYandexWidget();
return;
}
// 尝试点击翻译按钮(如果存在)
const translateButtons = document.querySelectorAll('[data-lang]');
if (translateButtons.length > 0) {
console.log('找到翻译按钮,尝试点击');
for (const button of translateButtons) {
if (button.textContent.includes('翻译') || button.getAttribute('data-lang') === 'zh') {
button.click();
showStatusMessage('重新激活Yandex翻译', 2000);
return;
}
}
}
// 如果页面上有Yandex翻译框架,尝试触发翻译
const yandexIframe = document.querySelector('iframe[src*="translate.yandex"]');
if (yandexIframe) {
console.log('找到Yandex翻译iframe');
// 重新加载iframe
yandexIframe.src = yandexIframe.src;
showStatusMessage('重新加载Yandex翻译', 2000);
}
}
// 初始化微软翻译器
async function initMicrosoftTranslator() {
if (widgetContainer) {
removeYandexWidget();
}
// 停止Yandex检查
if (yandexCheckInterval) {
clearInterval(yandexCheckInterval);
yandexCheckInterval = null;
}
await setupMicrosoftTranslator();
if (widgetEnabled || isWhitelistedDomain) {
console.log('微软翻译器就绪,立即开始翻译');
// 显示翻译状态提示(根据设置)
if (showTranslationIndicator) {
showTranslatingIndicator('正在翻译页面...');
}
// 立即开始翻译,减少延迟
if (isPageLoaded) {
setTimeout(() => {
translateWholePage();
}, 50);
} else {
// 如果页面还没加载完成,等待一小会儿再开始
autoTranslateTimeout = setTimeout(() => {
translateWholePage();
}, CONFIG.AUTO_TRANSLATE_DELAY);
}
}
startDOMObservation();
startContinuousTranslation();
}
// 停用所有翻译器
function deactivateAllTranslators() {
if (autoTranslateTimeout) {
clearTimeout(autoTranslateTimeout);
autoTranslateTimeout = null;
}
stopAllObservations();
removeYandexWidget();
deactivateMicrosoftTranslator();
// 停止Yandex检查
if (yandexCheckInterval) {
clearInterval(yandexCheckInterval);
yandexCheckInterval = null;
}
// 隐藏翻译状态提示
hideTranslatingIndicator();
yandexTranslationActive = false;
yandexTranslationAttempts = 0;
console.log('所有翻译器已停用');
}
// 停止所有观察器
function stopAllObservations() {
stopDOMObservation();
stopScrollCheck();
stopContinuousTranslation();
}
// 注册菜单命令 - 修复版,确保菜单总是显示
function registerMenuCommands() {
// 先清除之前的菜单命令
unregisterMenuCommands();
const engineToUse = getCurrentEngine();
const engineName = engineToUse === CONFIG.ENGINES.MICROSOFT ? '微软' : 'Yandex';
let mainMenuText = widgetEnabled ? '❌ 关闭自动翻译' : '✅ 开启自动翻译';
mainMenuText += ` (${engineName})`;
if (isRussianContent) mainMenuText += ' 🇷🇺';
if (isUsingSiteSpecificEngine) mainMenuText += ' 🌐';
try {
mainMenuCommandId = GM_registerMenuCommand(mainMenuText, toggleWidget, 't');
console.log('主菜单命令注册成功:', mainMenuText);
} catch (e) {
console.error('主菜单命令注册失败:', e);
}
const engineText = engineToUse === CONFIG.ENGINES.MICROSOFT ?
'🔵 切换到Yandex翻译' : '🔴 切换到微软翻译';
try {
engineMenuCommandId = GM_registerMenuCommand(engineText, toggleTranslationEngine, 'e');
console.log('引擎菜单命令注册成功:', engineText);
} catch (e) {
console.error('引擎菜单命令注册失败:', e);
}
const whitelistText = isWhitelistedDomain ?
`⭐ 从白名单中移除: ${currentDomain}` :
`☆ 添加到白名单: ${currentDomain}`;
try {
whitelistMenuCommandId = GM_registerMenuCommand(whitelistText, toggleWhitelist, 'a');
console.log('白名单菜单命令注册成功:', whitelistText);
} catch (e) {
console.error('白名单菜单命令注册失败:', e);
}
try {
settingsMenuCommandId = GM_registerMenuCommand('⚙️ 翻译设置', showSettings, 's');
console.log('设置菜单命令注册成功');
} catch (e) {
console.error('设置菜单命令注册失败:', e);
}
console.log('菜单命令注册完成');
}
// 注销菜单命令
function unregisterMenuCommands() {
try {
if (mainMenuCommandId !== null) {
GM_unregisterMenuCommand(mainMenuCommandId);
mainMenuCommandId = null;
}
if (whitelistMenuCommandId !== null) {
GM_unregisterMenuCommand(whitelistMenuCommandId);
whitelistMenuCommandId = null;
}
if (engineMenuCommandId !== null) {
GM_unregisterMenuCommand(engineMenuCommandId);
engineMenuCommandId = null;
}
if (settingsMenuCommandId !== null) {
GM_unregisterMenuCommand(settingsMenuCommandId);
settingsMenuCommandId = null;
}
} catch (e) {
console.error('注销菜单命令失败:', e);
}
}
// 显示设置窗口
function showSettings() {
isModalOpen = true;
// 获取当前模态框的滚动位置(如果存在)
const existingContent = document.querySelector('.translation-settings-content');
let scrollTopToRestore = 0;
if (existingContent) {
scrollTopToRestore = existingContent.scrollTop;
// 移除现有的模态框
const existingModal = existingContent.closest('.translation-settings-modal');
if (existingModal) {
existingModal.remove();
}
}
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2147483647;
display: flex;
justify-content: center;
align-items: center;
font-family: Arial, sans-serif;
`;
modal.setAttribute('data-no-translate', 'true');
modal.setAttribute('translate', 'no');
modal.setAttribute('data-protect-translation', 'true');
modal.className = 'no-translate yandex-no-translate translation-settings-modal yandex-script-element';
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background-color: white;
padding: 20px;
border-radius: 10px;
max-width: 480px;
width: 90%;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
font-size: 13px;
`;
modalContent.setAttribute('data-no-translate', 'true');
modalContent.setAttribute('translate', 'no');
modalContent.setAttribute('data-protect-translation', 'true');
modalContent.className = 'no-translate yandex-no-translate translation-settings-content yandex-script-element';
const title = document.createElement('h3');
title.textContent = '翻译设置';
title.style.cssText = `
margin-top: 0;
margin-bottom: 15px;
color: #333;
text-align: center;
font-size: 16px;
`;
title.setAttribute('data-no-translate', 'true');
title.setAttribute('translate', 'no');
title.setAttribute('data-protect-translation', 'true');
title.className = 'no-translate';
modalContent.appendChild(title);
// 当前状态显示
const statusInfo = document.createElement('div');
statusInfo.style.cssText = `
margin-bottom: 15px;
padding: 10px;
background-color: #f0f7ff;
border-radius: 6px;
border-left: 3px solid #0066cc;
font-size: 12px;
`;
statusInfo.setAttribute('data-no-translate', 'true');
statusInfo.setAttribute('translate', 'no');
statusInfo.setAttribute('data-protect-translation', 'true');
statusInfo.className = 'no-translate';
const engineToUse = getCurrentEngine();
const engineName = engineToUse === CONFIG.ENGINES.MICROSOFT ? '微软翻译' : 'Yandex翻译';
let statusHTML = `
当前状态
${engineName}`;
if (isUsingSiteSpecificEngine) {
statusHTML += ` 网站专用`;
}
statusHTML += `
`;
if (isRussianSite) {
statusHTML += `${isRussianContent ? '检测到俄语内容 🇷🇺' : '俄语域名'}
`;
}
statusHTML += `默认引擎: ${currentEngine === CONFIG.ENGINES.MICROSOFT ? '微软翻译' : 'Yandex翻译'}
`;
if (isRussianContent && engineToUse === CONFIG.ENGINES.MICROSOFT) {
statusHTML += `⚠️ 建议使用Yandex翻译
`;
}
if (engineToUse === CONFIG.ENGINES.YANDEX) {
statusHTML += `✓ Yandex翻译将持续工作
`;
}
statusInfo.innerHTML = statusHTML;
modalContent.appendChild(statusInfo);
// 新增:翻译提示开关设置
const indicatorSection = document.createElement('div');
indicatorSection.className = 'compact-setting';
indicatorSection.setAttribute('data-no-translate', 'true');
indicatorSection.setAttribute('translate', 'no');
indicatorSection.setAttribute('data-protect-translation', 'true');
indicatorSection.className += ' no-translate';
const indicatorTitle = document.createElement('div');
indicatorTitle.className = 'compact-setting-title';
indicatorTitle.textContent = '翻译状态提示';
indicatorTitle.setAttribute('data-no-translate', 'true');
indicatorTitle.setAttribute('translate', 'no');
indicatorTitle.setAttribute('data-protect-translation', 'true');
indicatorSection.appendChild(indicatorTitle);
const indicatorDesc = document.createElement('div');
indicatorDesc.className = 'compact-setting-desc';
indicatorDesc.textContent = '控制是否显示"正在翻译页面..."等翻译状态提示';
indicatorDesc.setAttribute('data-no-translate', 'true');
indicatorDesc.setAttribute('translate', 'no');
indicatorDesc.setAttribute('data-protect-translation', 'true');
indicatorSection.appendChild(indicatorDesc);
const indicatorSwitch = document.createElement('div');
indicatorSwitch.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
background-color: #f9f9f9;
border-radius: 6px;
`;
const indicatorLabel = document.createElement('div');
indicatorLabel.style.cssText = `
flex: 1;
`;
const indicatorLabelTitle = document.createElement('div');
indicatorLabelTitle.className = 'setting-title';
indicatorLabelTitle.textContent = '显示翻译状态提示';
indicatorLabelTitle.setAttribute('data-no-translate', 'true');
indicatorLabelTitle.setAttribute('translate', 'no');
indicatorLabelTitle.setAttribute('data-protect-translation', 'true');
const indicatorLabelDesc = document.createElement('div');
indicatorLabelDesc.className = 'setting-description';
indicatorLabelDesc.textContent = '开启后会在翻译时显示"正在翻译页面..."和"翻译完成"等提示';
indicatorLabelDesc.setAttribute('data-no-translate', 'true');
indicatorLabelDesc.setAttribute('translate', 'no');
indicatorLabelDesc.setAttribute('data-protect-translation', 'true');
indicatorLabel.appendChild(indicatorLabelTitle);
indicatorLabel.appendChild(indicatorLabelDesc);
const switchContainer = document.createElement('label');
switchContainer.className = 'switch';
const switchInput = document.createElement('input');
switchInput.type = 'checkbox';
switchInput.checked = showTranslationIndicator;
switchInput.setAttribute('data-no-translate', 'true');
switchInput.setAttribute('translate', 'no');
switchInput.setAttribute('data-protect-translation', 'true');
const switchSlider = document.createElement('span');
switchSlider.className = 'slider';
switchSlider.setAttribute('data-no-translate', 'true');
switchSlider.setAttribute('translate', 'no');
switchSlider.setAttribute('data-protect-translation', 'true');
switchContainer.appendChild(switchInput);
switchContainer.appendChild(switchSlider);
switchInput.addEventListener('change', function() {
showTranslationIndicator = this.checked;
GM_setValue(CONFIG.showTranslationIndicatorKey, showTranslationIndicator);
// 如果关闭了提示,立即隐藏当前提示
if (!showTranslationIndicator) {
hideTranslatingIndicator();
}
showStatusMessage(`翻译状态提示 ${showTranslationIndicator ? '已开启' : '已关闭'}`, 2000);
});
indicatorSwitch.appendChild(indicatorLabel);
indicatorSwitch.appendChild(switchContainer);
indicatorSection.appendChild(indicatorSwitch);
modalContent.appendChild(indicatorSection);
// 网站专用引擎设置
const siteEngineSection = document.createElement('div');
siteEngineSection.className = 'compact-setting';
siteEngineSection.setAttribute('data-no-translate', 'true');
siteEngineSection.setAttribute('translate', 'no');
siteEngineSection.setAttribute('data-protect-translation', 'true');
siteEngineSection.className += ' no-translate';
const siteEngineTitle = document.createElement('div');
siteEngineTitle.className = 'compact-setting-title';
siteEngineTitle.textContent = '网站专用引擎';
siteEngineTitle.setAttribute('data-no-translate', 'true');
siteEngineTitle.setAttribute('translate', 'no');
siteEngineTitle.setAttribute('data-protect-translation', 'true');
siteEngineSection.appendChild(siteEngineTitle);
const siteEngineDesc = document.createElement('div');
siteEngineDesc.className = 'compact-setting-desc';
siteEngineDesc.textContent = '为当前网站设置专用翻译引擎,该设置会覆盖默认引擎';
siteEngineDesc.setAttribute('data-no-translate', 'true');
siteEngineDesc.setAttribute('translate', 'no');
siteEngineDesc.setAttribute('data-protect-translation', 'true');
siteEngineSection.appendChild(siteEngineDesc);
// 当前网站状态
const currentDomain = getCurrentDomain();
const isSiteHasEngine = isUsingSiteSpecificEngine;
const currentSiteInfo = document.createElement('div');
currentSiteInfo.style.cssText = `
margin-bottom: 8px;
padding: 6px;
background-color: ${isSiteHasEngine ? '#e8f5e9' : '#f5f5f5'};
border-radius: 4px;
font-size: 12px;
`;
currentSiteInfo.setAttribute('data-no-translate', 'true');
currentSiteInfo.setAttribute('translate', 'no');
currentSiteInfo.setAttribute('data-protect-translation', 'true');
currentSiteInfo.className = 'no-translate';
currentSiteInfo.innerHTML = `
${currentDomain}
专用引擎: ${isSiteHasEngine ? (siteSpecificEngineName === CONFIG.ENGINES.MICROSOFT ? '微软翻译' : 'Yandex翻译') : '无'}
`;
siteEngineSection.appendChild(currentSiteInfo);
// 引擎选择按钮
const engineSelection = document.createElement('div');
engineSelection.style.cssText = `
display: flex;
gap: 6px;
margin-bottom: 8px;
`;
// 微软引擎按钮
const microsoftBtn = document.createElement('button');
microsoftBtn.className = 'engine-btn';
if (isSiteHasEngine && siteSpecificEngineName === CONFIG.ENGINES.MICROSOFT) {
microsoftBtn.classList.add('active');
}
microsoftBtn.innerHTML = `
🔵
微软
`;
microsoftBtn.setAttribute('data-no-translate', 'true');
microsoftBtn.setAttribute('translate', 'no');
microsoftBtn.setAttribute('data-protect-translation', 'true');
microsoftBtn.className += ' no-translate yandex-script-element';
microsoftBtn.addEventListener('click', function() {
setSiteSpecificEngine(CONFIG.ENGINES.MICROSOFT);
showStatusMessage('已设置当前网站专用引擎: 微软翻译', 2000);
showSettings(); // 直接重新打开,不关闭
});
// Yandex引擎按钮 - 增强防护
const yandexBtn = document.createElement('button');
yandexBtn.className = 'engine-btn';
if (isSiteHasEngine && siteSpecificEngineName === CONFIG.ENGINES.YANDEX) {
yandexBtn.classList.add('active');
}
// 使用span元素包裹"Yandex"文本,并添加防护
yandexBtn.innerHTML = `
🔴
Yandex
`;
// 添加所有防护属性
yandexBtn.setAttribute('data-no-translate', 'true');
yandexBtn.setAttribute('translate', 'no');
yandexBtn.setAttribute('data-protect-translation', 'true');
yandexBtn.className += ' no-translate yandex-script-element';
yandexBtn.addEventListener('click', function() {
setSiteSpecificEngine(CONFIG.ENGINES.YANDEX);
showStatusMessage('已设置当前网站专用引擎: Yandex翻译', 2000);
showSettings(); // 直接重新打开,不关闭
});
// 清除按钮
const clearBtn = document.createElement('button');
clearBtn.textContent = '清除专用引擎';
clearBtn.style.cssText = `
padding: 5px 8px;
background-color: ${isSiteHasEngine ? '#ff4757' : '#ccc'};
color: white;
border: none;
border-radius: 4px;
cursor: ${isSiteHasEngine ? 'pointer' : 'not-allowed'};
font-size: 11px;
transition: background-color 0.2s;
width: 100%;
`;
clearBtn.setAttribute('data-no-translate', 'true');
clearBtn.setAttribute('translate', 'no');
clearBtn.setAttribute('data-protect-translation', 'true');
clearBtn.className += ' no-translate yandex-script-element';
if (isSiteHasEngine) {
clearBtn.addEventListener('click', function() {
clearSiteSpecificEngine();
showStatusMessage('已清除当前网站专用引擎', 2000);
showSettings(); // 直接重新打开,不关闭
});
}
engineSelection.appendChild(microsoftBtn);
engineSelection.appendChild(yandexBtn);
siteEngineSection.appendChild(engineSelection);
siteEngineSection.appendChild(clearBtn);
modalContent.appendChild(siteEngineSection);
// 目标语言设置
const languageSection = document.createElement('div');
languageSection.style.cssText = `
margin-bottom: 15px;
`;
languageSection.setAttribute('data-no-translate', 'true');
languageSection.setAttribute('translate', 'no');
languageSection.setAttribute('data-protect-translation', 'true');
languageSection.className = 'no-translate';
const languageLabel = document.createElement('div');
languageLabel.textContent = '目标语言:';
languageLabel.style.cssText = `
font-weight: bold;
color: #555;
margin-bottom: 8px;
font-size: 13px;
`;
languageLabel.setAttribute('data-no-translate', 'true');
languageLabel.setAttribute('translate', 'no');
languageLabel.setAttribute('data-protect-translation', 'true');
languageSection.appendChild(languageLabel);
const languageGrid = document.createElement('div');
languageGrid.style.cssText = `
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 6px;
`;
languageGrid.setAttribute('data-no-translate', 'true');
languageGrid.setAttribute('translate', 'no');
languageGrid.setAttribute('data-protect-translation', 'true');
languageGrid.className = 'no-translate';
Object.values(CONFIG.LANGUAGES).forEach(lang => {
const languageBtn = document.createElement('button');
languageBtn.style.cssText = `
padding: 8px;
border: ${lang.code === targetLanguage ? '2px solid #0066cc' : '1px solid #ddd'};
border-radius: 5px;
background: ${lang.code === targetLanguage ? '#f0f7ff' : 'white'};
cursor: pointer;
font-size: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
transition: all 0.2s;
`;
languageBtn.setAttribute('data-no-translate', 'true');
languageBtn.setAttribute('translate', 'no');
languageBtn.setAttribute('data-protect-translation', 'true');
languageBtn.className = 'no-translate yandex-script-element';
languageBtn.innerHTML = `
${lang.icon}
${lang.name}
`;
languageBtn.addEventListener('click', function() {
if (lang.code !== targetLanguage) {
targetLanguage = lang.code;
GM_setValue(CONFIG.translationLanguageKey, targetLanguage);
// 清除翻译缓存
translationCache.clear();
// 重新打开设置窗口
showSettings();
// 立即重新翻译
const engineToUse = getCurrentEngine();
if (engineToUse === CONFIG.ENGINES.MICROSOFT && microsoftTranslatorActive && (widgetEnabled || isWhitelistedDomain)) {
showStatusMessage(`目标语言: ${lang.name},立即重新翻译...`, 2000);
scheduleImmediateTranslation();
} else {
showStatusMessage(`目标语言: ${lang.name}`, 2000);
}
}
});
languageBtn.addEventListener('mouseenter', function() {
if (lang.code !== targetLanguage) {
this.style.borderColor = '#999';
this.style.backgroundColor = '#f9f9f9';
}
});
languageBtn.addEventListener('mouseleave', function() {
if (lang.code !== targetLanguage) {
this.style.borderColor = '#ddd';
this.style.backgroundColor = 'white';
}
});
languageGrid.appendChild(languageBtn);
});
languageSection.appendChild(languageGrid);
modalContent.appendChild(languageSection);
// 翻译文字颜色设置 - 增强版
const colorSection = document.createElement('div');
colorSection.style.cssText = `
margin-bottom: 20px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 8px;
border-left: 3px solid ${translationTextColor};
`;
colorSection.setAttribute('data-no-translate', 'true');
colorSection.setAttribute('translate', 'no');
colorSection.setAttribute('data-protect-translation', 'true');
colorSection.className = 'no-translate';
const colorLabel = document.createElement('div');
colorLabel.textContent = '翻译文字颜色:';
colorLabel.style.cssText = `
font-weight: bold;
color: #555;
margin-bottom: 10px;
font-size: 14px;
`;
colorLabel.setAttribute('data-no-translate', 'true');
colorLabel.setAttribute('translate', 'no');
colorLabel.setAttribute('data-protect-translation', 'true');
colorSection.appendChild(colorLabel);
// 当前颜色预览
const colorPreview = document.createElement('div');
colorPreview.style.cssText = `
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 8px;
background-color: white;
border-radius: 6px;
border: 1px solid #ddd;
`;
const previewLabel = document.createElement('span');
previewLabel.textContent = '当前颜色预览: ';
previewLabel.style.cssText = `
margin-right: 10px;
font-size: 12px;
color: #666;
`;
const previewColor = document.createElement('span');
previewColor.textContent = '翻译文字示例';
previewColor.style.cssText = `
color: ${translationTextColor};
font-weight: bold;
padding: 4px 8px;
background-color: ${translationBgColor};
border-radius: 4px;
border: 1px solid ${translationTextColor}20;
`;
colorPreview.appendChild(previewLabel);
colorPreview.appendChild(previewColor);
colorSection.appendChild(colorPreview);
const colorGrid = document.createElement('div');
colorGrid.style.cssText = `
display: grid;
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
gap: 6px;
margin-bottom: 10px;
`;
colorGrid.setAttribute('data-no-translate', 'true');
colorGrid.setAttribute('translate', 'no');
colorGrid.setAttribute('data-protect-translation', 'true');
colorGrid.className = 'no-translate';
CONFIG.COLOR_PRESETS.forEach(preset => {
const colorBtn = document.createElement('button');
colorBtn.style.cssText = `
padding: 8px;
border: ${translationTextColor === preset.value ? '2px solid #333' : '1px solid #ddd'};
border-radius: 5px;
background: ${preset.value};
color: white;
cursor: pointer;
font-size: 11px;
text-shadow: 0 1px 1px rgba(0,0,0,0.3);
height: 35px;
position: relative;
overflow: hidden;
transition: all 0.2s;
`;
colorBtn.setAttribute('data-no-translate', 'true');
colorBtn.setAttribute('translate', 'no');
colorBtn.setAttribute('data-protect-translation', 'true');
colorBtn.className = 'no-translate yandex-script-element';
const colorName = document.createElement('div');
colorName.textContent = preset.name;
colorName.style.cssText = `
font-weight: bold;
font-size: 10px;
`;
colorBtn.appendChild(colorName);
if (translationTextColor === preset.value) {
const checkmark = document.createElement('div');
checkmark.textContent = '✓';
checkmark.style.cssText = `
position: absolute;
top: 1px;
right: 1px;
font-size: 10px;
font-weight: bold;
`;
colorBtn.appendChild(checkmark);
}
colorBtn.addEventListener('click', function() {
translationTextColor = preset.value;
translationBgColor = preset.bg;
GM_setValue(CONFIG.translationColorKey, translationTextColor);
GM_setValue(CONFIG.translationBgColorKey, translationBgColor);
updateColorStyles();
// 立即更新现有翻译元素的颜色
updateExistingTranslationColors();
showStatusMessage(`已应用颜色: ${preset.name}`, 2000);
// 重新打开设置窗口,保持滚动位置
showSettings();
});
colorGrid.appendChild(colorBtn);
});
colorSection.appendChild(colorGrid);
// 自定义颜色输入
const customColorContainer = document.createElement('div');
customColorContainer.style.cssText = `
display: flex;
gap: 8px;
margin-top: 10px;
align-items: center;
`;
const colorInput = document.createElement('input');
colorInput.type = 'color';
colorInput.value = translationTextColor;
colorInput.style.cssText = `
width: 40px;
height: 40px;
padding: 0;
border: none;
cursor: pointer;
border-radius: 4px;
`;
colorInput.setAttribute('data-no-translate', 'true');
colorInput.setAttribute('translate', 'no');
colorInput.setAttribute('data-protect-translation', 'true');
colorInput.className = 'no-translate';
const colorInputText = document.createElement('input');
colorInputText.type = 'text';
colorInputText.value = translationTextColor;
colorInputText.placeholder = '#RRGGBB';
colorInputText.style.cssText = `
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
`;
colorInputText.setAttribute('data-no-translate', 'true');
colorInputText.setAttribute('translate', 'no');
colorInputText.setAttribute('data-protect-translation', 'true');
colorInputText.className = 'no-translate yandex-script-element';
const applyColorBtn = document.createElement('button');
applyColorBtn.textContent = '应用';
applyColorBtn.style.cssText = `
padding: 8px 12px;
background-color: #0066cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
`;
applyColorBtn.setAttribute('data-no-translate', 'true');
applyColorBtn.setAttribute('translate', 'no');
applyColorBtn.setAttribute('data-protect-translation', 'true');
applyColorBtn.className = 'no-translate yandex-script-element';
applyColorBtn.addEventListener('click', function() {
const colorValue = colorInputText.value.trim();
if (/^#[0-9A-Fa-f]{6}$/.test(colorValue) || /^#[0-9A-Fa-f]{3}$/.test(colorValue)) {
translationTextColor = colorValue;
translationBgColor = colorValue.replace('#', 'rgba(') + ',0.1)';
GM_setValue(CONFIG.translationColorKey, translationTextColor);
GM_setValue(CONFIG.translationBgColorKey, translationBgColor);
updateColorStyles();
// 立即更新现有翻译元素的颜色
updateExistingTranslationColors();
showStatusMessage('自定义颜色已应用', 2000);
// 重新打开设置窗口,保持滚动位置
showSettings();
} else {
showStatusMessage('颜色格式错误,请使用#RRGGBB格式', 2000);
}
});
colorInput.addEventListener('input', function() {
colorInputText.value = this.value;
});
colorInputText.addEventListener('input', function() {
const colorValue = this.value.trim();
if (/^#[0-9A-Fa-f]{6}$/.test(colorValue) || /^#[0-9A-Fa-f]{3}$/.test(colorValue)) {
colorInput.value = colorValue;
}
});
customColorContainer.appendChild(colorInput);
customColorContainer.appendChild(colorInputText);
customColorContainer.appendChild(applyColorBtn);
colorSection.appendChild(customColorContainer);
modalContent.appendChild(colorSection);
// 显示模式设置
const modeSection = document.createElement('div');
modeSection.style.cssText = `
margin-bottom: 15px;
`;
modeSection.setAttribute('data-no-translate', 'true');
modeSection.setAttribute('translate', 'no');
modeSection.setAttribute('data-protect-translation', 'true');
modeSection.className = 'no-translate';
const modeLabel = document.createElement('div');
modeLabel.textContent = '显示模式:';
modeLabel.style.cssText = `
font-weight: bold;
color: #555;
margin-bottom: 8px;
font-size: 13px;
`;
modeLabel.setAttribute('data-no-translate', 'true');
modeLabel.setAttribute('translate', 'no');
modeLabel.setAttribute('data-protect-translation', 'true');
modeSection.appendChild(modeLabel);
const modeGrid = document.createElement('div');
modeGrid.style.cssText = `
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
`;
modeGrid.setAttribute('data-no-translate', 'true');
modeGrid.setAttribute('translate', 'no');
modeGrid.setAttribute('data-protect-translation', 'true');
modeGrid.className = 'no-translate';
// 替换模式按钮
const replaceModeBtn = document.createElement('button');
replaceModeBtn.style.cssText = `
padding: 8px;
border: ${translationMode === CONFIG.MODES.REPLACE ? '2px solid #0066cc' : '1px solid #ddd'};
border-radius: 5px;
background: ${translationMode === CONFIG.MODES.REPLACE ? '#f0f7ff' : 'white'};
cursor: pointer;
font-size: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
transition: all 0.2s;
`;
replaceModeBtn.setAttribute('data-no-translate', 'true');
replaceModeBtn.setAttribute('translate', 'no');
replaceModeBtn.setAttribute('data-protect-translation', 'true');
replaceModeBtn.className = 'no-translate yandex-script-element';
replaceModeBtn.innerHTML = `
🔁
替换原文
`;
replaceModeBtn.addEventListener('click', function() {
if (translationMode !== CONFIG.MODES.REPLACE) {
translationMode = CONFIG.MODES.REPLACE;
GM_setValue(CONFIG.translationModeKey, translationMode);
showSettings();
const engineToUse = getCurrentEngine();
if (engineToUse === CONFIG.ENGINES.MICROSOFT && microsoftTranslatorActive && (widgetEnabled || isWhitelistedDomain)) {
showStatusMessage('替换模式,立即重新翻译...', 2000);
scheduleImmediateTranslation();
} else {
showStatusMessage('替换模式', 2000);
}
}
});
// 双语模式按钮
const dualModeBtn = document.createElement('button');
dualModeBtn.style.cssText = `
padding: 8px;
border: ${translationMode === CONFIG.MODES.DUAL ? '2px solid #00b894' : '1px solid #ddd'};
border-radius: 5px;
background: ${translationMode === CONFIG.MODES.DUAL ? '#f0fff4' : 'white'};
cursor: pointer;
font-size: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
transition: all 0.2s;
`;
dualModeBtn.setAttribute('data-no-translate', 'true');
dualModeBtn.setAttribute('translate', 'no');
dualModeBtn.setAttribute('data-protect-translation', 'true');
dualModeBtn.className = 'no-translate yandex-script-element';
dualModeBtn.innerHTML = `
↩️
双语显示
`;
dualModeBtn.addEventListener('click', function() {
if (translationMode !== CONFIG.MODES.DUAL) {
translationMode = CONFIG.MODES.DUAL;
GM_setValue(CONFIG.translationModeKey, translationMode);
showSettings();
const engineToUse = getCurrentEngine();
if (engineToUse === CONFIG.ENGINES.MICROSOFT && microsoftTranslatorActive && (widgetEnabled || isWhitelistedDomain)) {
showStatusMessage('双语模式,立即重新翻译...', 2000);
scheduleImmediateTranslation();
} else {
showStatusMessage('双语模式', 2000);
}
}
});
modeGrid.appendChild(replaceModeBtn);
modeGrid.appendChild(dualModeBtn);
modeSection.appendChild(modeGrid);
modalContent.appendChild(modeSection);
// 白名单管理部分
const whitelistSection = document.createElement('div');
whitelistSection.style.cssText = `
margin-bottom: 15px;
border-top: 1px solid #eee;
padding-top: 15px;
`;
whitelistSection.setAttribute('data-no-translate', 'true');
whitelistSection.setAttribute('translate', 'no');
whitelistSection.setAttribute('data-protect-translation', 'true');
whitelistSection.className = 'no-translate';
const whitelistHeader = document.createElement('div');
whitelistHeader.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
`;
const whitelistTitle = document.createElement('div');
whitelistTitle.textContent = '白名单管理';
whitelistTitle.style.cssText = `
font-weight: bold;
color: #555;
font-size: 13px;
`;
whitelistTitle.setAttribute('data-no-translate', 'true');
whitelistTitle.setAttribute('translate', 'no');
whitelistTitle.setAttribute('data-protect-translation', 'true');
whitelistTitle.className = 'no-translate';
const whitelist = getWhitelist();
const isInWhitelist = whitelist.includes(currentDomain);
const toggleWhitelistBtn = document.createElement('button');
toggleWhitelistBtn.textContent = isInWhitelist ? `⭐ 移除` : `☆ 添加`;
toggleWhitelistBtn.style.cssText = `
padding: 4px 8px;
background-color: ${isInWhitelist ? '#ffa502' : '#2ed573'};
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
`;
toggleWhitelistBtn.setAttribute('data-no-translate', 'true');
toggleWhitelistBtn.setAttribute('translate', 'no');
toggleWhitelistBtn.setAttribute('data-protect-translation', 'true');
toggleWhitelistBtn.className = 'no-translate yandex-script-element';
toggleWhitelistBtn.addEventListener('click', function() {
toggleWhitelist();
showSettings();
});
whitelistHeader.appendChild(whitelistTitle);
whitelistHeader.appendChild(toggleWhitelistBtn);
whitelistSection.appendChild(whitelistHeader);
if (whitelist.length > 0) {
const whitelistList = document.createElement('div');
whitelistList.style.cssText = `
max-height: 100px;
overflow-y: auto;
margin-bottom: 10px;
border: 1px solid #eee;
border-radius: 4px;
padding: 5px;
background-color: #f9f9f9;
`;
whitelistList.setAttribute('data-no-translate', 'true');
whitelistList.setAttribute('translate', 'no');
whitelistList.setAttribute('data-protect-translation', 'true');
whitelistList.className = 'no-translate';
whitelist.forEach((domain, index) => {
const domainItem = document.createElement('div');
domainItem.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 6px;
background-color: ${index % 2 === 0 ? 'white' : '#f5f5f5'};
border-radius: 3px;
margin-bottom: 3px;
font-size: 11px;
`;
domainItem.setAttribute('data-no-translate', 'true');
domainItem.setAttribute('translate', 'no');
domainItem.setAttribute('data-protect-translation', 'true');
domainItem.className = 'no-translate yandex-script-element';
const domainText = document.createElement('span');
domainText.textContent = domain;
domainText.style.cssText = `
flex-grow: 1;
word-break: break-all;
`;
domainText.setAttribute('data-no-translate', 'true');
domainText.setAttribute('translate', 'no');
domainText.setAttribute('data-protect-translation', 'true');
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除';
deleteBtn.className = 'compact-btn';
deleteBtn.style.cssText = `
font-size: 10px;
padding: 3px 6px;
margin-left: 5px;
flex-shrink: 0;
`;
deleteBtn.setAttribute('data-no-translate', 'true');
deleteBtn.setAttribute('translate', 'no');
deleteBtn.setAttribute('data-protect-translation', 'true');
deleteBtn.className += ' no-translate yandex-script-element';
deleteBtn.addEventListener('click', function() {
if (confirm(`确定要从白名单中删除 "${domain}" 吗?`)) {
let updatedWhitelist = getWhitelist();
updatedWhitelist = updatedWhitelist.filter(d => d !== domain);
saveWhitelist(updatedWhitelist);
showStatusMessage(`已从白名单中删除: ${domain}`, 2000);
showSettings();
}
});
domainItem.appendChild(domainText);
domainItem.appendChild(deleteBtn);
whitelistList.appendChild(domainItem);
});
whitelistSection.appendChild(whitelistList);
const clearAllBtn = document.createElement('button');
clearAllBtn.textContent = '清除所有白名单';
clearAllBtn.className = 'compact-btn';
clearAllBtn.style.cssText = `
width: 100%;
padding: 6px;
font-size: 11px;
`;
clearAllBtn.setAttribute('data-no-translate', 'true');
clearAllBtn.setAttribute('translate', 'no');
clearAllBtn.setAttribute('data-protect-translation', 'true');
clearAllBtn.className += ' no-translate yandex-script-element';
clearAllBtn.addEventListener('click', function() {
if (confirm(`确定要清除所有白名单域名吗?`)) {
cachedWhitelist = [];
GM_deleteValue(CONFIG.whitelistKey);
showStatusMessage('已清除所有白名单', 2000);
setTimeout(function() {
checkWhitelist();
showSettings();
}, 100);
}
});
whitelistSection.appendChild(clearAllBtn);
} else {
const emptyMessage = document.createElement('div');
emptyMessage.textContent = '白名单为空';
emptyMessage.style.cssText = `
text-align: center;
color: #999;
padding: 15px;
font-style: italic;
font-size: 12px;
`;
emptyMessage.setAttribute('data-no-translate', 'true');
emptyMessage.setAttribute('translate', 'no');
emptyMessage.setAttribute('data-protect-translation', 'true');
emptyMessage.className = 'no-translate';
whitelistSection.appendChild(emptyMessage);
}
modalContent.appendChild(whitelistSection);
// 关闭按钮
const closeBtn = document.createElement('button');
closeBtn.textContent = '关闭设置';
closeBtn.style.cssText = `
background-color: #747d8c;
color: white;
border: none;
padding: 10px 15px;
border-radius: 6px;
cursor: pointer;
width: 100%;
font-size: 13px;
font-weight: 600;
transition: background-color 0.2s;
`;
closeBtn.setAttribute('data-no-translate', 'true');
closeBtn.setAttribute('translate', 'no');
closeBtn.setAttribute('data-protect-translation', 'true');
closeBtn.className = 'no-translate yandex-script-element';
closeBtn.addEventListener('click', function() {
document.body.removeChild(modal);
isModalOpen = false;
});
closeBtn.addEventListener('mouseenter', function() {
this.style.backgroundColor = '#636e72';
});
closeBtn.addEventListener('mouseleave', function() {
this.style.backgroundColor = '#747d8c';
});
modalContent.appendChild(closeBtn);
modal.appendChild(modalContent);
modal.addEventListener('click', function(e) {
if (e.target === modal) {
document.body.removeChild(modal);
isModalOpen = false;
}
});
const escHandler = function(e) {
if (e.key === 'Escape') {
document.body.removeChild(modal);
document.removeEventListener('keydown', escHandler);
isModalOpen = false;
}
};
document.addEventListener('keydown', escHandler);
document.body.appendChild(modal);
// 恢复滚动位置
setTimeout(() => {
modalContent.scrollTop = scrollTopToRestore;
}, 0);
}
// 更新现有翻译元素的颜色
function updateExistingTranslationColors() {
console.log('更新现有翻译元素的颜色:', translationTextColor);
// 更新替换模式下的翻译元素
document.querySelectorAll('.microsoft-translated.replace-mode').forEach(element => {
element.style.color = translationTextColor + ' !important';
element.style.setProperty('color', translationTextColor, 'important');
// 更新悬停样式
element.addEventListener('mouseenter', function() {
this.style.backgroundColor = translationBgColor + ' !important';
});
element.addEventListener('mouseleave', function() {
this.style.backgroundColor = 'transparent';
});
});
// 更新双语模式下的翻译元素
document.querySelectorAll('.microsoft-translated.dual-mode .translated-text').forEach(element => {
element.style.color = translationTextColor + ' !important';
element.style.setProperty('color', translationTextColor, 'important');
});
console.log('已更新现有翻译元素的颜色');
}
// =============== Yandex翻译器相关函数 ===============
// 创建Yandex小部件
function createYandexWidget() {
if (widgetContainer) return;
if (!document.body) {
setTimeout(createYandexWidget, 100);
return;
}
widgetContainer = document.createElement("div");
widgetContainer.id = CONFIG.widgetId;
widgetContainer.className = "yandex-translate-widget";
widgetContainer.setAttribute("translate", "no");
widgetContainer.setAttribute("data-no-translate", "true");
widgetContainer.setAttribute("data-protect-translation", "true");
widgetContainer.classList.add("no-translate");
widgetContainer.classList.add("yandex-script-element");
let widgetParams = `widgetId=${CONFIG.widgetId}&pageLang=auto&widgetLang=zh&widgetTheme=minimal&autoMode=true`;
document.body.appendChild(widgetContainer);
loadYandexScript(widgetParams);
}
// 加载Yandex脚本
function loadYandexScript(widgetParams) {
if (yandexScript || window.YandexTranslateWidgetLoaded) return;
yandexScript = document.createElement("script");
yandexScript.src = `https://translate.yandex.net/website-widget/v1/widget.js?${widgetParams}`;
yandexScript.async = true;
yandexScript.defer = true;
yandexScript.onload = function() {
window.YandexTranslateWidgetLoaded = true;
console.log('Yandex自动翻译脚本加载成功');
// 等待一段时间后检查翻译状态
setTimeout(() => {
if (!isYandexTranslatorActive()) {
console.log('Yandex翻译未自动激活,尝试手动激活');
reactivateYandexTranslator();
}
}, 3000);
};
yandexScript.onerror = function() {
console.error('Yandex脚本加载失败');
};
document.body.appendChild(yandexScript);
}
// 移除Yandex小部件
function removeYandexWidget() {
if (widgetContainer && widgetContainer.parentNode) {
widgetContainer.parentNode.removeChild(widgetContainer);
widgetContainer = null;
}
if (yandexScript && yandexScript.parentNode) {
yandexScript.parentNode.removeChild(yandexScript);
yandexScript = null;
}
window.YandexTranslateWidgetLoaded = false;
yandexTranslationActive = false;
}
// =============== 微软翻译器相关函数 ===============
// 获取微软翻译认证token
function getMicrosoftAuthToken() {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: "https://edge.microsoft.com/translate/auth",
onload: function(res) {
if (res.status === 200) {
resolve(res.responseText);
} else {
reject(new Error('获取微软认证token失败'));
}
},
onerror: function(res) {
reject(new Error('网络错误'));
}
});
});
}
// 微软翻译函数(支持批量)
function translateMicrosoftBatch(texts, fromLang, toLang) {
return new Promise((resolve, reject) => {
if (!microsoftAuthToken) {
reject(new Error('未获取到微软认证token'));
return;
}
if (!texts || texts.length === 0) {
reject(new Error('翻译文本为空'));
return;
}
const fromParam = fromLang ? `&from=${fromLang}` : '';
const translateData = texts.map(text => ({ "Text": text }));
GM_xmlhttpRequest({
method: "POST",
url: `https://api-edge.cognitive.microsofttranslator.com/translate?to=${toLang}${fromParam}&api-version=3.0&includeSentenceLength=true`,
headers: {
"Authorization": `Bearer ${microsoftAuthToken}`,
"Content-Type": "application/json",
},
data: JSON.stringify(translateData),
onload: function(res) {
if (res.status === 200) {
try {
const result = JSON.parse(res.responseText);
const translations = result.map(item =>
item && item.translations && item.translations[0]
? item.translations[0].text
: null
);
resolve(translations);
} catch (e) {
reject(new Error('解析微软响应失败'));
}
} else {
reject(new Error(`微软翻译失败: ${res.status}`));
}
},
onerror: function(res) {
reject(new Error('微软网络请求失败'));
}
});
});
}
// 设置微软翻译器
async function setupMicrosoftTranslator() {
if (microsoftTranslatorActive) return;
try {
microsoftAuthToken = await getMicrosoftAuthToken();
console.log('微软翻译认证成功');
microsoftTranslatorActive = true;
} catch (error) {
console.error('微软翻译初始化失败:', error);
}
}
// 翻译整个页面 - 修复版:统一提示管理
async function translateWholePage() {
if (pageTranslating) {
console.log('翻译正在进行中...');
return;
}
if (!microsoftTranslatorActive || !microsoftAuthToken) {
console.log('微软翻译未初始化,跳过翻译');
return;
}
try {
pageTranslating = true;
// 显示翻译状态指示器(根据设置)
if (showTranslationIndicator) {
showTranslatingIndicator('正在翻译页面...');
}
let textNodes;
if (isRussianContent && currentRussianSiteConfig) {
console.log('使用俄语网站优化翻译...');
textNodes = collectTextNodesForRussianSite();
} else {
textNodes = collectTextNodes();
}
if (textNodes.length === 0) {
console.log('无可翻译文本');
pageTranslating = false;
// 如果没有文本可翻译,显示"翻译完成"然后隐藏
if (showTranslationIndicator) {
showTranslatingIndicator('翻译完成');
setTimeout(() => {
hideTranslatingIndicator();
}, CONFIG.TRANSLATING_INDICATOR_DURATION);
}
return;
}
console.log(`找到 ${textNodes.length} 个文本节点需要翻译`);
const batches = createTextBatches(textNodes);
const totalBatches = batches.length;
let translatedCount = 0;
for (let i = 0; i < batches.length; i++) {
if (!pageTranslating) break;
const batch = batches[i];
try {
const textsToTranslate = [];
const nodeIndices = [];
batch.nodes.forEach((nodeInfo, index) => {
const cacheKey = `${nodeInfo.text}|${targetLanguage}|${translationMode}`;
if (translationCache.has(cacheKey)) {
const translation = translationCache.get(cacheKey);
applyTranslation(nodeInfo, translation);
translatedCount++;
} else {
textsToTranslate.push(nodeInfo.text);
nodeIndices.push(index);
}
});
if (textsToTranslate.length > 0) {
const fromLang = isRussianContent && currentRussianSiteConfig && currentRussianSiteConfig.forceRussianSource ? 'ru' : null;
const translations = await translateMicrosoftBatch(textsToTranslate, fromLang, targetLanguage);
translations.forEach((translation, transIndex) => {
const nodeIndex = nodeIndices[transIndex];
const nodeInfo = batch.nodes[nodeIndex];
if (translation && translation !== nodeInfo.text) {
applyTranslation(nodeInfo, translation);
const cacheKey = `${nodeInfo.text}|${targetLanguage}|${translationMode}`;
translationCache.set(cacheKey, translation);
translatedCount++;
}
});
}
if (i < batches.length - 1) {
await new Promise(resolve => setTimeout(resolve, 20));
}
} catch (error) {
console.error(`第 ${i + 1} 批翻译失败:`, error);
if (translationRetryCount < CONFIG.MAX_RETRY_COUNT) {
translationRetryCount++;
console.log(`翻译失败,第${translationRetryCount}次重试...`);
await new Promise(resolve => setTimeout(resolve, 200));
continue;
}
}
}
pageTranslating = false;
lastTranslationTime = Date.now();
translationRetryCount = 0;
console.log(`微软翻译完成: 已翻译 ${translatedCount} 个文本`);
// 修复:翻译完成后显示"翻译完成",然后延迟隐藏
if (showTranslationIndicator) {
showTranslatingIndicator('翻译完成');
setTimeout(() => {
hideTranslatingIndicator();
}, CONFIG.TRANSLATING_INDICATOR_DURATION);
}
setTimeout(() => {
checkForUntranslatedText();
}, 500);
} catch (error) {
console.error('页面翻译失败:', error);
pageTranslating = false;
// 修复:翻译失败时显示"翻译失败",然后延迟隐藏
if (showTranslationIndicator) {
showTranslatingIndicator('翻译失败');
setTimeout(() => {
hideTranslatingIndicator();
}, CONFIG.TRANSLATING_INDICATOR_DURATION);
}
}
}
// 显示翻译状态指示器 - 统一管理整个脚本的提示
function showTranslatingIndicator(message = '正在翻译页面...') {
// 如果提示已关闭,不显示
if (!showTranslationIndicator) return;
// 清除之前的隐藏定时器
if (indicatorHideTimeout) {
clearTimeout(indicatorHideTimeout);
indicatorHideTimeout = null;
}
// 如果已有状态提示,更新内容
if (currentTranslatingIndicator && currentTranslatingIndicator.parentNode) {
const spinner = currentTranslatingIndicator.querySelector('.spinner');
const text = currentTranslatingIndicator.querySelector('span:last-child');
if (text) {
text.textContent = message;
}
// 根据消息类型调整样式
if (message === '正在翻译页面...') {
// 显示spinner
if (!spinner) {
const newSpinner = document.createElement('div');
newSpinner.className = 'spinner';
currentTranslatingIndicator.insertBefore(newSpinner, text);
} else {
spinner.style.display = 'block';
}
currentTranslatingIndicator.style.background = 'rgba(0, 0, 0, 0.7)';
currentTranslatingIndicator.style.color = '#00ffcc';
currentTranslatingIndicator.style.borderColor = 'rgba(0, 255, 204, 0.3)';
} else if (message === '翻译完成') {
// 隐藏spinner,改变颜色为绿色
if (spinner) {
spinner.style.display = 'none';
}
currentTranslatingIndicator.style.background = 'rgba(0, 184, 148, 0.7)';
currentTranslatingIndicator.style.color = '#ffffff';
currentTranslatingIndicator.style.borderColor = 'rgba(0, 184, 148, 0.3)';
} else if (message === '翻译失败') {
// 隐藏spinner,改变颜色为红色
if (spinner) {
spinner.style.display = 'none';
}
currentTranslatingIndicator.style.background = 'rgba(255, 71, 87, 0.7)';
currentTranslatingIndicator.style.color = '#ffffff';
currentTranslatingIndicator.style.borderColor = 'rgba(255, 71, 87, 0.3)';
} else if (message === 'Yandex翻译已启动') {
// Yandex翻译状态
if (spinner) {
spinner.style.display = 'none';
}
currentTranslatingIndicator.style.background = 'rgba(255, 61, 0, 0.7)';
currentTranslatingIndicator.style.color = '#ffffff';
currentTranslatingIndicator.style.borderColor = 'rgba(255, 61, 0, 0.3)';
}
// 确保指示器可见
currentTranslatingIndicator.style.opacity = '0.9';
currentIndicatorText = message;
return;
}
// 创建新的状态指示器
currentTranslatingIndicator = document.createElement('div');
currentTranslatingIndicator.className = 'translating-indicator';
currentTranslatingIndicator.setAttribute('translate', 'no');
currentTranslatingIndicator.setAttribute('data-no-translate', 'true');
currentTranslatingIndicator.setAttribute('data-protect-translation', 'true');
currentTranslatingIndicator.classList.add('no-translate');
currentTranslatingIndicator.classList.add('yandex-script-element');
// 根据消息类型添加spinner
if (message === '正在翻译页面...') {
const spinner = document.createElement('div');
spinner.className = 'spinner';
currentTranslatingIndicator.appendChild(spinner);
}
const text = document.createElement('span');
text.textContent = message;
text.setAttribute('data-no-translate', 'true');
text.setAttribute('translate', 'no');
text.setAttribute('data-protect-translation', 'true');
currentTranslatingIndicator.appendChild(text);
// 根据消息类型设置样式
if (message === '正在翻译页面...') {
currentTranslatingIndicator.style.background = 'rgba(0, 0, 0, 0.7)';
currentTranslatingIndicator.style.color = '#00ffcc';
currentTranslatingIndicator.style.borderColor = 'rgba(0, 255, 204, 0.3)';
} else if (message === '翻译完成') {
currentTranslatingIndicator.style.background = 'rgba(0, 184, 148, 0.7)';
currentTranslatingIndicator.style.color = '#ffffff';
currentTranslatingIndicator.style.borderColor = 'rgba(0, 184, 148, 0.3)';
} else if (message === '翻译失败') {
currentTranslatingIndicator.style.background = 'rgba(255, 71, 87, 0.7)';
currentTranslatingIndicator.style.color = '#ffffff';
currentTranslatingIndicator.style.borderColor = 'rgba(255, 71, 87, 0.3)';
} else if (message === 'Yandex翻译已启动') {
currentTranslatingIndicator.style.background = 'rgba(255, 61, 0, 0.7)';
currentTranslatingIndicator.style.color = '#ffffff';
currentTranslatingIndicator.style.borderColor = 'rgba(255, 61, 0, 0.3)';
}
document.body.appendChild(currentTranslatingIndicator);
isTranslatingIndicatorVisible = true;
currentIndicatorText = message;
console.log('翻译状态提示已显示:', message);
}
// 隐藏翻译状态指示器
function hideTranslatingIndicator() {
// 清除之前的隐藏定时器
if (indicatorHideTimeout) {
clearTimeout(indicatorHideTimeout);
indicatorHideTimeout = null;
}
if (currentTranslatingIndicator && currentTranslatingIndicator.parentNode) {
// 添加淡出效果
currentTranslatingIndicator.style.opacity = '0';
// 等待动画完成后移除元素
setTimeout(() => {
if (currentTranslatingIndicator && currentTranslatingIndicator.parentNode) {
currentTranslatingIndicator.parentNode.removeChild(currentTranslatingIndicator);
currentTranslatingIndicator = null;
isTranslatingIndicatorVisible = false;
currentIndicatorText = '';
}
}, CONFIG.TRANSLATING_INDICATOR_FADE_OUT);
} else {
isTranslatingIndicatorVisible = false;
currentIndicatorText = '';
}
}
// 智能文本收集 - 不收集数字和单位
function collectTextNodes() {
const textNodes = [];
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
const parent = node.parentElement;
if (!parent) return NodeFilter.FILTER_REJECT;
// 排除脚本菜单和脚本相关元素
const excludedSelectors = [
'SCRIPT', 'STYLE', 'NOSCRIPT',
'.microsoft-translated',
'.translation-popup',
'.translating-indicator',
'.yandex-translate-widget',
'.translation-settings-modal',
'[data-no-translate]',
'.no-translate',
'.yandex-no-translate',
'.yandex-script-element',
'[data-protect-translation="true"]',
'.tm-script-menu', '#tm-script-menu',
'.vm-script-menu', '#vm-script-menu',
'.gm-script-menu', '#gm-script-menu',
'[class*="userscript"]', '[id*="userscript"]',
'[class*="tampermonkey"]', '[id*="tampermonkey"]',
'[class*="violentmonkey"]', '[id*="violentmonkey"]',
'[class*="greasemonkey"]', '[id*="greasemonkey"]',
'.engine-btn', '.site-engine-badge',
'.yandex-protected-text'
];
for (const selector of excludedSelectors) {
if (parent.matches(selector) || parent.closest(selector)) {
return NodeFilter.FILTER_REJECT;
}
}
// 检查属性和类名
if (parent.hasAttribute('translate') && parent.getAttribute('translate') === 'no') {
return NodeFilter.FILTER_REJECT;
}
if (parent.classList && (
parent.classList.contains('no-translate') ||
parent.classList.contains('yandex-no-translate') ||
parent.classList.contains('yandex-script-element'))) {
return NodeFilter.FILTER_REJECT;
}
const text = node.textContent.trim();
if (!text || text.length < 2) return NodeFilter.FILTER_REJECT;
// 不翻译纯数字
if (/^\d+$/.test(text)) return NodeFilter.FILTER_REJECT;
// 不翻译数字+单位(如22M, 100GB等)
if (/^\d+\.?\d*\s*[KMGT]?[B]?$/i.test(text)) return NodeFilter.FILTER_REJECT;
// 不翻译百分比
if (/^\d+\.?\d*\s*%$/.test(text)) return NodeFilter.FILTER_REJECT;
// 不翻译纯符号
if (/^[\d\s\W]+$/.test(text)) return NodeFilter.FILTER_REJECT;
if (parent.classList.contains('microsoft-translated')) return NodeFilter.FILTER_REJECT;
// 只收集有意义的文本(包含字母)
if (!/[a-zA-Z\u0400-\u04FF\u4e00-\u9fff]/.test(text)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
}
);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
if (text && text.length > 0) {
textNodes.push({
node: node,
text: text,
parent: node.parentElement,
originalText: node.textContent
});
}
}
return textNodes;
}
// 收集文本节点(俄语网站优化版)- 修复排除脚本菜单
function collectTextNodesForRussianSite() {
const textNodes = [];
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
const parent = node.parentElement;
if (!parent) return NodeFilter.FILTER_REJECT;
// 排除脚本菜单和脚本相关元素
const excludedSelectors = [
'SCRIPT', 'STYLE', 'NOSCRIPT',
'.microsoft-translated',
'.translation-popup',
'.translating-indicator',
'.yandex-translate-widget',
'.translation-settings-modal',
'[data-no-translate]',
'.no-translate',
'.yandex-no-translate',
'.yandex-script-element',
'[data-protect-translation="true"]',
'.tm-script-menu', '#tm-script-menu',
'.vm-script-menu', '#vm-script-menu',
'.gm-script-menu', '#gm-script-menu',
'[class*="userscript"]', '[id*="userscript"]',
'[class*="tampermonkey"]', '[id*="tampermonkey"]',
'[class*="violentmonkey"]', '[id*="violentmonkey"]',
'[class*="greasemonkey"]', '[id*="greasemonkey"]',
'.engine-btn', '.site-engine-badge',
'.yandex-protected-text'
];
for (const selector of excludedSelectors) {
if (parent.matches(selector) || parent.closest(selector)) {
return NodeFilter.FILTER_REJECT;
}
}
// 检查属性和类名
if (parent.hasAttribute('translate') && parent.getAttribute('translate') === 'no') {
return NodeFilter.FILTER_REJECT;
}
if (parent.classList && (
parent.classList.contains('no-translate') ||
parent.classList.contains('yandex-no-translate') ||
parent.classList.contains('yandex-script-element'))) {
return NodeFilter.FILTER_REJECT;
}
// 排除当前俄语网站配置中的选择器
if (currentRussianSiteConfig && currentRussianSiteConfig.excludeSelectors) {
for (const selector of currentRussianSiteConfig.excludeSelectors) {
if (parent.matches(selector) || parent.closest(selector)) {
return NodeFilter.FILTER_REJECT;
}
}
}
const text = node.textContent.trim();
if (!text || text.length < 1) return NodeFilter.FILTER_REJECT;
// 不翻译纯数字
if (/^\d+$/.test(text)) return NodeFilter.FILTER_REJECT;
// 不翻译数字+单位(如22M, 100GB等)
if (/^\d+\.?\d*\s*[KMGT]?[B]?$/i.test(text)) return NodeFilter.FILTER_REJECT;
// 不翻译百分比
if (/^\d+\.?\d*\s*%$/.test(text)) return NodeFilter.FILTER_REJECT;
// 不翻译纯符号
if (/^[\d\s\W]+$/.test(text)) return NodeFilter.FILTER_REJECT;
if (parent.classList.contains('microsoft-translated')) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
}
);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
if (text && text.length > 0) {
textNodes.push({
node: node,
text: text,
parent: node.parentElement,
originalText: node.textContent
});
}
}
return textNodes;
}
// 创建文本批次
function createTextBatches(textNodes) {
const batches = [];
let currentBatch = {
nodes: [],
totalLength: 0
};
for (const nodeInfo of textNodes) {
const textLength = nodeInfo.text.length;
if (textLength > CONFIG.MAX_TEXT_LENGTH) {
if (currentBatch.nodes.length > 0) {
batches.push(currentBatch);
currentBatch = { nodes: [], totalLength: 0 };
}
const truncatedText = nodeInfo.text.substring(0, CONFIG.MAX_TEXT_LENGTH);
batches.push({
nodes: [{...nodeInfo, text: truncatedText}],
totalLength: truncatedText.length
});
continue;
}
if (currentBatch.totalLength + textLength > CONFIG.MAX_TEXT_LENGTH ||
currentBatch.nodes.length >= CONFIG.BATCH_SIZE) {
batches.push(currentBatch);
currentBatch = { nodes: [], totalLength: 0 };
}
currentBatch.nodes.push(nodeInfo);
currentBatch.totalLength += textLength;
}
if (currentBatch.nodes.length > 0) {
batches.push(currentBatch);
}
return batches;
}
// 应用翻译结果 - 修复文字颜色功能
function applyTranslation(nodeInfo, translation) {
try {
const originalNode = nodeInfo.node;
const parent = nodeInfo.parent;
const originalText = nodeInfo.originalText || originalNode.textContent;
const elementId = generateElementId();
originalTextMap.set(elementId, originalText);
const translatedElement = document.createElement('span');
translatedElement.className = `microsoft-translated ${translationMode}-mode`;
translatedElement.dataset.originalId = elementId;
translatedElement.setAttribute('translate', 'no');
translatedElement.setAttribute('data-no-translate', 'true');
translatedElement.setAttribute('data-protect-translation', 'true');
translatedElement.classList.add('no-translate');
translatedElement.classList.add('yandex-script-element');
if (translationMode === CONFIG.MODES.REPLACE) {
translatedElement.textContent = translation;
translatedElement.title = `原文: ${originalText}\n点击恢复原文`;
// 直接设置颜色和内联样式
translatedElement.style.cssText += `color: ${translationTextColor} !important;`;
translatedElement.addEventListener('click', function(e) {
e.stopPropagation();
const originalId = this.dataset.originalId;
const original = originalTextMap.get(originalId);
if (original) {
const textNode = document.createTextNode(original);
parent.replaceChild(textNode, this);
}
});
// 添加悬停效果
translatedElement.addEventListener('mouseenter', function() {
this.style.cssText += `background-color: ${translationBgColor} !important;`;
});
translatedElement.addEventListener('mouseleave', function() {
this.style.cssText = `color: ${translationTextColor} !important;`;
});
} else {
const originalSpan = document.createElement('span');
originalSpan.className = 'original-text';
originalSpan.textContent = originalText;
originalSpan.setAttribute('data-no-translate', 'true');
originalSpan.setAttribute('translate', 'no');
originalSpan.setAttribute('data-protect-translation', 'true');
originalSpan.classList.add('no-translate');
originalSpan.classList.add('yandex-script-element');
const translatedSpan = document.createElement('span');
translatedSpan.className = 'translated-text';
translatedSpan.textContent = translation;
// 直接设置颜色和内联样式
translatedSpan.style.cssText += `color: ${translationTextColor} !important;`;
translatedSpan.setAttribute('data-no-translate', 'true');
translatedSpan.setAttribute('translate', 'no');
translatedSpan.setAttribute('data-protect-translation', 'true');
translatedSpan.classList.add('no-translate');
translatedSpan.classList.add('yandex-script-element');
translatedElement.appendChild(originalSpan);
translatedElement.appendChild(translatedSpan);
}
parent.replaceChild(translatedElement, originalNode);
} catch (error) {
console.error('应用翻译失败:', error);
}
}
// 生成元素ID
function generateElementId() {
return 'translated_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
// 检查是否有未翻译的文本
function checkForUntranslatedText() {
if (!microsoftTranslatorActive || pageTranslating) return;
const untranslatedNodes = collectUntranslatedNodes();
if (untranslatedNodes.length > 0) {
console.log(`发现 ${untranslatedNodes.length} 个未翻译文本,重新翻译...`);
if (untranslatedNodes.length > 10) {
translateWholePage();
} else {
translateSpecificNodes(untranslatedNodes);
}
}
}
// 收集未翻译的文本节点
function collectUntranslatedNodes() {
const textNodes = [];
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
const parent = node.parentElement;
if (!parent) return NodeFilter.FILTER_REJECT;
// 排除脚本菜单和脚本相关元素
const excludedSelectors = [
'SCRIPT', 'STYLE', 'NOSCRIPT',
'.microsoft-translated',
'.translation-popup',
'.translating-indicator',
'.yandex-translate-widget',
'.translation-settings-modal',
'[data-no-translate]',
'.no-translate',
'.yandex-no-translate',
'.yandex-script-element',
'[data-protect-translation="true"]',
'.tm-script-menu', '#tm-script-menu',
'.vm-script-menu', '#vm-script-menu',
'.gm-script-menu', '#gm-script-menu',
'[class*="userscript"]', '[id*="userscript"]',
'[class*="tampermonkey"]', '[id*="tampermonkey"]',
'[class*="violentmonkey"]', '[id*="violentmonkey"]',
'[class*="greasemonkey"]', '[id*="greasemonkey"]',
'.engine-btn', '.site-engine-badge',
'.yandex-protected-text'
];
for (const selector of excludedSelectors) {
if (parent.matches(selector) || parent.closest(selector)) {
return NodeFilter.FILTER_REJECT;
}
}
// 检查属性和类名
if (parent.hasAttribute('translate') && parent.getAttribute('translate') === 'no') {
return NodeFilter.FILTER_REJECT;
}
if (parent.classList && (
parent.classList.contains('no-translate') ||
parent.classList.contains('yandex-no-translate') ||
parent.classList.contains('yandex-script-element'))) {
return NodeFilter.FILTER_REJECT;
}
const text = node.textContent.trim();
if (!text || text.length < 2) return NodeFilter.FILTER_REJECT;
// 不翻译纯数字
if (/^\d+$/.test(text)) return NodeFilter.FILTER_REJECT;
// 不翻译数字+单位(如22M, 100GB等)
if (/^\d+\.?\d*\s*[KMGT]?[B]?$/i.test(text)) return NodeFilter.FILTER_REJECT;
// 不翻译百分比
if (/^\d+\.?\d*\s*%$/.test(text)) return NodeFilter.FILTER_REJECT;
// 不翻译纯符号
if (/^[\d\s\W]+$/.test(text)) return NodeFilter.FILTER_REJECT;
if (parent.classList.contains('microsoft-translated')) return NodeFilter.FILTER_REJECT;
// 只收集有意义的文本(包含字母)
if (!/[a-zA-Z\u0400-\u04FF\u4e00-\u9fff]/.test(text)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
}
);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
if (text && text.length > 0) {
textNodes.push({
node: node,
text: text,
parent: node.parentElement,
originalText: node.textContent
});
}
}
return textNodes;
}
// 翻译特定的文本节点
async function translateSpecificNodes(textNodes) {
if (!microsoftTranslatorActive || !microsoftAuthToken || pageTranslating) return;
try {
pageTranslating = true;
// 显示翻译状态提示(根据设置)
if (showTranslationIndicator) {
showTranslatingIndicator('正在翻译页面...');
}
const batches = createTextBatches(textNodes);
let translatedCount = 0;
for (let i = 0; i < batches.length; i++) {
if (!pageTranslating) break;
const batch = batches[i];
const textsToTranslate = [];
const nodeIndices = [];
batch.nodes.forEach((nodeInfo, index) => {
const cacheKey = `${nodeInfo.text}|${targetLanguage}|${translationMode}`;
if (translationCache.has(cacheKey)) {
const translation = translationCache.get(cacheKey);
applyTranslation(nodeInfo, translation);
translatedCount++;
} else {
textsToTranslate.push(nodeInfo.text);
nodeIndices.push(index);
}
});
if (textsToTranslate.length > 0) {
const fromLang = isRussianContent && currentRussianSiteConfig && currentRussianSiteConfig.forceRussianSource ? 'ru' : null;
const translations = await translateMicrosoftBatch(textsToTranslate, fromLang, targetLanguage);
translations.forEach((translation, transIndex) => {
const nodeIndex = nodeIndices[transIndex];
const nodeInfo = batch.nodes[nodeIndex];
if (translation && translation !== nodeInfo.text) {
applyTranslation(nodeInfo, translation);
const cacheKey = `${nodeInfo.text}|${targetLanguage}|${translationMode}`;
translationCache.set(cacheKey, translation);
translatedCount++;
}
});
}
if (i < batches.length - 1) {
await new Promise(resolve => setTimeout(resolve, 20));
}
}
pageTranslating = false;
if (translatedCount > 0) {
console.log(`已翻译 ${translatedCount} 个未翻译文本`);
// 修复:翻译完成后显示"翻译完成",然后延迟隐藏
if (showTranslationIndicator) {
showTranslatingIndicator('翻译完成');
setTimeout(() => {
hideTranslatingIndicator();
}, CONFIG.TRANSLATING_INDICATOR_DURATION);
}
} else {
// 修复:没有翻译任何内容时也显示"翻译完成"
if (showTranslationIndicator) {
showTranslatingIndicator('翻译完成');
setTimeout(() => {
hideTranslatingIndicator();
}, CONFIG.TRANSLATING_INDICATOR_DURATION);
}
}
} catch (error) {
console.error('翻译特定节点失败:', error);
pageTranslating = false;
if (showTranslationIndicator) {
showTranslatingIndicator('翻译失败');
setTimeout(() => {
hideTranslatingIndicator();
}, CONFIG.TRANSLATING_INDICATOR_DURATION);
}
}
}
// =============== DOM变化监听相关函数 ===============
// 开始观察DOM变化
function startDOMObservation() {
if (isObservingDOM || !microsoftTranslatorActive) return;
stopDOMObservation();
const observerConfig = {
childList: true,
subtree: true,
characterData: true
};
domObserver = new MutationObserver((mutations) => {
if (pageTranslating) return;
if (domChangeTimer) clearTimeout(domChangeTimer);
domChangeTimer = setTimeout(() => {
let hasSignificantChanges = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' &&
(mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
hasSignificantChanges = true;
}
if (mutation.type === 'characterData' && mutation.target.nodeType === Node.TEXT_NODE) {
hasSignificantChanges = true;
}
});
if (hasSignificantChanges) {
console.log('检测到DOM变化,重新翻译...');
// 重新检测俄语内容
smartEngineDetection();
setTimeout(() => {
translateWholePage();
}, 300);
}
}, CONFIG.DOM_CHANGE_DELAY);
});
try {
domObserver.observe(document.body, observerConfig);
isObservingDOM = true;
} catch (error) {
console.error('DOM观察器启动失败:', error);
}
}
// 停止观察DOM变化
function stopDOMObservation() {
if (domObserver) {
domObserver.disconnect();
domObserver = null;
}
if (domChangeTimer) {
clearTimeout(domChangeTimer);
domChangeTimer = null;
}
isObservingDOM = false;
}
// 开始持续翻译监控
function startContinuousTranslation() {
stopContinuousTranslation();
if (!microsoftTranslatorActive) return;
console.log('开始持续翻译监控');
continuousTranslationTimer = setInterval(() => {
if (!pageTranslating && microsoftTranslatorActive) {
const now = Date.now();
if (now - lastTranslationTime > 20000) {
console.log('长时间未翻译,检查未翻译文本...');
checkForUntranslatedText();
}
}
}, 8000);
startScrollCheck();
}
// 停止持续翻译监控
function stopContinuousTranslation() {
if (continuousTranslationTimer) {
clearInterval(continuousTranslationTimer);
continuousTranslationTimer = null;
}
stopScrollCheck();
}
// 开始滚动检查
function startScrollCheck() {
stopScrollCheck();
if (!microsoftTranslatorActive) return;
lastScrollY = window.scrollY;
scrollCheckCount = 0;
window.addEventListener('scroll', handleScroll);
}
// 停止滚动检查
function stopScrollCheck() {
window.removeEventListener('scroll', handleScroll);
if (scrollCheckTimer) {
clearTimeout(scrollCheckTimer);
scrollCheckTimer = null;
}
}
// 处理滚动事件
function handleScroll() {
const currentScrollY = window.scrollY;
const scrollDelta = Math.abs(currentScrollY - lastScrollY);
if (scrollDelta > 300) {
scrollCheckCount++;
lastScrollY = currentScrollY;
if (scrollCheckTimer) clearTimeout(scrollCheckTimer);
scrollCheckTimer = setTimeout(() => {
if (!pageTranslating && microsoftTranslatorActive) {
console.log(`检测到翻页行为,检查翻译...`);
checkForUntranslatedText();
}
}, CONFIG.SCROLL_CHECK_DELAY);
}
}
// 停用微软翻译器
function deactivateMicrosoftTranslator() {
stopAllObservations();
document.querySelectorAll('.microsoft-translation-popup').forEach(popup => {
popup.remove();
});
document.querySelectorAll('.microsoft-translated').forEach(translatedElement => {
const originalId = translatedElement.dataset.originalId;
if (originalId) {
const originalText = originalTextMap.get(originalId);
if (originalText) {
const textNode = document.createTextNode(originalText);
translatedElement.parentNode.replaceChild(textNode, translatedElement);
}
}
});
microsoftTranslatorActive = false;
microsoftAuthToken = null;
pageTranslating = false;
originalTextMap.clear();
translationRetryCount = 0;
lastTranslationTime = 0;
}
// 切换翻译引擎
function toggleTranslationEngine() {
// 切换默认引擎
if (currentEngine === CONFIG.ENGINES.MICROSOFT) {
currentEngine = CONFIG.ENGINES.YANDEX;
} else {
currentEngine = CONFIG.ENGINES.MICROSOFT;
}
GM_setValue(CONFIG.translationEngineKey, currentEngine);
// 如果当前网站没有专用引擎,立即应用新引擎
if (!isUsingSiteSpecificEngine) {
updateWidgetBasedOnSettings();
showStatusMessage(`切换到${currentEngine === CONFIG.ENGINES.MICROSOFT ? '微软' : 'Yandex'}翻译`, 2000);
} else {
// 如果有专用引擎,只更新默认引擎,不改变当前网站的翻译
showStatusMessage(`默认引擎已切换为${currentEngine === CONFIG.ENGINES.MICROSOFT ? '微软' : 'Yandex'}翻译`, 2000);
}
registerMenuCommands();
}
// 切换主开关
function toggleWidget() {
if (isWhitelistedDomain) {
widgetEnabled = !widgetEnabled;
GM_setValue(CONFIG.storageKey, widgetEnabled);
showStatusMessage(widgetEnabled ? '自动翻译已开启' : '自动翻译已关闭', 2000);
} else {
widgetEnabled = !widgetEnabled;
GM_setValue(CONFIG.storageKey, widgetEnabled);
if (widgetEnabled) {
console.log('开启翻译,立即更新设置');
updateWidgetBasedOnSettings();
const engineToUse = getCurrentEngine();
const engineName = engineToUse === CONFIG.ENGINES.MICROSOFT ? '微软' : 'Yandex';
let message = `${engineName}翻译已开启`;
if (isUsingSiteSpecificEngine) {
message += ' (网站专用引擎)';
}
showStatusMessage(message, 2000);
} else {
console.log('关闭翻译,立即停用');
deactivateAllTranslators();
showStatusMessage('自动翻译已关闭', 2000);
}
}
registerMenuCommands();
}
// 设置键盘快捷键
function setupHotkey() {
document.addEventListener('keydown', function(e) {
if (isModalOpen) return;
if (e.ctrlKey && e.shiftKey && e.key === 'Y') {
e.preventDefault();
toggleWidget();
}
if (e.ctrlKey && e.shiftKey && e.key === 'W') {
e.preventDefault();
toggleWhitelist();
}
if (e.ctrlKey && e.shiftKey && e.key === 'E') {
e.preventDefault();
toggleTranslationEngine();
}
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
e.preventDefault();
showSettings();
}
if (e.ctrlKey && e.shiftKey && e.key === 'T') {
e.preventDefault();
const engineToUse = getCurrentEngine();
if (engineToUse === CONFIG.ENGINES.MICROSOFT && microsoftTranslatorActive) {
translateWholePage();
}
}
}, true);
}
// 显示状态提示
function showStatusMessage(message, duration = 2000) {
const existingMsg = document.getElementById('yandex-status-msg');
if (existingMsg) {
existingMsg.remove();
}
const msgDiv = document.createElement('div');
msgDiv.id = 'yandex-status-msg';
// 处理消息中的"Yandex"文本
const protectedMessage = message.replace(/Yandex/g,
'Yandex'
);
msgDiv.innerHTML = protectedMessage;
// 添加所有防护属性
msgDiv.setAttribute('data-no-translate', 'true');
msgDiv.setAttribute('translate', 'no');
msgDiv.setAttribute('data-protect-translation', 'true');
msgDiv.className = 'no-translate yandex-script-element';
msgDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 12px 20px;
border-radius: 10px;
z-index: 2147483646;
font-family: Arial, sans-serif;
font-size: 14px;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
opacity: 1;
transform: translateY(0);
max-width: 300px;
word-wrap: break-word;
`;
if (message.includes('白名单')) {
msgDiv.style.background = 'rgba(255, 193, 7, 0.95)';
msgDiv.style.color = '#000';
} else if (message.includes('微软')) {
msgDiv.style.background = 'rgba(0, 120, 215, 0.95)';
msgDiv.style.color = 'white';
} else if (message.includes('Yandex')) {
msgDiv.style.background = 'rgba(255, 61, 0, 0.95)';
msgDiv.style.color = 'white';
} else if (message.includes('俄语内容') || message.includes('俄语网站')) {
msgDiv.style.background = 'rgba(255, 61, 0, 0.95)';
msgDiv.style.color = 'white';
} else if (message.includes('专用引擎')) {
msgDiv.style.background = 'rgba(108, 92, 231, 0.95)';
msgDiv.style.color = 'white';
} else if (message.includes('翻译中') || message.includes('翻译完成') || message.includes('✓')) {
msgDiv.style.background = 'rgba(0, 184, 148, 0.95)';
msgDiv.style.color = 'white';
} else if (message.includes('✗') || message.includes('失败')) {
msgDiv.style.background = 'rgba(255, 71, 87, 0.95)';
msgDiv.style.color = 'white';
} else if (message.includes('颜色')) {
msgDiv.style.background = 'rgba(255, 159, 67, 0.95)';
msgDiv.style.color = 'white';
}
document.body.appendChild(msgDiv);
// 添加淡出效果
setTimeout(function() {
msgDiv.classList.add('fade-out');
setTimeout(function() {
if (msgDiv.parentNode) {
msgDiv.parentNode.removeChild(msgDiv);
}
}, 500);
}, duration);
}
// 启动脚本
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();