Greasy Fork

Greasy Fork is available in English.

翻译机

该脚本用于翻译各类常用社交网站为中文,不会经过中间服务器。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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 'Translation failed: Google Translate returned an abnormal response.';
    } catch (err) {
        console.error('[翻译机] Google Translate failed:', err);
        return 'Translation failed: Google Translate request error.';
    }
}

async function translate_ggm(raw) {
    console.warn('[翻译机] Google Translate mobile: Function not implemented.');
    return 'Function not implemented: Google Translate mobile';
}

async function translate_tencent(raw) {
    console.warn('[翻译机] Tencent Translate: Function not implemented.');
    return 'Function not implemented: Tencent Translate';
}

async function translate_tencentai(raw) {
    console.warn('[翻译机] Tencent AI Translate: Function not implemented.');
    return 'Function not implemented: Tencent AI Translate';
}

async function translate_youdao_mobile(raw) {
    console.warn('[翻译机] Youdao Translate mobile: Function not implemented.');
    return 'Function not implemented: Youdao Translate mobile';
}

async function translate_baidu(raw) {
    console.warn('[翻译机] Baidu Translate: Function not implemented.');
    return 'Function not implemented: Baidu Translate';
}

async function translate_caiyun(raw) {
    console.warn('[翻译机] Caiyun Translate: Function not implemented.');
    return 'Function not implemented: Caiyun Translate';
}

async function translate_biying(raw) {
    console.warn('[翻译机] Bing Translate: Function not implemented.');
    return 'Function not implemented: Bing Translate';
}

async function translate_papago(raw) {
    console.warn('[翻译机] Papago Translate: Function not implemented.');
    return 'Function not implemented: Papago Translate';
}

async function translate_alibaba(raw) {
    console.warn('[翻译机] Alibaba Translate: Function not implemented.');
    return 'Function not implemented: Alibaba Translate';
}

async function translate_icib(raw) {
    console.warn('[翻译机] IciBa Translate: Function not implemented.');
    return 'Function not implemented: IciBa Translate';
}

async function translate_deepl(raw) {
    console.warn('[翻译机] Deepl Translate: Function not implemented.');
    return 'Function not implemented: Deepl Translate';
}

// --- Startup Functions (placeholders) ---
async function translate_tencent_startup() {
    console.log('[翻译机] Tencent Translate startup: No additional startup steps required.');
    return Promise.resolve();
}
async function translate_caiyun_startup() {
    console.log('[翻译机] Caiyun Translate startup: No additional startup steps required.');
    return Promise.resolve();
}
async function translate_papago_startup() {
    console.log('[翻译机] Papago Translate startup: No additional startup steps required.');
    return Promise.resolve();
}
async function translate_gg_startup() {
    console.log('[翻译机] Google Translate startup: No additional startup steps required.');
    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(`【翻译机】Detected URL change, now using 【${newRule.name}】 rule`);
                } else {
                    console.log("【翻译机】Detected URL change, no matching rule currently");
                }
                activeRule = newRule;
                if (mainIntervalId) {
                    clearInterval(mainIntervalId);
                    mainIntervalId = null;
                }
                if (activeRule) {
                    PromiseRetryWrap(startup[GM_getValue('translate_choice', '谷歌翻译')] || translate_gg_startup).then(() => {
                        console.log(`【翻译机】Starting main translation loop for ${activeRule.name}`);
                        mainIntervalId = setInterval(main, 200);
                    }).catch(err => {
                        console.error('[翻译机] Failed to start translation engine:', err);
                    });
                }
            }
        }
    }, 500);

    console.log(activeRule ? `【翻译机】Using 【${activeRule.name}】 rule` : "【翻译机】No matching rule currently");
    console.log(`Current 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(`[翻译机] Unknown translation engine: ${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(`[翻译机] Translation failed for element:`, element, `Error:`, 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 in main loop:', error);
        } finally {
            isTranslating = false;
        }
    }

    if (activeRule) {
        PromiseRetryWrap(startup[GM_getValue('translate_choice', '谷歌翻译')] || translate_gg_startup).then(() => {
            console.log(`【翻译机】Starting main translation loop for ${activeRule.name}`);
            mainIntervalId = setInterval(main, 200);
        }).catch(err => {
            console.error('[翻译机] Failed to start translation engine:', 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 = 'Translation failed';

    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(`[翻译机] Detected simplified Chinese, skipping translation.`);
            return new Promise(() => {});
        }
        if (enable_pass_lang_cht && (result === 'cht' || result === 'zh-tw')) {
            console.log(`[翻译机] Detected traditional Chinese, skipping translation.`);
            return new Promise(() => {});
        }
        return result;
    } catch (err) {
        console.error("[翻译机] Language detection failed:", 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;
    }
}