Переводчик

Этот скрипт используется для перевода различных популярных сайтов социальных сетей на китайский язык без использования промежуточного сервера.

// ==UserScript==
// @name Переводчик
// @namespace http://tampermonkey.net/
// @version 0.99
// @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-body
// ==/UserScript==

// --- Вспомогательные функции ---

/**
 * Функция для сжатия/распаковки данных для sessionStorage с использованием LZ-String.
 * @param {Storage} storage - Объект sessionStorage или localStorage.
 * @returns {object} Объект с методами getItem, setItem, removeItem, clear.
 */
function CompressMergeSession(storage) {
    return {
        getItem: function(key) {
            const compressed = storage.getItem(key);
            return compressed ? LZString.decompressFromUTF16(compressed) : null;
        },
        setItem: function(key, value) {
            const compressed = LZString.compressToUTF16(value);
            storage.setItem(key, compressed);
        },
        removeItem: function(key) {
            storage.removeItem(key);
        },
        clear: function() {
            storage.clear();
        }
    };
}

/**
 * Функция для выбора элементов DOM на основе CSS-селектора, с возможностью дополнительной фильтрации.
 * @param {string} selector - CSS-селектор.
 * @param {function} [filterFunc] - Необязательная функция для фильтрации найденных элементов.
 * @returns {function} Функция, которая при вызове возвращает массив элементов.
 */
function baseSelector(selector, filterFunc) {
    return () => {
        const elements = Array.from(document.querySelectorAll(selector));
        return filterFunc ? filterFunc(elements) : elements;
    };
}

/**
 * Функция для извлечения текстового содержимого из элемента DOM.
 * @param {HTMLElement} element - Элемент DOM.
 * @returns {string} Текстовое содержимое элемента.
 */
function baseTextGetter(element) {
    return element ? element.textContent || "" : "";
}

/**
 * Функция для установки переведенного текста обратно в элемент DOM.
 * @param {object} options - Объект с параметрами: element (элемент DOM), text (переведенный текст).
 * @returns {HTMLElement} Модифицированный элемент DOM.
 */
function baseTextSetter(options) {
    if (options.element && options.text !== undefined) {
        options.element.innerHTML = options.text;
    }
    return options.element;
}

/**
 * Функция для фильтрации URL-адресов из текста.
 * Это базовая реализация, вы можете улучшить ее, если нужно сохранять часть URL.
 * @param {string} text - Исходный текст.
 * @returns {string} Текст без URL.
 */
function url_filter(text) {
    return text.replace(/(https?:\/\/[^\s]+)/g, ''); // Удаляет http/https ссылки
}

/**
 * Функция для определения языка текста.
 * Это очень базовая заглушка. Для точного определения языка рассмотрите использование
 * специализированных библиотек или API (например, Google Cloud Translation API's language detection).
 * @param {string} text - Текст для определения языка.
 * @returns {Promise<string>} Промис, возвращающий код языка (например, 'en', 'zh', 'auto').
 */
function pass_lang(text) {
    return new Promise(resolve => {
        // Очень простая эвристика: если есть много китайских символов, считаем, что это китайский.
        const chineseCharCount = (text.match(/[\u4e00-\u9fff]/g) || []).length;
        if (chineseCharCount > text.length * 0.3) { // Если более 30% символов - китайские
            resolve('zh'); // Китайский
        } else {
            resolve('auto'); // Позволить API определить язык
        }
    });
}

/**
 * Вспомогательная функция для удаления элемента из массива.
 * @param {Array} array - Массив, из которого нужно удалить элемент.
 * @param {*} item - Элемент для удаления.
 */
function removeItem(array, item) {
    const index = array.indexOf(item);
    if (index > -1) {
        array.splice(index, 1);
    }
}

/**
 * Функция-обертка для выполнения промиса с возможностью повторных попыток.
 * Эта реализация просто выполняет функцию один раз.
 * Для реальных повторных попыток требуется дополнительная логика (например, setTimeout, счетчик попыток).
 * @param {function} func - Функция, возвращающая промис.
 * @returns {Promise<*>} Результат промиса.
 */
function PromiseRetryWrap(func) {
    // В реальной реализации здесь может быть логика для повторных попыток с задержкой
    return func();
}

// --- Функции перевода (заглушки, требуют реальной реализации API) ---

/**
 * Google Переводчик (Desktop/Web API)
 * Требует реальной реализации запроса к API Google Translate.
 * Обратите внимание, что прямое использование translate.google.com может быть заблокировано CORS
 * или требовать обходных путей. Рекомендуется использовать официальный Google Cloud Translation API,
 * который требует ключей API и оплаты.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык (например, 'auto', 'en', 'ru').
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function translate_gg(text, sourceLang) {
    console.log(`[Google Переводчик] Запрос: "${text}" с "${sourceLang}"`);
    return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: "GET",
            // Это общая конечная точка, которая часто используется. Она может быть нестабильной.
            url: `https://translate.google.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=zh-CN&dt=t&q=${encodeURIComponent(text)}`,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    // Структура ответа может меняться. Это пример для популярного формата.
                    if (data && data[0] && data[0][0] && data[0][0][0]) {
                        resolve(data[0][0][0]);
                    } else {
                        reject(new Error("Некорректный ответ от Google Translate"));
                    }
                } catch (e) {
                    console.error("[Google Переводчик] Ошибка парсинга:", e);
                    reject(e);
                }
            },
            onerror: function(error) {
                console.error("[Google Переводчик] Ошибка запроса:", error);
                reject(error);
            }
        });
    });
}

/**
 * Google Translate Mobile
 * Аналогично translate_gg, но может использовать другие эндпоинты или параметры для мобильных версий.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык.
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function translate_ggm(text, sourceLang) {
    console.log(`[Google Translate Mobile] Запрос: "${text}" с "${sourceLang}"`);
    // Реализация может быть похожа на translate_gg, но с учетом возможных отличий в API для мобильных
    return translate_gg(text, sourceLang); // Используем тот же API для примера
}

/**
 * Перевод Tencent
 * Требует ключей API Tencent Cloud Translation и подписания запросов.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык.
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function translate_tencent(text, sourceLang) {
    console.log(`[Перевод Tencent] Запрос: "${text}" с "${sourceLang}"`);
    // Пример структуры запроса (замените на реальные данные и подпись)
    return new Promise((resolve, reject) => {
        // Здесь потребуется генерация подписи (Signature) и другие параметры
        // согласно документации Tencent Cloud Translation API.
        // Это может включать AccessKeyId, SecretAccessKey, Region, Action и т.д.
        // Пример:
        // const SECRET_ID = 'YOUR_SECRET_ID';
        // const SECRET_KEY = 'YOUR_SECRET_KEY';
        // const TIMESTAMP = Math.floor(Date.now() / 1000);
        // const NONCE = Math.floor(Math.random() * 100000);
        // const PARAMS = {
        //     Action: 'TextTranslate',
        //     Region: 'ap-guangzhou',
        //     SourceText: text,
        //     Source: sourceLang === 'auto' ? 'auto' : sourceLang,
        //     Target: 'zh', // Или 'zh-Hans' для упрощенного китайского
        //     ProjectId: 0,
        //     // ... другие параметры
        // };
        // const SIGNED_PARAMS = signRequest(PARAMS, SECRET_KEY); // Функция signRequest должна быть реализована вами

        // GM_xmlhttpRequest({
        //     method: "POST",
        //     url: "https://tmt.tencentcloudapi.com/", // Или другая конечная точка
        //     headers: {
        //         "Content-Type": "application/json",
        //         "X-TC-Action": "TextTranslate",
        //         "X-TC-Version": "2018-03-21",
        //         "X-TC-Region": "ap-guangzhou",
        //         "X-TC-Timestamp": TIMESTAMP.toString(),
        //         "X-TC-Nonce": NONCE.toString(),
        //         "X-TC-Signature": SIGNED_PARAMS.Signature,
        //     },
        //     data: JSON.stringify(SIGNED_PARAMS),
        //     onload: function(response) { /* ... */ },
        //     onerror: function(error) { /* ... */ }
        // });
        resolve(`[Tencent] Перевод: ${text}`); // Заглушка
    });
}

/**
 * Перевод Tencent AI (может быть частью Tencent Cloud или отдельный сервис)
 * Предполагается аналогичная логика API, как и у Tencent Translation.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык.
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function translate_tencentai(text, sourceLang) {
    console.log(`[Перевод Tencent AI] Запрос: "${text}" с "${sourceLang}"`);
    return translate_tencent(text, sourceLang); // Используем тот же API для примера
}

/**
 * Мобильный перевод Youdao
 * Требует ключей API Youdao Translate и подписания запросов.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык.
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function Translate_youdao_mobile(text, sourceLang) {
    console.log(`[Мобильный перевод Youdao] Запрос: "${text}" с "${sourceLang}"`);
    // Youdao API требует appKey, secretKey, salt, timestamp и sig (подпись).
    // Подпись генерируется с помощью SHA256 и HMAC.
    // Пример:
    // const APP_KEY = 'YOUR_APP_KEY';
    // const SECRET_KEY = 'YOUR_SECRET_KEY';
    // const SALT = Date.now();
    // const CURTIME = Math.floor(Date.now() / 1000);
    // const SIGN_STR = APP_KEY + truncate(text) + SALT + CURTIME + SECRET_KEY;
    // const SIGN = CryptoJS.SHA256(SIGN_STR).toString(CryptoJS.enc.Hex); // Требует CryptoJS
    // GM_xmlhttpRequest({
    //     method: "POST",
    //     url: "https://openapi.youdao.com/api", // Или другой эндпоинт Youdao
    //     headers: { "Content-Type": "application/x-www-form-urlencoded" },
    //     data: new URLSearchParams({
    //         q: text,
    //         from: sourceLang === 'auto' ? 'auto' : sourceLang,
    //         to: 'zh-CHS', // Или другой китайский вариант
    //         appKey: APP_KEY,
    //         salt: SALT,
    //         sign: SIGN,
    //         signType: 'v3',
    //         curtime: CURTIME
    //     }).toString(),
    //     onload: function(response) { /* ... */ },
    //     onerror: function(error) { /* ... */ }
    // });
    return Promise.resolve(`[Youdao Mobile] Перевод: ${text}`); // Заглушка
}

/**
 * Переводчик Baidu
 * Требует AppId и SecretKey для Baidu Fanyi API.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык.
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function Translate_baidu(text, sourceLang) {
    console.log(`[Переводчик Baidu] Запрос: "${text}" с "${sourceLang}"`);
    // Baidu API требует appid, q, from, to, salt и sign.
    // sign генерируется md5(appid + q + salt + secretKey).
    // Пример:
    // const APP_ID = 'YOUR_APP_ID';
    // const SECRET_KEY = 'YOUR_SECRET_KEY';
    // const SALT = Math.random().toString().slice(-10);
    // const SIGN_STR = APP_ID + text + SALT + SECRET_KEY;
    // const SIGN = CryptoJS.MD5(SIGN_STR).toString(); // Требует CryptoJS
    // GM_xmlhttpRequest({
    //     method: "POST",
    //     url: "https://fanyi-api.baidu.com/api/trans/vip/translate",
    //     headers: { "Content-Type": "application/x-www-form-urlencoded" },
    //     data: new URLSearchParams({
    //         q: text,
    //         from: sourceLang === 'auto' ? 'auto' : sourceLang,
    //         to: 'zh',
    //         appid: APP_ID,
    //         salt: SALT,
    //         sign: SIGN
    //     }).toString(),
    //     onload: function(response) { /* ... */ },
    //     onerror: function(error) { /* ... */ }
    // });
    return Promise.resolve(`[Baidu] Перевод: ${text}`); // Заглушка
}

/**
 * Цайюнь Сяойи (Caiyun Xiaoyi)
 * Может быть более простым API или требовать токен.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык.
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function Translation_caiyun(text, sourceLang) {
    console.log(`[Цайюнь Сяойи] Запрос: "${text}" с "${sourceLang}"`);
    // Caiyun Xiaoyi (Rainbow Translate) часто имеет более простой API.
    // Может быть достаточно POST-запроса с текстом и целевым языком.
    // Возможно, требуется токен в заголовках.
    // GM_xmlhttpRequest({
    //     method: "POST",
    //     url: "https://api.interpreter.caiyunai.com/v1/translator", // Проверьте актуальный эндпоинт
    //     headers: {
    //         "Content-Type": "application/json",
    //         "X-Authorization": "token YOUR_CAIYUN_TOKEN" // Если требуется токен
    //     },
    //     data: JSON.stringify({
    //         source: text.split('\n'), // API может ожидать массив строк
    //         request_id: "demo",
    //         detect: true, // Позволить API определить исходный язык
    //         target: "zh", // Целевой язык
    //     }),
    //     onload: function(response) { /* ... */ },
    //     onerror: function(error) { /* ... */ }
    // });
    return Promise.resolve(`[Caiyun Xiaoyi] Перевод: ${text}`); // Заглушка
}

/**
 * Bing Translation
 * Использование Bing API обычно требует Azure Cognitive Services.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык.
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function translate_biying(text, sourceLang) {
    console.log(`[Bing Translation] Запрос: "${text}" с "${sourceLang}"`);
    // Microsoft Azure Translator Text API требует ключ подписки и регион.
    // GM_xmlhttpRequest({
    //     method: "POST",
    //     url: `https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from=${sourceLang}&to=zh-Hans`,
    //     headers: {
    //         "Ocp-Apim-Subscription-Key": "YOUR_AZURE_TRANSLATOR_KEY",
    //         "Ocp-Apim-Subscription-Region": "YOUR_AZURE_TRANSLATOR_REGION", // Например, "eastus"
    //         "Content-Type": "application/json"
    //     },
    //     data: JSON.stringify([{ "text": text }]),
    //     onload: function(response) { /* ... */ },
    //     onerror: function(error) { /* ... */ }
    // });
    return Promise.resolve(`[Bing] Перевод: ${text}`); // Заглушка
}

/**
 * Перевод Папаго (Naver Papago)
 * Требует Client ID и Client Secret от Naver Developer Center.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык.
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function translate_papago(text, sourceLang) {
    console.log(`[Перевод Папаго] Запрос: "${text}" с "${sourceLang}"`);
    // Papago API требует X-Naver-Client-Id и X-Naver-Client-Secret в заголовках.
    // GM_xmlhttpRequest({
    //     method: "POST",
    //     url: "https://papago.naver.com/apis/n2mt/translate", // Или https://openapi.naver.com/v1/papago/n2mt
    //     headers: {
    //         "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    //         "X-Naver-Client-Id": "YOUR_NAVER_CLIENT_ID",
    //         "X-Naver-Client-Secret": "YOUR_NAVER_CLIENT_SECRET"
    //     },
    //     data: new URLSearchParams({
    //         source: sourceLang === 'auto' ? 'auto' : sourceLang,
    //         target: 'zh-CN',
    //         text: text
    //     }).toString(),
    //     onload: function(response) { /* ... */ },
    //     onerror: function(error) { /* ... */ }
    // });
    return Promise.resolve(`[Papago] Перевод: ${text}`); // Заглушка
}

/**
 * Ali Translation (Alibaba Cloud Translate)
 * Требует AccessKeyId и AccessKeySecret для Alibaba Cloud.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык.
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function translate_alibaba(text, sourceLang) {
    console.log(`[Ali Translation] Запрос: "${text}" с "${sourceLang}"`);
    // Alibaba Cloud Translate API также требует подписания запросов.
    // Это сложнее и обычно включает SDK или ручную генерацию подписей HMAC-SHA1.
    // GM_xmlhttpRequest({
    //     method: "POST",
    //     url: "https://mt.cn-hangzhou.aliyuncs.com/", // Или другой регион
    //     headers: {
    //         "Content-Type": "application/json",
    //         // ... Заголовки авторизации Алибабы
    //     },
    //     data: JSON.stringify({
    //         "Format": "json",
    //         "Action": "TranslateText",
    //         "Version": "2018-09-17",
    //         "SourceText": text,
    //         "SourceLanguage": sourceLang === 'auto' ? 'auto' : sourceLang,
    //         "TargetLanguage": "zh",
    //         "Scene": "general"
    //     }),
    //     onload: function(response) { /* ... */ },
    //     onerror: function(error) { /* ... */ }
    // });
    return Promise.resolve(`[Alibaba] Перевод: ${text}`); // Заглушка
}

/**
 * Iciba Translation
 * Часто используется для словарей, но может иметь и API перевода.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык.
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function translate_icib(text, sourceLang) {
    console.log(`[iciba translation] Запрос: "${text}" с "${sourceLang}"`);
    // Iciba API может быть простым GET-запросом или требовать токен.
    // Например: `http://ifanyi.iciba.com/index.php?c=trans&m=fy&client=6&q=${encodeURIComponent(text)}`
    return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: "GET",
            url: `https://ifanyi.iciba.com/index.php?c=trans&m=fy&client=6&q=${encodeURIComponent(text)}`,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data && data.content && data.content.out) {
                        resolve(data.content.out);
                    } else {
                        reject(new Error("Некорректный ответ от Iciba"));
                    }
                } catch (e) {
                    console.error("[iciba translation] Ошибка парсинга:", e);
                    reject(e);
                }
            },
            onerror: function(error) {
                console.error("[iciba translation] Ошибка запроса:", error);
                reject(error);
            }
        });
    });
}

/**
 * Deepl Translation
 * DeepL API требует аутентификационного ключа.
 * @param {string} text - Текст для перевода.
 * @param {string} sourceLang - Исходный язык.
 * @returns {Promise<string>} Промис, возвращающий переведенный текст.
 */
function translate_deepl(text, sourceLang) {
    console.log(`[Deepl Translation] Запрос: "${text}" с "${sourceLang}"`);
    // DeepL API: https://www.deepl.com/docs-api
    // const DEEPL_AUTH_KEY = "YOUR_DEEPL_AUTH_KEY";
    // GM_xmlhttpRequest({
    //     method: "POST",
    //     url: "https://api-free.deepl.com/v2/translate", // Или api.deepl.com/v2/translate для Pro
    //     headers: {
    //         "Content-Type": "application/x-www-form-urlencoded",
    //         "Authorization": `DeepL-Auth-Key ${DEEPL_AUTH_KEY}`
    //     },
    //     data: new URLSearchParams({
    //         text: text,
    //         source_lang: sourceLang === 'auto' ? '' : sourceLang.toUpperCase(), // DeepL использует коды языков в верхнем регистре, 'auto' - пустая строка
    //         target_lang: 'ZH',
    //     }).toString(),
    //     onload: function(response) { /* ... */ },
    //     onerror: function(error) { /* ... */ }
    // });
    return Promise.resolve(`[Deepl] Перевод: ${text}`); // Заглушка
}

// --- Функции запуска переводчиков (заглушки) ---

/**
 * Функция, выполняемая при запуске для Tencent Translation.
 * Может использоваться для инициализации, загрузки зависимостей или проверки состояния API.
 * @returns {Promise<void>} Промис, который разрешается после выполнения стартовых операций.
 */
function translate_tencent_startup() {
    console.log("[Tencent Translation] Выполняется запуск...");
    // Здесь может быть код для предварительной проверки API,
    // загрузки необходимых библиотек или инициализации.
    return Promise.resolve();
}

/**
 * Функция, выполняемая при запуске для Caiyun Xiaoyi.
 * @returns {Promise<void>} Промис, который разрешается после выполнения стартовых операций.
 */
function Translation_caiyun_startup() {
    console.log("[Цайюнь Сяойи] Выполняется запуск...");
    return Promise.resolve();
}

/**
 * Функция, выполняемая при запуске для Papago Translation.
 * @returns {Promise<void>} Промис, который разрешается после выполнения стартовых операций.
 */
function translate_papago_startup() {
    console.log("[Papago Translation] Выполняется запуск...");
    return Promise.resolve();
}

// --- Основной код скрипта (остается без изменений) ---

GM_registerMenuCommand('Сбросить положение панели управления (обновить приложение)', () => {
    GM_setValue('position_top', '9px');
    GM_setValue('position_right', '9px');
    location.reload(); // Добавлено для обновления приложения, как указано в комментарии
});

GM_registerMenuCommand('Глобально скрыть/показать плавающий шар (обновить приложение)', () => {
    GM_setValue('show_translate_ball', !GM_getValue('show_translate_ball', true));
    location.reload(); // Добавлено для обновления приложения
});

const transdict = {
    'Google Переводчик': translate_gg,
    'Google Translate mobile': translate_ggm,
    'Перевод Tencent': translate_tencent,
    'Перевод Tencent AI': translate_tencentai,
    //'Перевод Youdao': Translate_youdao, // Закомментировано в оригинале
    'Мобильный перевод Youdao': Translate_youdao_mobile,
    'Переводчик Baidu': Translate_baidu,
    'Цайюнь Сяойи': Translation_caiyun,
    'Bing Translation': translate_biying,
    'Перевод Папаго': translate_papago,
    'Ali Translation': translate_alibaba,
    'iciba translation': translate_icib,
    'Deepl Translation': translate_deepl,
    'Отключить перевод': () => {}
};

const startup = {
    // 'Перевод Youdao': Translate_youdao_startup, // Закомментировано в оригинале
    'Перевод Tencent': translate_tencent_startup,
    'Цайюнь Сяойи': Translation_caiyun_startup,
    'Papago Translation': translate_papago_startup
};

const baseoptions = {
    'enable_pass_lang': {
        declare: 'Не переводить китайский (упрощенный)',
        default_value: true,
        change_func: self => {
            if (self.checked) sessionStorage.clear();
        }
    },
    'enable_pass_lang_cht': {
        declare: 'Не переводите китайский (традиционный)',
        default_value: true,
        change_func: self => {
            if (self.checked) sessionStorage.clear();
        }
    },
    '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,
    }
};

const [enable_pass_lang, enable_pass_lang_cht, remove_url, show_info, fullscrenn_hidden, replace_translate, compress_storage] =
    Object.keys(baseoptions).map(key => GM_getValue(key, baseoptions[key].default_value));

const globalProcessingSave = [];

const sessionStorage = compress_storage ? CompressMergeSession(window.sessionStorage) : window.sessionStorage;

const p = window.trustedTypes !== undefined ? window.trustedTypes.createPolicy('translator', {
    createHTML: (string, sink) => string
}) : {
    createHTML: (string, sink) => string
};

function initPanel() {
    let choice = GM_getValue('translate_choice', 'Google Переводчик');
    let select = document.createElement("select");
    select.className = 'js_translate';
    select.style = 'height: 35px; width: 100px; background-color: #fff; border-radius: 17.5px; text-align: center; color: #000000; margin: 5px 0;';
    select.onchange = () => {
        GM_setValue('translate_choice', select.value);
        title.innerText = "Панель управления (обновите, чтобы применить)";
    };
    for (let i in transdict) select.innerHTML = p.createHTML(select.innerHTML + '<option value="' + i + '">' + i + '</option>');

    let enable_details = document.createElement('details');
    enable_details.innerHTML = p.createHTML(enable_details.innerHTML + "<summary>Включить правила</summary>");
    for (let i of rules) {
        let temp = document.createElement('input');
        temp.type = 'checkbox';
        temp.name = i.name;
        if (GM_getValue("enable_rule:" + temp.name, true)) temp.setAttribute('checked', true);
        enable_details.appendChild(temp);
        enable_details.innerHTML = p.createHTML(enable_details.innerHTML + "<span>" + i.name + "</span><br>");
    }

    let current_details = document.createElement('details');
    let mask = document.createElement('div'),
        dialog = document.createElement("div"),
        js_dialog = document.createElement("div"),
        title = document.createElement('p');

    let shadowRoot = document.createElement('div');
    shadowRoot.style = "position: absolute; visibility: hidden;";
    window.top.document.body.appendChild(shadowRoot);
    let shadow = shadowRoot.attachShadow({
        mode: "closed"
    });
    shadow.appendChild(mask);

    dialog.appendChild(js_dialog);
    mask.appendChild(dialog);
    js_dialog.appendChild(title);
    js_dialog.appendChild(document.createElement('p')).appendChild(select);
    js_dialog.appendChild(document.createElement('p')).appendChild(enable_details);
    js_dialog.appendChild(document.createElement('p')).appendChild(current_details);

    mask.style = "display: none; position: fixed; height: 100vh; width: 100vw; z-index: 99999; top: 0; left: 0; overflow: hidden; background-color: rgba(0,0,0,0.4); justify-content: center; align-items: center; visibility: visible;";
    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 = document.createElement('input'),
            temp_p = document.createElement('p');
        js_dialog.appendChild(temp_p);
        temp_p.appendChild(temp);
        temp.type = 'checkbox';
        temp.name = i;
        temp_p.style = "display:flex;align-items: center;margin:5px 0";
        temp_p.innerHTML = p.createHTML(temp_p.innerHTML + baseoptions[i].declare);
    }

    for (let i of js_dialog.querySelectorAll('input')) {
        if (i.name && baseoptions[i.name]) {
            i.onclick = _ => {
                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')) {
        i.onclick = _ => {
            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-last: center; color: #000000; display: flex; align-items: center; align-content: center; cursor: pointer; font-size: 15px; user-select: none; visibility: visible;`;
    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 = "flex";
            current_details.innerHTML = p.createHTML(`<summary>В настоящее время включено - ${currentRule.name}</summary>`);
            for (const option of currentRule.options) {
                const fieldset = document.createElement("fieldset");
                fieldset.innerHTML = p.createHTML(fieldset.innerHTML + `<legend>${option.name}</legend>`);
                current_details.appendChild(fieldset);
                fieldset.innerHTML = p.createHTML(fieldset.innerHTML + `<div style="display:flex;align-items:center"><span>отображает теги в исходном тексте</span><input type="checkbox"></div>`);
                for (const key in baseoptions) {
                    if (!baseoptions[key].option_enable) {
                        continue;
                    }
                    fieldset.innerHTML = p.createHTML(fieldset.innerHTML + `<span>${baseoptions[key].declare}</span><br>`);
                    const baseValueList = [
                        ["", "default"],
                        ["true", "enabled"],
                        ["false", "disabled"]
                    ];
                    fieldset.innerHTML = p.createHTML(fieldset.innerHTML + "<div>" + baseValueList.map(value => `<input type="radio" value="${value[0]}" name="${key}:${currentRule.name}-${option.name}">${value[1]}</input>`).join('') + "</div>");
                }
                const enableInput = fieldset.querySelector('input[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);
                };
                const optionInputs = fieldset.querySelectorAll("input[type=radio]");
                for (const input of optionInputs) {
                    const key = `option_setting:${input.name}`;
                    if (GM_getValue(key, '').toString() === input.value) {
                        input.checked = true;
                    }
                    input.onchange = () => {
                        title.innerText = "Панель управления (обновите, чтобы применить)";
                        switch (input.value) {
                            case 'true':
                                GM_setValue(key, true);
                                break;
                            case 'false':
                                GM_setValue(key, false);
                                break;
                            case '':
                                GM_deleteValue(key);
                                break;
                        }
                    };
                }
            }
        }
    };

    open.onclick = () => {
        renderCurrentRule();
        mask.style.display = 'flex';
    };

    open.draggable = true;
    open.addEventListener("dragstart", function(ev) {
        ev.stopImmediatePropagation();
        this.tempNode = document.createElement('div');
        this.tempNode.style = "width: 1px; height: 1px; opacity: 0;";
        document.body.appendChild(this.tempNode);
        ev.dataTransfer.setDragImage(this.tempNode, 0, 0);
        this.oldX = ev.offsetX - Number(this.style.width.replace('px', ''));
        this.oldY = ev.offsetY;
    });
    open.addEventListener("drag", function(ev) {
        ev.stopImmediatePropagation();
        if (!ev.x && !ev.y) return;
        this.style.right = Math.max(window.innerWidth - ev.x + this.oldX, 0) + "px";
        this.style.top = Math.max(ev.y - this.oldY, 0) + "px";
    });
    open.addEventListener("dragend", function(ev) {
        ev.stopImmediatePropagation();
        GM_setValue("position_right", this.style.right);
        GM_setValue("position_top", this.style.top);
        document.body.removeChild(this.tempNode);
    });

    open.addEventListener("touchstart", ev => {
        ev.stopImmediatePropagation();
        ev.preventDefault();
        ev = ev.touches[0];
        open._tempTouch = {};
        const base = open.getBoundingClientRect();
        open._tempTouch.oldX = ev.clientX - base.left;
        open._tempTouch.oldY = ev.clientY - base.top;
        open._tempIsMove = false;
    });

    open.addEventListener("touchmove", ev => {
        ev.stopImmediatePropagation();
        ev = ev.touches[0];
        open.style.right = Math.max(window.innerWidth - (ev.clientX + open._tempTouch.oldX), 0) + 'px';
        open.style.top = Math.max(ev.clientY - open._tempTouch.oldY, 0) + 'px';
        open._tempIsMove = true;
    });

    open.addEventListener("touchend", ev => {
        ev.stopImmediatePropagation();
        GM_setValue("position_right", open.style.right);
        GM_setValue("position_top", open.style.top);
        if (!open._tempIsMove) {
            renderCurrentRule();
            mask.style.display = 'flex';
        }
        open._tempIsMove = false;
    });

    shadow.appendChild(open);
    shadow.querySelector('.js_translate option[value="' + choice + '"]').selected = true;

    if (fullscrenn_hidden) window.top.document.addEventListener('fullscreenchange', () => {
        open.style.display = window.top.document.fullscreenElement ? "none" : "flex";
    });
}

const rules = [{
        name: 'Twitter General',
        matcher: /https:\/\/(?:[a-zA-Z.]*?\.|)twitter\.com/,
        options: [{
                name: "Твит",
                selector: baseSelector('div[dir="auto"][lang]'),
                textGetter: baseTextGetter,
                textSetter: options => {
                    options.element.style.cssText = options.element.style.cssText.replace(/-webkit-line-clamp.*?;/, '');
                    baseTextSetter(options).style.display = 'flex';
                }
            },
            {
                name: "Справочная информация",
                selector: baseSelector('div[data-testid=birdwatch-pivot]>div[dir=ltr]'),
                textGetter: baseTextGetter,
                textSetter: options => {
                    options.element.style.cssText = options.element.style.cssText.replace(/-webkit-line-clamp.*?;/, '');
                    baseTextSetter(options).style.display = 'flex';
                }
            }
        ]
    },
    {
        name: 'x General',
        matcher: /https:\/\/(?:[a-zA-Z.]*?\.|)x\.com/,
        options: [{
                name: "Твит",
                selector: baseSelector('div[dir="auto"][lang]'),
                textGetter: baseTextGetter,
                textSetter: options => {
                    options.element.style.cssText = options.element.style.cssText.replace(/-webkit-line-clamp.*?;/, '');
                    baseTextSetter(options).style.display = 'flex';
                }
            },
            {
                name: "Справочная информация",
                selector: baseSelector('div[data-testid=birdwatch-pivot]>div[dir=ltr]'),
                textGetter: baseTextGetter,
                textSetter: options => {
                    options.element.style.cssText = options.element.style.cssText.replace(/-webkit-line-clamp.*?;/, '');
                    baseTextSetter(options).style.display = 'flex';
                }
            }
        ]
    },
    {
        name: 'youtube pc universal',
        matcher: /https:\/\/www\.youtube\.com\/(?:watch|shorts|results\?)/,
        options: [{
                name: "Область комментариев",
                selector: baseSelector("#content>#content-text"),
                textGetter: baseTextGetter,
                textSetter: options => {
                    baseTextSetter(options);
                    options.element.parentNode.parentNode.removeAttribute('collapsed');
                }
            },
            {
                name: "Видеовведение",
                selector: baseSelector("#content>#description>.content,.ytd-text-inline-expander>.yt-core-attributed-string"),
                textGetter: baseTextGetter,
                textSetter: options => {
                    baseTextSetter(options);
                    options.element.parentNode.parentNode.removeAttribute('collapsed');
                }
            },
            {
                name: "CC Subtitles",
                selector: baseSelector(".ytp-caption-segment"),
                textGetter: baseTextGetter,
                textSetter: baseTextSetter
            }
        ]
    },
    {
        name: 'youtube mobile universal',
        matcher: /https:\/\/m\.youtube\.com\/watch/,
        options: [{
                name: "Область комментариев",
                selector: baseSelector(".comment-text.user-text"),
                textGetter: baseTextGetter,
                textSetter: baseTextSetter,
            },
            {
                name: "Видеовведение",
                selector: baseSelector(".slim-video-metadata-description"),
                textGetter: baseTextGetter,
                textSetter: baseTextSetter,
            }
        ]
    },
    {
        name: 'короткое видео YouTube',
        matcher: /https:\/\/(?:www|m)\.youtube\.com\/shorts/,
        options: [{
            name: "Область комментариев",
            selector: baseSelector("#comment-content #content-text,.comment-content .comment-text"),
            textGetter: baseTextGetter,
            textSetter: baseTextSetter,
        }]
    },
    {
        name: 'сообщество YouTube',
        matcher: /https:\/\/(?:www|m)\.youtube\.com\/(?:.*?\/community|post)/,
        options: [{
            name: "Область комментариев",
            selector: baseSelector("#post #content #content-text,#comment #content #content-text,#replies #content #content-text"),
            textGetter: baseTextGetter,
            textSetter: options => {
                baseTextSetter(options);
                options.element.parentNode.parentNode.removeAttribute('collapsed');
            }
        }]
    },
    {
        name: 'facebook-universal',
        matcher: /https:\/\/www\.facebook\.com\/.+/,
        options: [{
                name: "Опубликовать контент",
                selector: baseSelector("div[data-ad-comet-preview=message],div[role=article] div[id]"),
                textGetter: baseTextGetter,
                textSetter: options => setTimeout(baseTextSetter, 0, options),
            },
            {
                name: "Область комментариев",
                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: 'Заголовок сообщения',
                selector: baseSelector("*[slot=title][id|=post-title]"),
                textGetter: baseTextGetter,
                textSetter: baseTextSetter,
            },
            {
                name: "Опубликовать контент",
                selector: baseSelector("div[slot=text-body]>div>div[id*=-post-rtjson-content]"),
                textGetter: baseTextGetter,
                textSetter: baseTextSetter,
            },
            {
                name: "Область комментариев",
                selector: baseSelector("div[slot=comment]>div[id$=-post-rtjson-content]"),
                textGetter: baseTextGetter,
                textSetter: baseTextSetter,
            }
        ]
    },
    {
        name: '5ch Comments',
        matcher: /https?:\/\/(?:.*?\.|)5ch\.net\/.*/,
        options: [{
                name: "Название",
                selector: baseSelector('.post>.post-content,#threadtitle,.thread_title'),
                textGetter: baseTextGetter,
                textSetter: baseTextSetter,
            },
            {
                name: "контент",
                selector: baseSelector('.threadview_response_body'),
                textGetter: baseTextGetter,
                textSetter: baseTextSetter,
            }
        ]
    },
    {
        name: 'чат Discord',
        matcher: /https:\/\/discord\.com\/.+/,
        options: [{
            name: "Содержимое чата",
            selector: baseSelector('div[class*=messageContent]'),
            textGetter: baseTextGetter,
            textSetter: baseTextSetter,
        }]
    },
    {
        name: 'телеграм чат новый',
        matcher: /https:\/\/.*\.telegram\.org\/(?:a|z)\//,
        options: [{
            name: "Содержимое чата",
            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: 'чат телеграмм',
        matcher: /https:\/\/.*\.telegram\.org\/.+/,
        options: [{
            name: "Содержимое чата",
            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: "Название",
                selector: baseSelector(".puppeteer_test_question_title>span>span"),
                textGetter: baseTextGetter,
                textSetter: options => {
                    options.element.parentNode.parentNode.style.cssText = options.element.parentNode.parentNode.style.cssText.replace(/-webkit-line-clamp.*?;/, '');
                    baseTextSetter(options).style.display = 'flex';
                },
            },
            {
                name: "Опубликовать контент",
                selector: baseSelector('div.q-text>span>span.q-box:has(pq-text),div.q-box>div.q-box>div.q-text>span.q-box:has(pq-text)'),
                textGetter: baseTextGetter,
                textSetter: baseTextSetter,
            }
        ]
    },
    {
        name: 'комментарии тикток',
        matcher: /https:\/\/www\.tiktok\.com/,
        options: [{
            name: "Область комментариев",
            selector: baseSelector('p[data-e2e|=comment-level]'),
            textGetter: baseTextGetter,
            textSetter: baseTextSetter,
        }]
    },
    {
        name: "Комментарии Instagram",
        matcher: /https:\/\/www\.instagram\.com/,
        options: [{
            name: "Область комментариев",
            selector: baseSelector('li>div>div>div>div>span[dir=auto]'),
            textGetter: baseTextGetter,
            textSetter: baseTextSetter,
        }]
    },
    {
        name: 'threads',
        matcher: /https:\/\/www\.threads\.net/,
        options: [{
            name: "Пост",
            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: "проблемы",
                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: "обсуждения",
                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: "Домашняя запись",
                selector: baseSelector('div[dir=auto][data-testid=postText]'),
                textGetter: baseTextGetter,
                textSetter: baseTextSetter,
            },
            {
                name: "Контент-посты и ответы",
                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));

(function() {
    'use strict';
    let url = document.location.href;
    let rule = GetActiveRule();
    setInterval(() => {
        if (document.location.href !== url) {
            url = document.location.href;
            const ruleNew = GetActiveRule();
            if (ruleNew !== rule) {
                if (ruleNew !== null) {
                    console.log(`[Переводческая машина] обнаружила изменение URL и перешла на использование правила [${ruleNew.name}]`);
                } else {
                    console.log("[Переводческая машина] обнаружила изменение URL-адреса, в настоящее время нет соответствующих правил");
                }
                rule = ruleNew;
            }
        }
    }, 200);
    console.log(rule ? `[Машина перевода] использует правило [${rule.name}]` : "[Машина перевода] в настоящее время не имеет соответствующего правила");
    console.log(document.location.href);

    let main = _ => {
        if (!rule) return;
        const choice = GM_getValue('translate_choice', 'Google Переводчик');
        for (const option of rule.options) {
            if (!GM_getValue("enable_option:" + rule.name + "-" + option.name, true)) {
                continue;
            }
            const temp = [...new Set(option.selector())];
            for (let i = 0; i < temp.length; i++) {
                const now = temp[i];
                if (globalProcessingSave.includes(now)) continue;
                globalProcessingSave.push(now);
                const rawText = option.textGetter(now);
                const text = remove_url ? url_filter(rawText) : rawText;
                if (text.length === 0) {
                    removeItem(globalProcessingSave, now);
                    continue;
                }
                const setterParams = {
                    element: now,
                    translateName: choice,
                    rawText: rawText,
                    rule: rule,
                    option: option
                };
                if (sessionStorage.getItem(choice + '-' + text)) {
                    setterParams.text = sessionStorage.getItem(choice + '-' + text);
                    option.textSetter(setterParams);
                    removeItem(globalProcessingSave, now);
                } else {
                    pass_lang(text).then(lang => transdict[choice](text, lang)).then(s => {
                        setterParams.text = s;
                        option.textSetter(setterParams);
                        if (s) sessionStorage.setItem(choice + '-' + text, s);
                        removeItem(globalProcessingSave, now);
                    }).catch(error => {
                        console.error("Ошибка перевода:", error);
                        removeItem(globalProcessingSave, now);
                    });
                }
            }
        }
    };
    PromiseRetryWrap(startup[GM_getValue('translate_choice', 'Google Переводчик')]).then(() => {
        document.js_translater = setInterval(main, 20);
        initPanel();
    }).catch(error => {
        console.error("Ошибка при запуске: ", error);
        initPanel();
    });
})();