您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
该脚本用于翻译各类常用社交网站为中文,不会经过中间服务器。
// ==UserScript== // @name 翻译机 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 该脚本用于翻译各类常用社交网站为中文,不会经过中间服务器。 // @author HolynnChen // @license MIT // @match *://*.twitter.com/* // @match *://*.x.com/* // @match *://*.youtube.com/* // @match *://*.facebook.com/* // @match *://*.reddit.com/* // @match *://*.5ch.net/* // @match *://*.discord.com/* // @match *://*.telegram.org/* // @match *://*.quora.com/* // @match *://*.tiktok.com/* // @match *://*.instagram.com/* // @match *://*.threads.net/* // @match *://*.github.com/* // @match *://*.bsky.app/* // @connect fanyi.baidu.com // @connect translate.google.com // @connect ifanyi.iciba.com // @connect www.bing.com // @connect fanyi.youdao.com // @connect dict.youdao.com // @connect m.youdao.com // @connect api.interpreter.caiyunai.com // @connect papago.naver.com // @connect fanyi.qq.com // @connect translate.alibaba.com // @connect www2.deepl.com // @connect transmart.qq.com // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/base64.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/libs/lz-string.min.js // @require https://cdn.jsdelivr.net/gh/Tampermonkey/utils@3b32b826e84ccc99a0a3e3d8d6e5ce0fa9834f23/requires/gh_2215_make_GM_xhr_more_parallel_again.js // @run-at document-idle // ==/UserScript== // --- Polyfills and Helper Functions --- // Mock for Base64 if not exposed globally by js-base64 if (typeof Base64 === 'undefined' && typeof window.Base64 !== 'undefined') { var Base64 = window.Base64; } // Basic implementation of CompressMergeSession (needs lz-string) function CompressMergeSession(storage) { if (typeof LZString === 'undefined') { console.warn("[翻译机] LZString is not loaded. Compression will be skipped."); return storage; } return { getItem: function(key) { const compressed = storage.getItem(key); try { return compressed ? LZString.decompressFromUTF16(compressed) : null; } catch (e) { console.error("[翻译机] Failed to decompress item:", key, e); // If decompression fails, clear the item to avoid future errors storage.removeItem(key); return null; } }, setItem: function(key, value) { try { storage.setItem(key, LZString.compressToUTF16(value)); } catch (e) { console.error("[翻译机] Failed to compress and set item:", key, e); } }, removeItem: function(key) { storage.removeItem(key); }, clear: function() { storage.clear(); } }; } // GM_xmlhttpRequest wrapper for Promises function Request(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...options, onload: (response) => { if (response.status >= 200 && response.status < 300) { resolve(response); } else { reject(new Error(`Request failed with status ${response.status}: ${response.statusText || 'Unknown error'}`)); } }, onerror: (error) => { reject(error); }, ontimeout: () => { reject(new Error("Request timed out")); } }); }); } // Basic Promise Retry Wrap (simplified) async function PromiseRetryWrap(promiseFunc, retries = 3, delay = 1000) { if (typeof promiseFunc !== 'function') { return Promise.resolve(); } for (let i = 0; i < retries; i++) { try { return await promiseFunc(); } catch (error) { console.warn(`[翻译机] Promise retry failed (attempt ${i + 1}/${retries}):`, error); if (i < retries - 1) { await new Promise(res => setTimeout(res, delay)); } else { throw error; } } } } // --- GM_registerMenuCommand --- GM_registerMenuCommand('重置控制面板位置(刷新应用)', () => { GM_setValue('position_top', '9px'); GM_setValue('position_right', '9px'); alert('Контрольная панель была сброшена. Обновите страницу, чтобы применить изменения.'); }); GM_registerMenuCommand('全局隐藏/展示悬浮球(刷新应用)', () => { const currentState = GM_getValue('show_translate_ball', true); GM_setValue('show_translate_ball', !currentState); alert(`Плавающая кнопка ${!currentState ? 'показана' : 'скрыта'}. Пожалуйста, обновите страницу, чтобы применить изменения.`); }); // --- Translation Provider Functions (placeholders) --- // ВАЖНО: Эти функции являются заглушками. Вы ДОЛЖНЫ заполнить // фактическую логику запросов к API (URL-адреса, заголовки, данные и разбор ответа) для каждого провайдера. async function translate_gg(raw) { const options = { method: "GET", url: `https://translate.google.com/translate_a/single?client=gtx&sl=auto&tl=zh-CN&dt=t&q=` + encodeURIComponent(raw) }; try { const res = await Request(options); const data = JSON.parse(res.responseText); if (data && data[0] && data[0][0] && data[0][0][0]) { return data[0].map(segment => segment[0]).join(''); } return 'Перевод не удался: Google Translate вернул некорректный ответ.'; } catch (err) { console.error('[翻译机] Google Translate failed:', err); return 'Перевод не удался: ошибка запроса Google Translate.'; } } async function translate_ggm(raw) { console.warn('[翻译机] Google Translate mobile: Функция не реализована.'); return 'Функция не реализована: Google Translate mobile'; } async function translate_tencent(raw) { console.warn('[翻译机] Tencent Translate: Функция не реализована.'); return 'Функция не реализована: Tencent Translate'; } async function translate_tencentai(raw) { console.warn('[翻译机] Tencent AI Translate: Функция не реализована.'); return 'Функция не реализована: Tencent AI Translate'; } async function translate_youdao_mobile(raw) { console.warn('[翻译机] Youdao Translate mobile: Функция не реализована.'); return 'Функция не реализована: Youdao Translate mobile'; } async function translate_baidu(raw) { console.warn('[翻译机] Baidu Translate: Функция не реализована.'); return 'Функция не реализована: Baidu Translate'; } async function translate_caiyun(raw) { console.warn('[翻译机] Caiyun Translate: Функция не реализована.'); return 'Функция не реализована: Caiyun Translate'; } async function translate_biying(raw) { console.warn('[翻译机] Bing Translate: Функция не реализована.'); return 'Функция не реализована: Bing Translate'; } async function translate_papago(raw) { console.warn('[翻译机] Papago Translate: Функция не реализована.'); return 'Функция не реализована: Papago Translate'; } async function translate_alibaba(raw) { console.warn('[翻译机] Alibaba Translate: Функция не реализована.'); return 'Функция не реализована: Alibaba Translate'; } async function translate_icib(raw) { console.warn('[翻译机] IciBa Translate: Функция не реализована.'); return 'Функция не реализована: IciBa Translate'; } async function translate_deepl(raw) { console.warn('[翻译机] Deepl Translate: Функция не реализована.'); return 'Функция не реализована: Deepl Translate'; } // --- Startup Functions (placeholders) --- async function translate_tencent_startup() { console.log('[翻译机] Tencent Translate startup: Дополнительные шаги запуска не требуются.'); return Promise.resolve(); } async function translate_caiyun_startup() { console.log('[翻译机] Caiyun Translate startup: Дополнительные шаги запуска не требуются.'); return Promise.resolve(); } async function translate_papago_startup() { console.log('[翻译机] Papago Translate startup: Дополнительные шаги запуска не требуются.'); return Promise.resolve(); } async function translate_gg_startup() { console.log('[翻译机] Google Translate startup: Дополнительные шаги запуска не требуются.'); return Promise.resolve(); } // --- Core Configuration and Data --- const transdict = { '谷歌翻译': translate_gg, '谷歌翻译mobile': translate_ggm, '腾讯翻译': translate_tencent, '腾讯AI翻译': translate_tencentai, '有道翻译mobile': translate_youdao_mobile, '百度翻译': translate_baidu, '彩云小译': translate_caiyun, '必应翻译': translate_biying, 'Papago翻译': translate_papago, '阿里翻译': translate_alibaba, '爱词霸翻译': translate_icib, 'Deepl翻译': translate_deepl, '关闭翻译': () => { return Promise.resolve(''); } }; const startup = { '谷歌翻译': translate_gg_startup, '谷歌翻译mobile': translate_gg_startup, '腾讯翻译': translate_tencent_startup, '彩云小译': translate_caiyun_startup, 'Papago翻译': translate_papago_startup }; const baseoptions = { 'enable_pass_lang': { declare: 'Не переводить китайский (упрощённый)', default_value: true, change_func: self => { if (self.checked) sessionStorage.clear(); console.log('[翻译机] Setting: Do not translate Chinese (Simplified) updated.'); } }, 'enable_pass_lang_cht': { declare: 'Не переводить китайский (традиционный)', default_value: true, change_func: self => { if (self.checked) sessionStorage.clear(); console.log('[翻译机] Setting: Do not translate Chinese (Traditional) updated.'); } }, 'remove_url': { declare: 'Автоматически фильтровать URL-адреса', default_value: true, }, 'show_info': { declare: 'Показывать источник перевода', default_value: true, option_enable: true }, 'fullscrenn_hidden': { declare: 'Не показывать в полноэкранном режиме', default_value: true, }, 'replace_translate': { declare: 'Заменяющий перевод', default_value: false, option_enable: true }, 'compress_storage':{ declare: 'Сжимать кэш', default_value: false, change_func: () => { alert('Настройка сжатия кэша изменена, пожалуйста, обновите страницу, чтобы применить изменения.'); } } }; const enable_pass_lang = GM_getValue('enable_pass_lang', baseoptions.enable_pass_lang.default_value); const enable_pass_lang_cht = GM_getValue('enable_pass_lang_cht', baseoptions.enable_pass_lang_cht.default_value); const remove_url = GM_getValue('remove_url', baseoptions.remove_url.default_value); const show_info = GM_getValue('show_info', baseoptions.show_info.default_value); const fullscrenn_hidden = GM_getValue('fullscrenn_hidden', baseoptions.fullscrenn_hidden.default_value); const replace_translate = GM_getValue('replace_translate', baseoptions.replace_translate.default_value); const compress_storage = GM_getValue('compress_storage', baseoptions.compress_storage.default_value); const globalProcessingSave = []; const sessionStorage = compress_storage ? CompressMergeSession(window.sessionStorage) : window.sessionStorage; const p = window.trustedTypes !== undefined ? window.trustedTypes.createPolicy('translator', { createHTML: (string) => string }) : { createHTML: (string) => string }; // --- UI Panel Initialization --- function initPanel() { let choice = GM_getValue('translate_choice', '谷歌翻译'); let select = document.createElement("select"); select.className = 'js_translate'; select.style = 'height:35px;width:100px;background-color:#fff;border-radius:17.5px;text-align-last:center;color:#000000;margin:5px 0'; select.onchange = () => { GM_setValue('translate_choice', select.value); title.innerText = "Панель управления (обновите для применения)"; alert('Переводчик изменен, пожалуйста, обновите страницу, чтобы применить изменения.'); }; for (let i in transdict) { select.innerHTML = p.createHTML(select.innerHTML + '<option value="' + i + '">' + i + '</option>'); } select.querySelector(`option[value="${choice}"]`).selected = true; let enable_details = document.createElement('details'); enable_details.innerHTML = p.createHTML("<summary>Правила активации</summary>"); for (let i of rules) { let temp_p = document.createElement('p'); let temp_input = document.createElement('input'); temp_input.type = 'checkbox'; temp_input.name = i.name; if (GM_getValue("enable_rule:" + temp_input.name, true)) { temp_input.setAttribute('checked', 'true'); } temp_p.appendChild(temp_input); temp_p.innerHTML = p.createHTML(temp_p.innerHTML + `<span>${i.name}</span>`); enable_details.appendChild(temp_p); } let current_details = document.createElement('details'); current_details.className = 'current-rule-details'; let mask = document.createElement('div'); let dialog = document.createElement("div"); let js_dialog = document.createElement("div"); let title = document.createElement('p'); let shadowContainer = document.createElement('div'); shadowContainer.style = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 99999; pointer-events: none;"; document.body.appendChild(shadowContainer); let shadow = shadowContainer.attachShadow({ mode: "open" }); shadow.appendChild(mask); mask.appendChild(dialog); dialog.appendChild(js_dialog); js_dialog.appendChild(title); let select_p = document.createElement('p'); select_p.appendChild(select); js_dialog.appendChild(select_p); js_dialog.appendChild(enable_details); js_dialog.appendChild(current_details); mask.style = "display: none;position: absolute;height: 100%;width: 100%;z-index: 1;top: 0;left: 0;overflow: hidden;background-color: rgba(0,0,0,0.4);justify-content: center;align-items: center;pointer-events: auto;"; mask.addEventListener('click', event => { if (event.target === mask) mask.style.display = 'none'; }); dialog.style = 'padding:0;border-radius:10px;background-color: #fff;box-shadow: 0 0 5px 4px rgba(0,0,0,0.3);'; js_dialog.style = "min-height:10vh;min-width:10vw;display:flex;flex-direction:column;align-items:center;padding:10px;border-radius:4px;color:#000"; title.style = 'margin:5px 0;font-size:20px;'; title.innerText = "Панель управления"; for (let i in baseoptions) { let temp_p = document.createElement('p'); let temp_input = document.createElement('input'); temp_p.style = "display:flex;align-items: center;margin:5px 0"; temp_input.type = 'checkbox'; temp_input.name = i; temp_input.id = `base-option-${i}`; temp_p.appendChild(temp_input); temp_p.innerHTML = p.createHTML(temp_p.innerHTML + `<label for="base-option-${i}">${baseoptions[i].declare}</label>`); js_dialog.appendChild(temp_p); } for (let i of js_dialog.querySelectorAll('input[type="checkbox"]')) { if (i.name && baseoptions[i.name]) { i.onchange = () => { title.innerText = "Панель управления (обновите для применения)"; GM_setValue(i.name, i.checked); if (baseoptions[i.name].change_func) { baseoptions[i.name].change_func(i); } }; i.checked = GM_getValue(i.name, baseoptions[i.name].default_value); } } for (let i of enable_details.querySelectorAll('input[type="checkbox"]')) { i.onchange = () => { title.innerText = "Панель управления (обновите для применения)"; GM_setValue('enable_rule:' + i.name, i.checked); }; } let open = document.createElement('div'); open.style = `z-index:9999;height:35px;width:35px;background-color:#fff;position:fixed;border:1px solid rgba(0,0,0,0.2);border-radius:17.5px;right:${GM_getValue('position_right', '9px')};top:${GM_getValue('position_top', '9px')};text-align:center;color:#000000;display:flex;align-items:center;justify-content:center;cursor: grab;font-size:15px;user-select:none;visibility: visible;pointer-events: auto;`; open.innerHTML = p.createHTML("Я"); const renderCurrentRule = () => { current_details.style.display = "none"; current_details.innerHTML = p.createHTML(''); const currentRule = GetActiveRule(); if (currentRule) { current_details.style.display = "block"; current_details.innerHTML = p.createHTML(`<summary>Текущая активная - ${currentRule.name}</summary>`); for (const option of currentRule.options) { const fieldset = document.createElement("fieldset"); fieldset.innerHTML = p.createHTML(`<legend>${option.name}</legend>`); current_details.appendChild(fieldset); const enableDiv = document.createElement('div'); enableDiv.style = "display:flex;align-items:center; margin-bottom: 5px;"; const enableInput = document.createElement('input'); enableInput.type = 'checkbox'; const enableKey = `enable_option:${currentRule.name}-${option.name}`; enableInput.checked = GM_getValue(enableKey, true); enableInput.onchange = () => { title.innerText = "Панель управления (обновите для применения)"; GM_setValue(enableKey, enableInput.checked); }; enableDiv.appendChild(enableInput); enableDiv.innerHTML = p.createHTML(enableDiv.innerHTML + `<span>Включить перевод</span>`); fieldset.appendChild(enableDiv); for (const key in baseoptions) { if (!baseoptions[key].option_enable) { continue; } const optionDiv = document.createElement('div'); optionDiv.style = "margin-top: 10px;"; optionDiv.innerHTML = p.createHTML(`<span>${baseoptions[key].declare}</span>`); const baseValueList = [["", "По умолчанию"], ["true", "Включить"], ["false", "Отключить"]]; const radioGroupDiv = document.createElement('div'); radioGroupDiv.style = "display:flex; gap: 10px;"; for (const value of baseValueList) { const radioInput = document.createElement('input'); radioInput.type = "radio"; radioInput.value = value[0]; radioInput.name = `${key}:${currentRule.name}-${option.name}`; radioInput.id = `${key}-${currentRule.name}-${option.name}-${value[0]}`; const currentSetting = GM_getValue(`option_setting:${radioInput.name}`, ''); if (currentSetting.toString() === radioInput.value) { radioInput.checked = true; } radioInput.onchange = () => { title.innerText = "Панель управления (обновите для применения)"; switch (radioInput.value) { case 'true': GM_setValue(`option_setting:${radioInput.name}`, true); break; case 'false': GM_setValue(`option_setting:${radioInput.name}`, false); break; case '': GM_deleteValue(`option_setting:${radioInput.name}`); break; } }; const radioLabel = document.createElement('label'); radioLabel.htmlFor = radioInput.id; radioLabel.textContent = value[1]; radioGroupDiv.appendChild(radioInput); radioGroupDiv.appendChild(radioLabel); } optionDiv.appendChild(radioGroupDiv); fieldset.appendChild(optionDiv); } } } }; open.onclick = () => { renderCurrentRule(); mask.style.display = 'flex'; }; let isDragging = false; let offsetX, offsetY; open.addEventListener("mousedown", (e) => { isDragging = true; open.style.cursor = 'grabbing'; offsetX = e.clientX - open.getBoundingClientRect().left; offsetY = e.clientY - open.getBoundingClientRect().top; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; let newX = e.clientX - offsetX; let newY = e.clientY - offsetY; newX = Math.max(0, Math.min(newX, window.innerWidth - open.offsetWidth)); newY = Math.max(0, Math.min(newY, window.innerHeight - open.offsetHeight)); open.style.left = `${newX}px`; open.style.top = `${newY}px`; open.style.right = 'auto'; }); document.addEventListener("mouseup", () => { if (isDragging) { isDragging = false; open.style.cursor = 'grab'; GM_setValue("position_right", `${window.innerWidth - open.getBoundingClientRect().right}px`); GM_setValue("position_top", `${open.getBoundingClientRect().top}px`); } }); open.addEventListener("touchstart", ev => { ev.stopImmediatePropagation(); ev.preventDefault(); const touch = ev.touches[0]; open._tempTouch = {}; const rect = open.getBoundingClientRect(); open._tempTouch.offsetX = touch.clientX - rect.left; open._tempTouch.offsetY = touch.clientY - rect.top; open._tempIsMove = false; }, { passive: false }); open.addEventListener("touchmove", ev => { ev.stopImmediatePropagation(); ev.preventDefault(); const touch = ev.touches[0]; let newX = touch.clientX - open._tempTouch.offsetX; let newY = touch.clientY - open._tempTouch.offsetY; newX = Math.max(0, Math.min(newX, window.innerWidth - open.offsetWidth)); newY = Math.max(0, Math.min(newY, window.innerHeight - open.offsetHeight)); open.style.left = `${newX}px`; open.style.top = `${newY}px`; open.style.right = 'auto'; open._tempIsMove = true; }, { passive: false }); open.addEventListener("touchend", ev => { ev.stopImmediatePropagation(); GM_setValue("position_right", `${window.innerWidth - open.getBoundingClientRect().right}px`); GM_setValue("position_top", `${open.getBoundingClientRect().top}px`); if (!open._tempIsMove) { renderCurrentRule(); mask.style.display = 'flex'; } open._tempIsMove = false; }); shadow.appendChild(open); if (fullscrenn_hidden) { window.document.addEventListener('fullscreenchange', () => { open.style.display = window.document.fullscreenElement ? "none" : "flex"; }); } const storedRight = GM_getValue('position_right', '9px'); const storedTop = GM_getValue('position_top', '9px'); open.style.right = storedRight; open.style.top = storedTop; open.style.left = 'auto'; } // --- Rule Definitions --- const rules = [ { name: 'Twitter/X General', matcher: /https:\/\/([a-zA-Z.]*?\.|)(twitter|x)\.com/, options: [ { name: "Tweets", selector: baseSelector('div[dir="auto"][lang]'), textGetter: baseTextGetter, textSetter: options => { options.element.style.setProperty('-webkit-line-clamp', 'unset', 'important'); options.element.style.setProperty('max-height', 'unset', 'important'); baseTextSetter(options).style.display = 'flex'; } }, { name: "Background Info", selector: baseSelector('div[data-testid=birdwatch-pivot]>div[dir=ltr]'), textGetter: baseTextGetter, textSetter: options => { options.element.style.setProperty('-webkit-line-clamp', 'unset', 'important'); options.element.style.setProperty('max-height', 'unset', 'important'); baseTextSetter(options).style.display = 'flex'; } } ] }, { name: 'YouTube PC General', matcher: /https:\/\/(www\.|)youtube\.com\/(watch|shorts|results\?)/, options: [ { name: "Comments Section", selector: baseSelector("#content>#content-text"), textGetter: baseTextGetter, textSetter: options => { baseTextSetter(options); let parentCollapsed = options.element.closest('[collapsed]'); if(parentCollapsed) parentCollapsed.removeAttribute('collapsed'); } }, { name: "Video Description", selector: baseSelector("#content>#description>.content,.ytd-text-inline-expander>.yt-core-attributed-string"), textGetter: baseTextGetter, textSetter: options => { baseTextSetter(options); let parentCollapsed = options.element.closest('[collapsed]'); if(parentCollapsed) parentCollapsed.removeAttribute('collapsed'); } }, { name: "CC Subtitles", selector: baseSelector(".ytp-caption-segment"), textGetter: baseTextGetter, textSetter: baseTextSetter } ] }, { name: 'YouTube Mobile General', matcher: /https:\/\/m\.youtube\.com\/watch/, options: [ { name: "Comments Section", selector: baseSelector(".comment-text.user-text"), textGetter: baseTextGetter, textSetter: baseTextSetter, }, { name: "Video Description", selector: baseSelector(".slim-video-metadata-description"), textGetter: baseTextGetter, textSetter: baseTextSetter, } ] }, { name: 'YouTube Shorts', matcher: /https:\/\/(www|m)\.youtube\.com\/shorts/, options: [ { name: "Comments Section", selector: baseSelector("#comment-content #content-text,.comment-content .comment-text"), textGetter: baseTextGetter, textSetter: baseTextSetter, } ] }, { name: 'YouTube Community', matcher: /https:\/\/(www|m)\.youtube\.com\/(.*?\/community|post)/, options: [ { name: "Comments Section", selector: baseSelector("#post #content #content-text,#comment #content #content-text,#replies #content #content-text"), textGetter: baseTextGetter, textSetter: options => { baseTextSetter(options); let parentCollapsed = options.element.closest('[collapsed]'); if(parentCollapsed) parentCollapsed.removeAttribute('collapsed'); } } ] }, { name: 'Facebook General', matcher: /https:\/\/www.facebook.com\/.+/, options: [ { name: "Post Content", selector: baseSelector("div[data-ad-comet-preview=message],div[role=article] div[id]"), textGetter: baseTextGetter, textSetter: options => setTimeout(baseTextSetter, 0, options), }, { name: "Comments Section", selector: baseSelector("div[role=article] div>span[dir=auto][lang]"), textGetter: baseTextGetter, textSetter: options => setTimeout(baseTextSetter, 0, options), } ] }, { name: 'Reddit General', matcher: /https:\/\/www.reddit.com\/.*/, options: [ { name: 'Post Title', selector: baseSelector("*[slot=title][id|=post-title]"), textGetter: baseTextGetter, textSetter: baseTextSetter, }, { name: 'Post Content', selector: baseSelector("div[slot=text-body]>div>div[id*=-post-rtjson-content]"), textGetter: baseTextGetter, textSetter: baseTextSetter, }, { name: 'Comments Section', selector: baseSelector("div[slot=comment]>div[id$=-post-rtjson-content]"), textGetter: baseTextGetter, textSetter: baseTextSetter, } ] }, { name: '5ch Comments', matcher: /http(|s):\/\/(.*?\.|)5ch.net\/.*/, options: [ { name: "Title", selector: baseSelector('.post>.post-content,#threadtitle,.thread_title'), textGetter: baseTextGetter, textSetter: baseTextSetter, }, { name: "Content", selector: baseSelector('.threadview_response_body'), textGetter: baseTextGetter, textSetter: baseTextSetter, } ] }, { name: 'Discord Chat', matcher: /https:\/\/discord.com\/.+/, options: [ { name: "Chat Content", selector: baseSelector('div[class*=messageContent]'), textGetter: baseTextGetter, textSetter: baseTextSetter, } ] }, { name: 'Telegram Chat (New)', matcher: /https:\/\/.*?.telegram.org\/(a|z)\//, options: [ { name: "Chat Content", selector: baseSelector('p.text-content[dir=auto],div.text-content'), textGetter: e => Array.from(e.childNodes).filter(item => !item.className).map(item => item.nodeName === "BR" ? "\n" : item.textContent).join(''), textSetter: baseTextSetter, } ] }, { name: 'Telegram Chat (Old)', matcher: /https:\/\/.*?.telegram.org\/.+/, options: [ { name: "Chat Content", selector: baseSelector('div.message[dir=auto],div.im_message_text'), textGetter: e => Array.from(e.childNodes).filter(item => !item.className || item.className === 'translatable-message').map(item => item.nodeValue || item.innerText).join(" "), textSetter: baseTextSetter, } ] }, { name: 'Quora General', matcher: /https:\/\/www.quora.com/, options: [ { name: "Title", selector: baseSelector(".puppeteer_test_question_title>span>span"), textGetter: baseTextGetter, textSetter: options => { options.element.parentNode.parentNode.style.setProperty('-webkit-line-clamp', 'unset', 'important'); options.element.parentNode.parentNode.style.setProperty('max-height', 'unset', 'important'); baseTextSetter(options).style.display = 'flex'; }, }, { name: "Post Content", selector: baseSelector('div.q-text>span>span.q-box:has(p.q-text),div.q-box>div.q-box>div.q-text>span.q-box:has(p.q-text)'), textGetter: baseTextGetter, textSetter: baseTextSetter, } ] }, { name: 'TikTok Comments', matcher: /https:\/\/www.tiktok.com/, options: [ { name: "Comments Section", selector: baseSelector('p[data-e2e|=comment-level]'), textGetter: baseTextGetter, textSetter: baseTextSetter, } ] }, { name: 'Instagram Comments', matcher: /https:\/\/www.instagram.com/, options: [ { name: "Comments Section", selector: baseSelector('li>div>div>div>div>span[dir=auto]'), textGetter: baseTextGetter, textSetter: baseTextSetter, } ] }, { name: 'Threads', matcher: /https:\/\/www.threads.net/, options: [ { name: "Posts", selector: baseSelector('div[data-pressable-container=true][data-interactive-id]>div>div:last-child>div>div:has(span[dir=auto]):not(:has(div[role=button]))'), textGetter: baseTextGetter, textSetter: baseTextSetter, } ] }, { name: 'GitHub', matcher: /https:\/\/github.com\/.+\/.+\/\w+\/\d+/, options: [ { name: "Issues", selector: baseSelector(".edit-comment-hide > task-lists > table > tbody > tr > td > p",items=>items.filter(i=>{ const nodeNameList = [...new Set([...i.childNodes].map(i=>i.nodeName))]; return nodeNameList.length>1 || (nodeNameList.length == 1 && nodeNameList[0] == "#text") })), textGetter: baseTextGetter, textSetter: baseTextSetter, }, { name: "Discussions", selector: baseSelector(".edit-comment-hide > task-lists > table > tbody > tr > td > p",items=>items.filter(i=>{ const nodeNameList=[...new Set([...i.childNodes].map(i=>i.nodeName))]; return nodeNameList.length>1 || (nodeNameList.length == 1 && nodeNameList[0] == "#text") })), textGetter: baseTextGetter, textSetter: baseTextSetter, }, ] }, { name: 'BSky', matcher: /https:\/\/bsky.app/, options: [ { name: "Homepage Posts", selector: baseSelector('div[dir=auto][data-testid=postText]'), textGetter: baseTextGetter, textSetter: baseTextSetter, }, { name: "Content Posts & Replies", selector: baseSelector('div[data-testid^="postThreadItem-by"] div[dir=auto][data-word-wrap]'), textGetter: baseTextGetter, textSetter: baseTextSetter, } ] }, ]; const GetActiveRule = () => rules.find(item => item.matcher.test(document.location.href) && GM_getValue('enable_rule:' + item.name, true)); // --- Main Execution Logic --- (function () { 'use strict'; let currentUrl = document.location.href; let activeRule = GetActiveRule(); let mainIntervalId = null; // Monitor URL changes for SPAs setInterval(() => { if (document.location.href !== currentUrl) { currentUrl = document.location.href; const newRule = GetActiveRule(); if (newRule !== activeRule) { if (newRule) { console.log(`【翻译机】Обнаружено изменение URL, теперь используется правило 【${newRule.name}】`); } else { console.log("【翻译机】Обнаружено изменение URL, в настоящее время нет подходящего правила."); } activeRule = newRule; if (mainIntervalId) { clearInterval(mainIntervalId); mainIntervalId = null; } if (activeRule) { PromiseRetryWrap(startup[GM_getValue('translate_choice', '谷歌翻译')] || translate_gg_startup).then(() => { console.log(`【翻译机】Запускается основной цикл перевода для ${activeRule.name}`); mainIntervalId = setInterval(main, 200); }).catch(err => { console.error('[翻译机] Не удалось запустить механизм перевода:', err); }); } } } }, 500); console.log(activeRule ? `【翻译机】Используется правило 【${activeRule.name}】` : "【翻译机】В настоящее время нет подходящего правила."); console.log(`Текущий URL: ${document.location.href}`); let processingQueue = []; let isTranslating = false; async function main() { if (!activeRule || isTranslating) return; isTranslating = true; try { const choice = GM_getValue('translate_choice', '谷歌翻译'); const translator = transdict[choice]; if (!translator) { console.warn(`[翻译机] Неизвестный движок перевода: ${choice}`); isTranslating = false; return; } for (const option of activeRule.options) { const enableOption = GM_getValue(`enable_option:${activeRule.name}-${option.name}`, true); if (!enableOption) { continue; } const elementsToTranslate = option.selector(); for (const element of elementsToTranslate) { if (globalProcessingSave.includes(element) || processingQueue.includes(element)) { continue; } const rawText = option.textGetter(element); const textToTranslate = remove_url ? url_filter(rawText) : rawText; if (textToTranslate.length === 0 || textToTranslate.trim().length === 0) { // Also check for empty after trim element.dataset.translate = "skipped_empty"; continue; } const cachedText = sessionStorage.getItem(`${choice}-${textToTranslate}`); if (cachedText) { const setterParams = { element: element, translatorName: choice, text: cachedText, rawText: rawText, rule: activeRule, option: option }; option.textSetter(setterParams); element.dataset.translate = "cached"; } else { processingQueue.push({ element, rawText, textToTranslate, option, choice }); globalProcessingSave.push(element); } } } while (processingQueue.length > 0) { const item = processingQueue.shift(); const { element, rawText, textToTranslate, option, choice } = item; try { const langCheckResult = await pass_lang(textToTranslate); if (langCheckResult instanceof Promise && typeof langCheckResult.then === 'function') { element.dataset.translate = "skipped_lang"; removeItem(globalProcessingSave, element); continue; } const translatedText = await translator(textToTranslate, langCheckResult); sessionStorage.setItem(`${choice}-${textToTranslate}`, translatedText); const setterParams = { element: element, translatorName: choice, text: translatedText, rawText: rawText, rule: activeRule, option: option }; option.textSetter(setterParams); element.dataset.translate = "translated"; } catch (err) { console.error(`[翻译机] Перевод элемента не удался:`, element, `Ошибка:`, err); const setterParams = { element: element, translatorName: choice, text: 'Translation failed', rawText: rawText, rule: activeRule, option: option }; option.textSetter(setterParams); element.dataset.translate = "translation_error"; } finally { removeItem(globalProcessingSave, element); } } } catch (error) { console.error('[翻译机] Ошибка в основном цикле:', error); } finally { isTranslating = false; } } if (activeRule) { PromiseRetryWrap(startup[GM_getValue('translate_choice', '谷歌翻译')] || translate_gg_startup).then(() => { console.log(`【翻译机】Запускается основной цикл перевода для ${activeRule.name}`); mainIntervalId = setInterval(main, 200); }).catch(err => { console.error('[翻译机] Не удалось запустить механизм перевода:', err); }); } if (GM_getValue('show_translate_ball', true)) { initPanel(); } })(); // --- Utility Functions --- function removeItem(arr, item) { const index = arr.indexOf(item); if (index > -1) arr.splice(index, 1); } function baseSelector(selector, customFilter) { return () => { const items = document.querySelectorAll(selector); let filterResult = Array.from(items).filter(item => { if (item.dataset.translate) return false; const nodes = item.querySelectorAll('[data-translate]'); if (nodes && Array.from(nodes).some(node => node.parentNode === item)) { return false; } return true; }); if (customFilter) { filterResult = customFilter(filterResult); } return filterResult; }; } function baseTextGetter(e) { return e.innerText ? e.innerText.trim() : ''; } function baseTextSetter({ element, translatorName, text, rawText, rule, option }) { if ((text || "").length === 0) text = 'Перевод не удался'; const currentReplaceTranslate = GM_getValue(`option_setting:replace_translate:${rule.name}-${option.name}`, replace_translate); const currentShowInfo = GM_getValue(`option_setting:show_info:${rule.name}-${option.name}`, show_info); const spanNode = document.createElement('span'); spanNode.style.whiteSpace = "pre-wrap"; spanNode.innerText = `${currentShowInfo ? "-----------" + translatorName + "-----------\n\n" : ""}` + text; spanNode.dataset.translate = "translated_content"; spanNode.className = "translate-processed-node"; spanNode.title = rawText; if (currentReplaceTranslate) { element.innerHTML = p.createHTML(''); element.appendChild(spanNode); } else { let originalContentSpan = element.querySelector('.original-content-wrapper'); if (!originalContentSpan) { originalContentSpan = document.createElement('span'); originalContentSpan.className = 'original-content-wrapper'; originalContentSpan.innerHTML = p.createHTML(element.innerHTML); element.innerHTML = p.createHTML(''); element.appendChild(originalContentSpan); } element.appendChild(spanNode); } element.style.setProperty('-webkit-line-clamp', 'unset', 'important'); element.style.setProperty('max-height', 'unset', 'important'); element.style.setProperty('overflow', 'visible', 'important'); return spanNode; } function url_filter(text) { return text.replace(/(https?|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/g, '').trim(); } async function pass_lang(raw) { if (!enable_pass_lang && !enable_pass_lang_cht) return; try { const result = await check_lang(raw); if (enable_pass_lang && result === 'zh') { console.log(`[翻译机] Обнаружен упрощённый китайский, перевод пропускается.`); return new Promise(() => {}); } if (enable_pass_lang_cht && (result === 'cht' || result === 'zh-tw')) { console.log(`[翻译机] Обнаружен традиционный китайский, перевод пропускается.`); return new Promise(() => {}); } return result; } catch (err) { console.error("[翻译机] Не удалось определить язык:", err); return; } } async function check_lang(raw) { const options = { method: "POST", url: 'https://fanyi.baidu.com/langdetect', data: 'query=' + encodeURIComponent(raw.replace(/[\uD800-\uDBFF]$/, "").slice(0, 50)), headers: { "Content-Type": "application/x-www-form-urlencoded", } }; try { const res = await Request(options); const data = JSON.parse(res.responseText); if (data && data.lan) { return data.lan; } throw new Error("Baidu language detection response malformed."); } catch (err) { console.error("[翻译机] Baidu language detection request failed:", err); throw err; } }