// ==UserScript==
// @name 微软自动翻译
// @namespace https://viayoo.com/
// @version 2.25.0
// @description 微软自动翻译小部件 | 自动翻译整个页面 | 简化界面 | 使用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
// @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij48cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIGZpbGw9InVybCgjZ3JhZGllbnQpIiByeD0iMzIiLz48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImdyYWRpZW50IiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMDA2NmNjIi8+PHN0b3Agb2Zmc2V0PSI1MCUiIHN0b3AtY29sb3I9IiMwMDY2Y2MiLz48c3RvcCBvZmZzZXQ9IjUxJSIgc3RvcC1jb2xvcj0iI2ZmNDc1NyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ZmNDc1NyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjx0ZXh0IHg9IjUwJSIgeT0iMzglIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjgiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXdlaWdodD0iYm9sZCIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+TTwvdGV4dD48dGV4dCB4PSI1MCUiIHk9IjY4JSIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjI4IiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPlQ8L3RleHQ+PC9zdmc+
// ==/UserScript==
(function() {
'use strict';
// 脚本配置
const CONFIG = {
widgetId: 'msWidget',
storageKey: 'microsoft_widget_enabled',
whitelistKey: 'microsoft_whitelist',
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: {
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']
},
'rutracker.org': {
textExtraction: 'simple',
excludeSelectors: ['.post_body', '.sp-wrap', '.code']
}
},
MAX_TEXT_LENGTH: 5000,
BATCH_SIZE: 50,
AUTO_TRANSLATE_DELAY: 100,
DOM_CHANGE_DELAY: 800,
SCROLL_CHECK_DELAY: 2000,
MAX_RETRY_COUNT: 2,
TRANSLATING_INDICATOR_DURATION: 3000,
TRANSLATING_INDICATOR_FADE_OUT: 1000
};
// 脚本状态
let widgetEnabled = CONFIG.defaultEnabled;
let isWhitelistedDomain = false;
let mainMenuCommandId = null;
let whitelistMenuCommandId = null;
let settingsMenuCommandId = null;
let isModalOpen = false;
let cachedWhitelist = null;
let currentDomain = '';
let isRussianSite = false;
let isRussianContent = false;
let currentRussianSiteConfig = null;
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 isPageLoaded = false;
let pageLoadedTimer = null;
let immediateTranslationScheduled = false;
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 {
// 获取页面主要内容的文本
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;
}
}
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++;
}
}
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]) {
// 只支持微软引擎
if (siteSpecificEngines[domain] === CONFIG.ENGINES.MICROSOFT) {
isUsingSiteSpecificEngine = true;
siteSpecificEngineName = CONFIG.ENGINES.MICROSOFT;
console.log(`当前网站有专用引擎: 微软`);
return CONFIG.ENGINES.MICROSOFT;
} else {
// 如果保存的是其他引擎,清除掉
delete siteSpecificEngines[domain];
saveSiteSpecificEngines(siteSpecificEngines);
}
}
return null;
}
// 为当前网站设置专用引擎(仅微软)
function setSiteSpecificEngine(engine) {
if (engine !== CONFIG.ENGINES.MICROSOFT) return;
const domain = getCurrentDomain();
siteSpecificEngines[domain] = engine;
saveSiteSpecificEngines(siteSpecificEngines);
isUsingSiteSpecificEngine = true;
siteSpecificEngineName = engine;
console.log(`为网站 ${domain} 设置专用引擎: 微软`);
}
// 清除当前网站的专用引擎
function clearSiteSpecificEngine() {
const domain = getCurrentDomain();
if (siteSpecificEngines[domain]) {
delete siteSpecificEngines[domain];
saveSiteSpecificEngines(siteSpecificEngines);
isUsingSiteSpecificEngine = false;
siteSpecificEngineName = '';
console.log(`清除网站 ${domain} 的专用引擎`);
}
}
// 获取当前应使用的引擎(始终微软)
function getCurrentEngine() {
return CONFIG.ENGINES.MICROSOFT;
}
// 智能引擎检测
function smartEngineDetection() {
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();
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 = 'translation-protection';
style.setAttribute('data-no-translate', 'true');
style.setAttribute('translate', 'no');
style.className = 'no-translate script-element';
style.textContent = `
/* 强制保护脚本创建的UI元素 */
body * {
transition: none !important;
}
/* 防护引擎选择器中的文本 */
.engine-btn,
.engine-btn * {
translate: no !important;
-webkit-translate: no !important;
font-language-override: normal !important;
unicode-bidi: isolate !important;
}
/* 为所有脚本元素添加额外防护 */
.translation-protected {
unicode-bidi: isolate !important;
font-language-override: normal !important;
}
`;
document.head.appendChild(style);
console.log('已创建翻译防护层');
}
// 初始化函数
function init() {
console.log('微软自动翻译脚本开始初始化...');
const savedState = GM_getValue(CONFIG.storageKey, CONFIG.defaultEnabled);
widgetEnabled = savedState;
// 读取用户设置
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;
}
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 languageName = CONFIG.LANGUAGES[targetLanguage]?.name || targetLanguage;
let message = `微软自动翻译已启动,目标语言: ${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.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;
}
/* 翻译状态指示器 */
.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;
height: 12px;
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); }
}
/* 状态消息 */
#microsoft-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;
}
#microsoft-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,
.microsoft-translated,
.microsoft-translated *,
.translating-indicator,
.translating-indicator *,
#microsoft-status-msg,
#microsoft-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"] *,
.script-element,
.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;
}
/* 为特定元素添加额外的防护 */
[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;
}
`;
document.head.appendChild(style);
console.log('已更新翻译颜色样式:', translationTextColor);
}
// 根据设置更新翻译功能
function updateWidgetBasedOnSettings() {
console.log('更新翻译设置,当前状态:', {
widgetEnabled,
isWhitelistedDomain,
isUsingSiteSpecificEngine,
isRussianContent,
russianContentRatio: (russianContentRatio * 100).toFixed(1) + '%'
});
if (autoTranslateTimeout) {
clearTimeout(autoTranslateTimeout);
autoTranslateTimeout = null;
}
stopAllObservations();
const shouldShowWidget = isWhitelistedDomain || widgetEnabled;
if (shouldShowWidget) {
console.log('启用微软翻译器');
initMicrosoftTranslator();
} else {
console.log('翻译已禁用');
deactivateAllTranslators();
}
registerMenuCommands();
}
// 初始化微软翻译器
async function initMicrosoftTranslator() {
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();
deactivateMicrosoftTranslator();
hideTranslatingIndicator();
console.log('所有翻译器已停用');
}
// 停止所有观察器
function stopAllObservations() {
stopDOMObservation();
stopScrollCheck();
stopContinuousTranslation();
}
// 注册菜单命令
function registerMenuCommands() {
unregisterMenuCommands();
// 主开关命令
let mainMenuText = widgetEnabled ? '❌ 关闭自动翻译' : '✅ 开启自动翻译';
mainMenuText += ` (微软)`;
if (isRussianContent) mainMenuText += ' 🇷🇺';
if (isUsingSiteSpecificEngine) mainMenuText += ' 🌐';
try {
mainMenuCommandId = GM_registerMenuCommand(mainMenuText, toggleWidget, 't');
console.log('主菜单命令注册成功:', mainMenuText);
} catch (e) {
console.error('主菜单命令注册失败:', e);
}
// 白名单命令
const whitelistText = isWhitelistedDomain ?
'⭐ 从白名单中移除' : '☆ 添加到白名单';
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 (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 translation-settings-modal 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 translation-settings-content 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';
let statusHTML = `
当前状态
微软翻译`;
if (isUsingSiteSpecificEngine) {
statusHTML += ` 网站专用`;
}
statusHTML += `
`;
if (isRussianSite) {
statusHTML += `${isRussianContent ? '检测到俄语内容 🇷🇺' : '俄语域名'}
`;
}
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 ? '微软翻译' : '无'}
`;
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 active';
microsoftBtn.innerHTML = `🔵微软`;
microsoftBtn.setAttribute('data-no-translate', 'true');
microsoftBtn.setAttribute('translate', 'no');
microsoftBtn.setAttribute('data-protect-translation', 'true');
microsoftBtn.className += ' no-translate script-element';
microsoftBtn.disabled = true;
microsoftBtn.style.cursor = 'default';
microsoftBtn.style.opacity = '0.7';
engineSelection.appendChild(microsoftBtn);
// 清除按钮
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 script-element';
if (isSiteHasEngine) {
clearBtn.addEventListener('click', function() {
clearSiteSpecificEngine();
showStatusMessage('已清除当前网站专用引擎', 2000);
showSettings();
});
}
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 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 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 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 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 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 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 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 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 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 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 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');
});
}
// =============== 微软翻译器相关函数 ===============
// 获取微软翻译认证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);
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 === '正在翻译页面...') {
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 === '翻译完成') {
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 === '翻译失败') {
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)';
}
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', 'script-element');
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)';
}
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',
'.translating-indicator',
'.translation-settings-modal',
'[data-no-translate]',
'.no-translate',
'.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'
];
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('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;
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',
'.translating-indicator',
'.translation-settings-modal',
'[data-no-translate]',
'.no-translate',
'.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'
];
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('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;
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', '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', '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', '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',
'.translating-indicator',
'.translation-settings-modal',
'[data-no-translate]',
'.no-translate',
'.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'
];
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('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;
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变化监听相关函数 ===============
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);
}
}
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 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();
let message = `微软翻译已开启`;
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 === 'S') {
e.preventDefault();
showSettings();
}
if (e.ctrlKey && e.shiftKey && e.key === 'T') {
e.preventDefault();
if (microsoftTranslatorActive) {
translateWholePage();
}
}
}, true);
}
// 显示状态提示
function showStatusMessage(message, duration = 2000) {
const existingMsg = document.getElementById('microsoft-status-msg');
if (existingMsg) existingMsg.remove();
const msgDiv = document.createElement('div');
msgDiv.id = 'microsoft-status-msg';
msgDiv.innerHTML = message;
msgDiv.setAttribute('data-no-translate', 'true');
msgDiv.setAttribute('translate', 'no');
msgDiv.setAttribute('data-protect-translation', 'true');
msgDiv.className = 'no-translate 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)';
} else if (message.includes('俄语内容') || message.includes('俄语网站')) {
msgDiv.style.background = 'rgba(255, 61, 0, 0.95)';
} else if (message.includes('专用引擎')) {
msgDiv.style.background = 'rgba(108, 92, 231, 0.95)';
} else if (message.includes('翻译中') || message.includes('翻译完成') || message.includes('✓')) {
msgDiv.style.background = 'rgba(0, 184, 148, 0.95)';
} else if (message.includes('✗') || message.includes('失败')) {
msgDiv.style.background = 'rgba(255, 71, 87, 0.95)';
} else if (message.includes('颜色')) {
msgDiv.style.background = 'rgba(255, 159, 67, 0.95)';
}
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();
}
})();