Greasy Fork is available in English.

翻译机

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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 'Перевод не удался: 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;
    }
}