// ==UserScript== // @name Auto Translate - TWP Engine Full Port // @namespace auto-translate-twp // @version 2.0.0 // @description 完整移植TWP翻译引擎,支持Google(v2+旧版双通道)、Microsoft、Tencent,249种语言 // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @connect translate-pa.googleapis.com // @connect translate.googleapis.com // @connect api-edge.cognitive.microsofttranslator.com // @connect edge.microsoft.com // @connect transmart.qq.com // @run-at document-idle // @downloadURL none // ==/UserScript== (async () => { 'use strict'; try { if (document.contentType === 'application/xml') return } catch (_) {} // ═══════════════════════════════════════════════════════════════ // TWP完整移植:GoogleHelper_v2 + GoogleHelper(旧版TKK) + 全部249种语言 // ═══════════════════════════════════════════════════════════════ const deviceLang = (navigator.language || navigator.userLanguage || 'zh-CN').split('-')[0]; // ── TWP完整语言列表(249种语言,从TWP源码options.html提取)── const ALL_LANGUAGES = { // ===== 常用语言 ===== "zh-CN": "中文(简体)", "zh-TW": "中文(繁體)", "en": "English", "ja": "日本語", "ko": "한국어", "fr": "Français", "de": "Deutsch", "es": "Español", "ru": "Русский", "pt": "Português", "pt-PT": "Português (Portugal)", "ar": "العربية", "th": "ไทย", "vi": "Tiếng Việt", "it": "Italiano", "tr": "Türkçe", "id": "Indonesia", "ms": "Bahasa Melayu", "nl": "Nederlands", "pl": "Polski", "uk": "Українська", "cs": "Čeština", "sk": "Slovenčina", "hu": "Magyar", "ro": "Română", "bg": "Български", "hr": "Hrvatski", "sr": "Српски", "sl": "Slovenščina", "lt": "Lietuvių", "lv": "Latviešu", "et": "Eesti", "fi": "Suomi", "sv": "Svenska", "da": "Dansk", "no": "Norsk", "is": "Íslenska", "el": "Ελληνικά", "he": "עברית", "hi": "हिन्दी", "bn": "বাংলা", "ta": "தமிழ்", "te": "తెలుగు", "kn": "ಕನ್ನಡ", "ml": "മലയാളം", "pa": "ਪੰਜਾਬੀ", "gu": "ગુજરાતી", "mr": "मराठी", "ne": "नेपाली", "si": "සිංහල", "ur": "اردو", "fa": "فارسی", "ps": "پښتو", "my": "မြန်မာ", "km": "ខ្មែរ", "lo": "ລາວ", "ka": "ქართული", "hy": "Հայերեն", "az": "Azərbaycan", "kk": "Қазақ", "uz": "Oʻzbek", "mn": "Монгол", "sq": "Shqip", "mk": "Македонски", "be": "Беларуская", "bs": "Bosanski", "ca": "Català", "gl": "Galego", "eu": "Euskara", "mt": "Malti", "cy": "Cymraeg", "ga": "Gaeilge", "gd": "Gàidhlig", "lb": "Lëtzebuergesch", "af": "Afrikaans", "sw": "Kiswahili", "ha": "Hausa", "ig": "Igbo", "yo": "Yorùbá", "zu": "isiZulu", "xh": "isiXhosa", "sn": "chiShona", "st": "Sesotho", "so": "Soomaali", "am": "አማርኛ", "ti": "ትግርኛ", "om": "Oromoo", "mg": "Malagasy", "ny": "Chichewa", "lg": "Luganda", "rw": "Kinyarwanda", "tg": "Тоҷикӣ", "tk": "Türkmen", "ky": "Кыргызча", "tt": "Татар", "eo": "Esperanto", "la": "Latina", "co": "Corsu", "fy": "Frysk", "haw": "ʻŌlelo Hawaiʻi", "sm": "Gagana Samoa", "mi": "Te Reo Māori", "ceb": "Cebuano", "fil": "Filipino", "jv": "Basa Jawa", "su": "Basa Sunda", "hmn": "Hmong", "ht": "Kreyòl Ayisyen", "ku": "Kurdî", "ckb": "کوردی", "sd": "سنڌي", "or": "ଓଡ଼ିଆ", "as": "অসমীয়া", "sa": "संस्कृतम्", "mai": "मैथिली", "bho": "भोजपुरी", "doi": "डोगरी", "ug": "ئۇيغۇرچە", "dv": "ދިވެހި", "ak": "Akan", "ee": "Eʋegbe", "gn": "Guarani", "ay": "Aymar", "bm": "Bamanankan", "ln": "Lingála", "nso": "Sepedi", "ts": "Xitsonga", "qu": "Runasimi", "ilo": "Ilokano", "kri": "Krio", "lus": "Mizo tawng", "mni-Mtei": "ꯃꯤꯇꯩꯂꯣꯟ", "gom": "कोंकणी", // ===== TWP v10.x 新增的114种语言(从Release日志提取)===== "ab": "Аԥсуа (Abkhaz)", "ace": "Bahsa Acèh (Acehnese)", "ach": "Lwo (Acholi)", "aa": "Qafaraf (Afar)", "alz": "Alur", "av": "Авар (Avar)", "awa": "अवधी (Awadhi)", "ban": "ᬩᬮᬶ (Balinese)", "bal": "بلوچی (Baluchi)", "bci": "Baoulé", "ba": "Башҡорт (Bashkir)", "btx": "Batak Karo", "bts": "Batak Simalungun", "bbc": "Batak Toba", "bem": "Bemba", "bew": "Betawi", "bik": "Bikol", "br": "Brezhoneg (Breton)", "bua": "Буряад (Buryat)", "yue": "粵語 (Cantonese)", "ch": "Chamoru (Chamorro)", "ce": "Нохчийн (Chechen)", "chk": "Chuukese", "cv": "Чӑваш (Chuvash)", "crh": "Qırımtatar (Crimean Tatar)", "prs": "دری (Dari)", "din": "Thuɔŋjäŋ (Dinka)", "dov": "Dombe", "dyu": "Julakan (Dyula)", "dz": "རྫོང་ཁ (Dzongkha)", "fo": "Føroyskt (Faroese)", "fj": "Na Vosa Vakaviti (Fijian)", "fon": "Fɔ̀ngbè (Fon)", "fr-CA": "Français (Canada)", "fur": "Furlan (Friulian)", "ff": "Pulaar (Fulani)", "gaa": "Gã (Ga)", "cnh": "Lai (Hakha Chin)", "hil": "Hiligaynon", "hrx": "Hunsrik", "iba": "Iban", "iu-Latn": "ᐃᓄᒃᑎᑐᑦ (Inuktut Latin)", "jam": "Jamaican Patois", "kac": "Jingpo (景颇语)", "kl": "Kalaallisut", "kr": "Kanuri", "pam": "Kapampangan", "kha": "Khasi", "cgg": "Rukiga (Kiga)", "kg": "Kikongo", "mkw": "Kituba", "trp": "Kokborok", "kv": "Коми (Komi)", "ltg": "Latgaļu (Latgalian)", "lij": "Lìgure (Ligurian)", "li": "Limburgs (Limburgish)", "lmo": "Lombard", "luo": "Dholuo (Luo)", "mad": "Madhurâ (Madurese)", "mak": "Makassar", "ms-Arab": "بهاس ملايو (Malay Jawi)", "mam": "Mam", "gv": "Gaelg (Manx)", "mh": "Kajin Majōl (Marshallese)", "mwr": "मारवाड़ी (Marwadi)", "mfe": "Kreol Morisien (Mauritian Creole)", "chm": "Марий (Meadow Mari)", "min": "Minangkabau", "nhe": "Nahuatl", "ndc-ZW": "Ndau", "nr": "isiNdebele (South Ndebele)", "new": "नेपाल भाषा (Newari)", "nqo": "ߒߞߏ (NKo)", "nus": "Thok Nath (Nuer)", "oc": "Occitan", "os": "Ирон (Ossetian)", "pag": "Pangasinan", "pap": "Papiamento", "pa-Arab": "پنجابی (Punjabi Shahmukhi)", "kek": "Qʼeqchiʼ", "rom": "Romani", "rn": "Ikirundi (Rundi)", "se": "Davvisámegiella (North Sami)", "sg": "Sängö (Sango)", "bo": "བོད་ཡིག (Tibetan)", "dsb": "Dolnoserbšćina (Lower Sorbian)", "hsb": "Hornjoserbšćina (Upper Sorbian)", "ikt": "Inuinnaqtun", "iu": "ᐃᓄᒃᑎᑐᑦ (Inuktitut)", "lzh": "文言文 (Classical Chinese)", "mvf": "ᠮᠣᠩᠭᠣᠯ (Mongolian Traditional)", "brx": "बर' (Bodo)", "hne": "छत्तीसगढ़ी (Chhattisgarhi)", "ks": "कॉशुर (Kashmiri)", "mrj": "Мары (Hill Mari)", "sa-Latn": "Sanskrit (Latin)", "sc": "Sardu (Sardinian)", "scn": "Sicilianu (Sicilian)", "szl": "Ślůnski (Silesian)", "su-Latn": "Sunda (Latin)", "tcy": "ತುಳು (Tulu)", "vec": "Vèneto (Venetian)", "war": "Winaray (Waray)", "wo": "Wolof", "zap": "Zapotec", "ms-Latn": "Malay (Latin)" }; // ── 语言代码到显示名称的简短映射(用于UI下拉菜单分组)── const LANG_GROUPS = { "常用": ["zh-CN","zh-TW","en","ja","ko","fr","de","es","ru","pt","ar","th","vi","it","tr","id"], "欧洲": ["nl","pl","uk","cs","sk","hu","ro","bg","hr","sr","sl","lt","lv","et","fi","sv","da","no","is","el","be","bs","ca","gl","eu","mt","cy","ga","gd","lb","af","eo","la","co","fy","fo","br","oc","sc","scn","szl","fur","lij","lmo","li","vec","ltg","dsb","hsb","gv","se"], "亚洲": ["hi","bn","ta","te","kn","ml","pa","gu","mr","ne","si","ur","fa","ps","my","km","lo","ka","hy","az","kk","uz","mn","tg","tk","ky","tt","ug","dv","or","as","sa","mai","bho","doi","mni-Mtei","gom","awa","ks","brx","hne","mwr","trp","kac","bo","dz","yue","lzh","ms","fil","ceb","jv","su","hmn","ilo","hil","bik","pam","pag","war","ban","mad","mak","min","ace","btx","bts","bbc","bew","iba","ms-Arab","kha"], "非洲": ["sw","ha","ig","yo","zu","xh","sn","st","so","am","ti","om","mg","ny","lg","rw","ak","ee","bm","ln","nso","ts","kri","wo","ff","gaa","fon","bci","dyu","bem","luo","sg","kg","mkw","dov","nus","din","ach","alz","ndc-ZW","nr","rn","mfe"], "美洲/大洋洲": ["pt-PT","fr-CA","ht","qu","gn","ay","haw","sm","mi","fj","mh","ch","chk","jam","nhe","mam","kek","pap","hrx","ikt","iu","iu-Latn","kl"], "其他": ["ab","av","ba","bua","ce","cv","crh","kv","chm","mrj","os","rom","nqo","aa","bal","cnh","kr","prs","pa-Arab","sd","ckb","ku","he","yi"] }; // ══════════════════════════════════════════════ // TWP核心:GoogleHelper_v2 - API密钥认证管理 // 完整从TWP translationService.js移植 // ══════════════════════════════════════════════ const GoogleHelper_v2 = { _lastRequestAuthTime: null, _translateAuth: null, _authNotFound: false, _authPromise: null, get translateAuth() { return this._translateAuth; }, // TWP源码中的备用密钥(字节数组解码) _getAlternativeKey() { return new TextDecoder().decode( new Uint8Array([ 65, 73, 122, 97, 83, 121, 65, 84, 66, 88, 97, 106, 118, 122, 81, 76, 84, 68, 72, 69, 81, 98, 99, 112, 113, 48, 73, 104, 101, 48, 118, 87, 68, 72, 109, 79, 53, 50, 48, ]) ); }, async findAuth() { if (this._authPromise) return await this._authPromise; this._authPromise = new Promise((resolve) => { let updateGoogleAuth = false; if (this._lastRequestAuthTime) { const date = new Date(); if (this._translateAuth) { date.setMinutes(date.getMinutes() - 20); // 有效密钥20分钟刷新 } else if (this._authNotFound) { date.setMinutes(date.getMinutes() - 5); // 未找到5分钟重试 } else { date.setMinutes(date.getMinutes() - 1); // 其他1分钟重试 } if (date.getTime() > this._lastRequestAuthTime) { updateGoogleAuth = true; } } else { updateGoogleAuth = true; } if (updateGoogleAuth) { this._lastRequestAuthTime = Date.now(); const alternativeKey = this._getAlternativeKey(); // TWP从Google的JS文件中动态提取x-goog-api-key GM_xmlhttpRequest({ method: 'GET', url: 'https://translate.googleapis.com/_/translate_http/_/js/k=translate_http.tr.en_US.YusFYy3P_ro.O/am=AAg/d=1/exm=el_conf/ed=1/rs=AN8SPfq1Hb8iJRleQqQc8zhdzXmF9E56eQ/m=el_main', onload: (r) => { if (r.responseText && r.responseText.length > 1) { const result = r.responseText.match( /['"]x-goog-api-key['"]\s*:\s*['"](\w{39})['"]/i ); if (result && result.length === 2) { this._translateAuth = result[1]; this._authNotFound = false; } else { this._authNotFound = true; this._translateAuth = alternativeKey; } } else { this._authNotFound = true; this._translateAuth = alternativeKey; } resolve(); }, onerror: () => { this._translateAuth = alternativeKey; resolve(); }, ontimeout: () => { this._translateAuth = alternativeKey; resolve(); } }); } else { resolve(); } }); const p = this._authPromise; p.finally(() => { this._authPromise = null; }); return await p; } }; // ══════════════════════════════════════════════ // TWP核心:GoogleHelper旧版 - TKK哈希令牌计算 // 完整从TWP translationService.js移植 // ══════════════════════════════════════════════ const GoogleHelper = { googleTranslateTKK: "448487.932609646", shiftLeftOrRightThenSumOrXor(num, optString) { for (let i = 0; i < optString.length - 2; i += 3) { let acc = optString.charAt(i + 2); if ("a" <= acc) { acc = acc.charCodeAt(0) - 87; } else { acc = Number(acc); } if (optString.charAt(i + 1) === "+") { acc = num >>> acc; } else { acc = num << acc; } if (optString.charAt(i) === "+") { num = (num + acc) & 4294967295; } else { num = num ^ acc; } } return num; }, transformQuery(query) { const bytesArray = []; let idx = 0; for (let i = 0; i < query.length; i++) { let charCode = query.charCodeAt(i); if (128 > charCode) { bytesArray[idx++] = charCode; } else { if (2048 > charCode) { bytesArray[idx++] = (charCode >> 6) | 192; } else { if ( 55296 === (charCode & 64512) && i + 1 < query.length && 56320 === (query.charCodeAt(i + 1) & 64512) ) { charCode = 65536 + ((charCode & 1023) << 10) + (query.charCodeAt(++i) & 1023); bytesArray[idx++] = (charCode >> 18) | 240; bytesArray[idx++] = ((charCode >> 12) & 63) | 128; } else { bytesArray[idx++] = (charCode >> 12) | 224; } bytesArray[idx++] = ((charCode >> 6) & 63) | 128; } bytesArray[idx++] = (charCode & 63) | 128; } } return bytesArray; }, calcHash(query) { const windowTkk = this.googleTranslateTKK; const tkkSplited = windowTkk.split("."); const tkkIndex = Number(tkkSplited[0]) || 0; const tkkKey = Number(tkkSplited[1]) || 0; const bytesArray = this.transformQuery(query); let encondingRound = tkkIndex; for (const item of bytesArray) { encondingRound += item; encondingRound = this.shiftLeftOrRightThenSumOrXor( encondingRound, "+-a^+6" ); } encondingRound = this.shiftLeftOrRightThenSumOrXor( encondingRound, "+-3^+b+-f" ); encondingRound ^= tkkKey; if (encondingRound <= 0) { encondingRound = (encondingRound & 2147483647) + 2147483648; } const normalizedResult = encondingRound % 1000000; return normalizedResult.toString() + "." + (normalizedResult ^ tkkIndex); } }; // ══════════════════════════════════════════════ // TWP核心:HTML转义/反转义工具 // 从TWP translationService.js移植 // ══════════════════════════════════════════════ const Utils = { escapeHTML(text) { const div = document.createElement('div'); div.appendChild(document.createTextNode(text)); return div.innerHTML; }, unescapeHTML(text) { const doc = new DOMParser().parseFromString(text, 'text/html'); return doc.documentElement.textContent; } }; // ── 网络层 ── function gmFetch(opts) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ timeout: 30000, ...opts, onload: resolve, onerror: reject, ontimeout: reject, }); }); } // ══════════════════════════════════════════════ // 引擎定义 - 完整移植TWP的三种Google通道 + MS + Tencent // ══════════════════════════════════════════════ const Engine = { // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 引擎1:Google v2(TWP新版API - translateHtml端点) // 完整移植自TWP googleService // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ google_v2: { name: 'Google (TWP v2)', // TWP中的语言代码替换规则 _fixLang(lang) { const replacements = [ { search: "prs", replace: "fa-AF" }, ]; for (const r of replacements) { if (lang === r.search) return r.replace; } return lang; }, // TWP的cbTransformRequest:将文本数组包装为带索引的HTML _transformRequest(sourceArray) { sourceArray = sourceArray.map(text => Utils.escapeHTML(text)); if (sourceArray.length > 1) { sourceArray = sourceArray.map( (text, index) => `${text}` ); } return `
${sourceArray.join("")}
`; }, // TWP的cbTransformResponse:解析Google返回的HTML格式翻译结果 _transformResponse(result, dontSortResults) { // 移除
标签
        if (result.indexOf("
") !== -1) {
          result = result.replace("
", "");
          const index = result.indexOf(">");
          result = result.slice(index + 1);
        }

        const sentences = [];
        let idx = 0;
        while (true) {
          const sentenceStartIndex = result.indexOf("", idx);
          if (sentenceStartIndex === -1) break;
          const sentenceFinalIndex = result.indexOf("", sentenceStartIndex);
          if (sentenceFinalIndex === -1) {
            sentences.push(result.slice(sentenceStartIndex + 3));
            break;
          } else {
            sentences.push(result.slice(sentenceStartIndex + 3, sentenceFinalIndex));
          }
          idx = sentenceFinalIndex;
        }

        result = sentences.length > 0 ? sentences.join(" ") : result;
        result = result.replace(/<\/b>/g, "");

        // 提取带索引的标签
        let resultArray = [];
        let lastEndPos = 0;
        for (const r of result.matchAll(/()([^<>]*(?=<\/a>))*/g)) {
          const fullLength = r[0].length;
          const pos = r.index;
          if (pos > lastEndPos) {
            const aTag = r[1];
            const insideText = r[2] || "";
            const outsideText = result.slice(lastEndPos, pos).replace(/<\/a>/g, "");
            resultArray.push(aTag + outsideText + insideText);
          } else {
            resultArray.push(r[0]);
          }
          lastEndPos = pos + fullLength;
        }

        let indexes;
        if (resultArray && resultArray.length > 0) {
          indexes = resultArray
            .map(value => parseInt(value.match(/[0-9]+(?=>)/g)?.[0]))
            .filter(value => !isNaN(value));
          resultArray = resultArray.map(value => {
            const resultStartAtIndex = value.indexOf(">");
            return value.slice(resultStartAtIndex + 1);
          });
        } else {
          resultArray = [result];
          indexes = [0];
        }

        resultArray = resultArray.map(value => Utils.unescapeHTML(value));

        if (dontSortResults) {
          return resultArray;
        } else {
          const finalResultArray = [];
          for (const j in indexes) {
            if (finalResultArray[indexes[j]]) {
              finalResultArray[indexes[j]] += " " + resultArray[j];
            } else {
              finalResultArray[indexes[j]] = resultArray[j];
            }
          }
          return finalResultArray;
        }
      },

      async translate(text, toLang) {
        const to = this._fixLang(toLang);
        await GoogleHelper_v2.findAuth();
        if (!GoogleHelper_v2.translateAuth) throw new Error('Google auth not available');

        const requestBody = JSON.stringify([
          [[text], "auto", to],
          "te"
        ]);

        const r = await gmFetch({
          method: 'POST',
          url: 'https://translate-pa.googleapis.com/v1/translateHtml',
          headers: {
            'Content-Type': 'application/json+protobuf',
            'X-Goog-Api-Key': GoogleHelper_v2.translateAuth,
          },
          data: requestBody,
        });

        if (r.status !== 200) throw new Error(`Google v2 API error: ${r.status}`);
        const data = JSON.parse(r.responseText);

        // 解析响应 - data[0] 是翻译结果数组
        if (data && data[0]) {
          const rawResult = Array.isArray(data[0]) ? data[0][0] : data[0];
          // 通过TWP的响应解析器处理
          const parsed = this._transformResponse(rawResult, false);
          return parsed[0] || rawResult;
        }
        throw new Error('Google v2: empty response');
      },

      // TWP风格批量翻译(将多个文本打包为一个请求)
      async translateBatch(texts, toLang) {
        const to = this._fixLang(toLang);
        await GoogleHelper_v2.findAuth();
        if (!GoogleHelper_v2.translateAuth) throw new Error('Google auth not available');

        const requestBody = JSON.stringify([
          [texts, "auto", to],
          "te"
        ]);

        const r = await gmFetch({
          method: 'POST',
          url: 'https://translate-pa.googleapis.com/v1/translateHtml',
          headers: {
            'Content-Type': 'application/json+protobuf',
            'X-Goog-Api-Key': GoogleHelper_v2.translateAuth,
          },
          data: requestBody,
        });

        if (r.status !== 200) throw new Error(`Google v2 batch error: ${r.status}`);
        const data = JSON.parse(r.responseText);

        if (data && data[0] && Array.isArray(data[0])) {
          return data[0].map(item => {
            const parsed = this._transformResponse(item, false);
            return parsed[0] || item;
          });
        }
        // fallback: 单条结果
        if (data && data[0]) {
          const parsed = this._transformResponse(
            Array.isArray(data[0]) ? data[0][0] : data[0], false
          );
          return [parsed[0]];
        }
        throw new Error('Google v2 batch: empty response');
      }
    },

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    //  引擎2:Google旧版(gtx端点 + TKK哈希)
    //  从TWP旧版代码移植,作为v2的降级备份
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    google_legacy: {
      name: 'Google (Legacy)',

      langCode(lang) {
        const map = {
          'zh': 'zh-CN', 'zh-CN': 'zh-CN', 'zh-TW': 'zh-TW',
          'en': 'en', 'ja': 'ja', 'ko': 'ko', 'fr': 'fr',
          'de': 'de', 'es': 'es', 'ru': 'ru', 'pt': 'pt',
        };
        return map[lang] || lang;
      },

      async translate(text, toLang) {
        const to = this.langCode(toLang);
        const tk = GoogleHelper.calcHash(text);
        const r = await gmFetch({
          method: 'GET',
          url: `https://translate.googleapis.com/translate_a/single?client=webapp&sl=auto&tl=${to}&hl=${to}&dt=t&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=at&ie=UTF-8&oe=UTF-8&otf=1&ssel=0&tsel=0&kc=7&tk=${tk}&q=${encodeURIComponent(text)}`,
        });
        if (r.status !== 200) {
          // 降级到gtx(无需TKK)
          return await this._translateGtx(text, to);
        }
        const data = JSON.parse(r.responseText);
        return data[0].filter(s => s && s[0]).map(s => s[0]).join('');
      },

      async _translateGtx(text, to) {
        const r = await gmFetch({
          method: 'GET',
          url: `https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=auto&tl=${to}&q=${encodeURIComponent(text)}`,
        });
        if (r.status !== 200) throw new Error('Google gtx error');
        const data = JSON.parse(r.responseText);
        return data[0].filter(s => s && s[0]).map(s => s[0]).join('');
      }
    },

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    //  引擎3:Google自动(先v2,失败降级legacy)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    google: {
      name: 'Google (Auto)',
      async translate(text, toLang) {
        try {
          return await Engine.google_v2.translate(text, toLang);
        } catch (e) {
          console.warn('Google v2 failed, falling back to legacy:', e.message);
          return await Engine.google_legacy.translate(text, toLang);
        }
      },
      async translateBatch(texts, toLang) {
        try {
          return await Engine.google_v2.translateBatch(texts, toLang);
        } catch (e) {
          console.warn('Google v2 batch failed, falling back to single:', e.message);
          // 逐条降级
          const results = [];
          for (const text of texts) {
            try {
              results.push(await Engine.google_legacy.translate(text, toLang));
            } catch (_) {
              results.push(null);
            }
          }
          return results;
        }
      }
    },

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    //  引擎4:Microsoft(保留原有实现)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    microsoft: {
      name: 'Microsoft',
      _token: null,
      _tokenTime: 0,

      async getToken() {
        if (this._token && Date.now() - this._tokenTime < 480000) return this._token;
        const r = await gmFetch({ method: 'GET', url: 'https://edge.microsoft.com/translate/auth' });
        if (r.status !== 200) throw new Error('MS auth error');
        this._token = r.responseText;
        this._tokenTime = Date.now();
        return this._token;
      },

      langCode(lang) {
        const map = {
          'zh': 'zh-Hans', 'zh-CN': 'zh-Hans', 'zh-TW': 'zh-Hant',
          'en': 'en', 'ja': 'ja', 'ko': 'ko', 'fr': 'fr', 'de': 'de',
          'es': 'es', 'ru': 'ru', 'pt': 'pt', 'pt-PT': 'pt-pt',
          'ar': 'ar', 'th': 'th', 'vi': 'vi', 'it': 'it', 'tr': 'tr',
          'id': 'id', 'fil': 'fil', 'ms': 'ms', 'nl': 'nl', 'pl': 'pl',
          'uk': 'uk', 'cs': 'cs', 'sk': 'sk', 'hu': 'hu', 'ro': 'ro',
          'bg': 'bg', 'hr': 'hr', 'sr': 'sr-Cyrl', 'sl': 'sl',
          'lt': 'lt', 'lv': 'lv', 'et': 'et', 'fi': 'fi', 'sv': 'sv',
          'da': 'da', 'no': 'nb', 'is': 'is', 'el': 'el', 'he': 'he',
          'hi': 'hi', 'bn': 'bn', 'ta': 'ta', 'te': 'te', 'kn': 'kn',
          'ml': 'ml', 'pa': 'pa', 'gu': 'gu', 'mr': 'mr', 'ne': 'ne',
          'ur': 'ur', 'fa': 'fa', 'ps': 'ps', 'my': 'my', 'km': 'km',
          'lo': 'lo', 'ka': 'ka', 'az': 'az', 'kk': 'kk', 'uz': 'uz',
          'mn': 'mn', 'sq': 'sq', 'mk': 'mk', 'bs': 'bs',
          'ca': 'ca', 'gl': 'gl', 'mt': 'mt', 'cy': 'cy', 'ga': 'ga',
          'af': 'af', 'sw': 'sw', 'ha': 'ha', 'ig': 'ig', 'yo': 'yo',
          'zu': 'zu', 'mg': 'mg', 'am': 'am', 'ht': 'ht',
          'bo': 'bo', 'dsb': 'dsb', 'hsb': 'hsb', 'ikt': 'ikt',
          'iu': 'iu', 'iu-Latn': 'iu-Latn', 'lzh': 'lzh',
          'fj': 'fj', 'fo': 'fo', 'fr-CA': 'fr-ca',
        };
        return map[lang] || lang;
      },

      async translate(text, toLang) {
        const token = await this.getToken();
        const to = this.langCode(toLang);
        const r = await gmFetch({
          method: 'POST',
          url: `https://api-edge.cognitive.microsofttranslator.com/translate?from=&to=${to}&api-version=3.0`,
          headers: { 'authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
          data: JSON.stringify([{ Text: text }]),
        });
        if (r.status !== 200) throw new Error('MS translate error');
        return JSON.parse(r.responseText)[0].translations[0].text;
      },

      async translateBatch(texts, toLang) {
        const token = await this.getToken();
        const to = this.langCode(toLang);
        const results = [];

        for (let batch = 0; batch < texts.length; batch += 25) {
          const chunk = texts.slice(batch, batch + 25);
          const r = await gmFetch({
            method: 'POST',
            url: `https://api-edge.cognitive.microsofttranslator.com/translate?from=&to=${to}&api-version=3.0`,
            headers: { 'authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
            data: JSON.stringify(chunk.map(t => ({ Text: t }))),
          });
          if (r.status === 200) {
            const data = JSON.parse(r.responseText);
            for (const item of data) {
              results.push(item.translations[0].text);
            }
          } else {
            for (let i = 0; i < chunk.length; i++) results.push(null);
          }
        }
        return results;
      }
    },

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    //  引擎5:Tencent(保留原有实现)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    tencent: {
      name: 'Tencent',
      _clientKey: null,

      getClientKey() {
        if (this._clientKey) return this._clientKey;
        this._clientKey = `browser-chrome-120.0-Windows_10-${crypto.randomUUID()}-${Date.now()}`;
        return this._clientKey;
      },

      langCode(lang) {
        const map = {
          'zh': 'zh', 'zh-CN': 'zh', 'zh-TW': 'zh-TW',
          'en': 'en', 'ja': 'ja', 'ko': 'ko', 'fr': 'fr',
          'de': 'de', 'es': 'es', 'ru': 'ru', 'pt': 'pt',
          'ar': 'ar', 'th': 'th', 'vi': 'vi', 'it': 'it',
          'tr': 'tr', 'id': 'id',
        };
        return map[lang] || lang;
      },

      async translate(text, toLang) {
        const to = this.langCode(toLang);
        const r = await gmFetch({
          method: 'POST',
          url: 'https://transmart.qq.com/api/imt',
          headers: { 'Content-Type': 'application/json' },
          data: JSON.stringify({
            header: { fn: 'auto_translation', session: '', client_key: this.getClientKey(), user: '' },
            type: 'plain', model_category: 'normal', text_domain: 'general',
            source: { lang: 'auto', text_list: [text] },
            target: { lang: to }
          }),
        });
        if (r.status !== 200) throw new Error('Tencent translate error');
        return JSON.parse(r.responseText).auto_translation[0];
      },

      async translateBatch(texts, toLang) {
        const to = this.langCode(toLang);
        const r = await gmFetch({
          method: 'POST',
          url: 'https://transmart.qq.com/api/imt',
          headers: { 'Content-Type': 'application/json' },
          data: JSON.stringify({
            header: { fn: 'auto_translation', session: '', client_key: this.getClientKey(), user: '' },
            type: 'plain', model_category: 'normal', text_domain: 'general',
            source: { lang: 'auto', text_list: texts },
            target: { lang: to }
          }),
        });
        if (r.status !== 200) throw new Error('Tencent batch error');
        return JSON.parse(r.responseText).auto_translation;
      }
    }
  };

  // ── 持久化状态 ──
  let currentEngine = await GM_getValue('engine', 'google');
  let targetLang = await GM_getValue('targetLang', deviceLang === 'zh' ? 'zh-CN' : deviceLang);
  let autoMode = await GM_getValue('autoMode', true);
  let excludedHosts = JSON.parse(await GM_getValue('excludedHosts', '[]'));

  if (excludedHosts.includes(location.host)) return;

  // ── 缓存层 ──
  const cache = new Map();
  const MAX_CACHE = 3000;

  function cacheGet(text) { return cache.get(text); }
  function cacheSet(text, translated) {
    if (cache.size >= MAX_CACHE) {
      const firstKey = cache.keys().next().value;
      cache.delete(firstKey);
    }
    cache.set(text, translated);
  }

  // ── 翻译核心(带引擎自动降级)──
  async function translate(text) {
    if (!text || !text.trim()) return null;
    const trimmed = text.trim();
    if (/^\d+$/.test(trimmed)) return null;

    const cached = cacheGet(trimmed);
    if (cached) return cached;

    try {
      const engine = Engine[currentEngine];
      const result = await engine.translate(trimmed, targetLang);
      if (result && result !== trimmed) {
        cacheSet(trimmed, result);
        return result;
      }
    } catch (e) {
      console.warn(`[${currentEngine}] translate error:`, e.message);
      // 自动降级:Google失败用Microsoft,Microsoft失败用Google legacy
      if (currentEngine === 'google') {
        try {
          const result = await Engine.microsoft.translate(trimmed, targetLang);
          if (result && result !== trimmed) { cacheSet(trimmed, result); return result; }
        } catch (_) {}
      } else if (currentEngine === 'microsoft') {
        try {
          const result = await Engine.google_legacy.translate(trimmed, targetLang);
          if (result && result !== trimmed) { cacheSet(trimmed, result); return result; }
        } catch (_) {}
      }
    }
    return null;
  }

  // ── 批量翻译(利用引擎原生批量能力)──
  async function batchTranslate(texts) {
    const results = new Array(texts.length).fill(null);
    const uncached = [];
    const uncachedIdx = [];

    for (let i = 0; i < texts.length; i++) {
      const t = texts[i].trim();
      if (!t || /^\d+$/.test(t)) continue;
      const c = cacheGet(t);
      if (c) { results[i] = c; continue; }
      uncached.push(t);
      uncachedIdx.push(i);
    }

    if (uncached.length === 0) return results;

    const engine = Engine[currentEngine];

    // 如果引擎支持批量翻译,优先使用
    if (engine.translateBatch && uncached.length > 1) {
      try {
        // 分批处理(每批最多50条,避免请求过大)
        const BATCH_SIZE = currentEngine === 'microsoft' ? 25 : 50;
        for (let batch = 0; batch < uncached.length; batch += BATCH_SIZE) {
          const chunk = uncached.slice(batch, batch + BATCH_SIZE);
          const chunkIdx = uncachedIdx.slice(batch, batch + BATCH_SIZE);

          const batchResults = await engine.translateBatch(chunk, targetLang);
          if (batchResults) {
            for (let j = 0; j < batchResults.length; j++) {
              const translated = batchResults[j];
              if (translated && translated !== chunk[j]) {
                cacheSet(chunk[j], translated);
                results[chunkIdx[j]] = translated;
              }
            }
          }
        }
        return results;
      } catch (e) {
        console.warn('Batch translate failed, falling back to single:', e.message);
      }
    }

    // 逐条翻译(fallback)
    const concurrency = 5;
    for (let i = 0; i < uncached.length; i += concurrency) {
      const batch = uncached.slice(i, i + concurrency);
      const batchIdx = uncachedIdx.slice(i, i + concurrency);
      await Promise.allSettled(
        batch.map(async (text, j) => {
          const result = await translate(text);
          if (result) results[batchIdx[j]] = result;
        })
      );
    }

    return results;
  }

  // ── DOM遍历与替换 ──
  const SKIP_TAGS = /^(script|style|code|pre|svg|math|noscript|iframe|canvas|video|audio|img|br|hr|input|select|option|textarea)$/i;
  const SKIP_CLASS = /translate-ui|notranslate|katex|mathjax/i;

  function shouldSkip(node) {
    if (!node) return true;
    if (node.nodeType === Node.ELEMENT_NODE) {
      if (SKIP_TAGS.test(node.tagName)) return true;
      if (SKIP_CLASS.test(node.className)) return true;
      if (node.isContentEditable) return true;
      if (node.dataset && node.dataset.translated) return true;
    }
    return false;
  }

  function isTargetLang(text) {
    if (!text || !text.trim()) return true;
    const lang = targetLang.split('-')[0];
    if (lang === 'zh') return /^[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\s\d\p{P}]+$/u.test(text.trim());
    if (lang === 'en') return /^[a-zA-Z\s\d\p{P}]+$/u.test(text.trim());
    if (lang === 'ja') return /^[\u3040-\u309f\u30a0-\u30ff\u4e00-\u9fff\s\d\p{P}]+$/u.test(text.trim());
    if (lang === 'ko') return /^[\uac00-\ud7af\u1100-\u11ff\s\d\p{P}]+$/u.test(text.trim());
    if (lang === 'ar') return /^[\u0600-\u06ff\u0750-\u077f\s\d\p{P}]+$/u.test(text.trim());
    if (lang === 'th') return /^[\u0e00-\u0e7f\s\d\p{P}]+$/u.test(text.trim());
    if (lang === 'ru') return /^[\u0400-\u04ff\s\d\p{P}]+$/u.test(text.trim());
    return false;
  }

  function collectTextNodes(root) {
    const nodes = [];
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
      acceptNode(node) {
        if (shouldSkip(node.parentElement)) return NodeFilter.FILTER_REJECT;
        const text = node.textContent.trim();
        if (!text || text.length < 2) return NodeFilter.FILTER_REJECT;
        if (/^\d+$/.test(text)) return NodeFilter.FILTER_REJECT;
        if (isTargetLang(text)) return NodeFilter.FILTER_REJECT;
        if (node.parentElement?.dataset?.translated) return NodeFilter.FILTER_REJECT;
        return NodeFilter.FILTER_ACCEPT;
      }
    });
    while (walker.nextNode()) nodes.push(walker.currentNode);
    return nodes;
  }

  function collectPlaceholders(root) {
    return [...root.querySelectorAll('input[placeholder], textarea[placeholder]')]
      .filter(el => !el.dataset.translated && el.placeholder.trim() && !isTargetLang(el.placeholder));
  }

  // ── 页面翻译执行 ──
  let isTranslating = false;

  async function translatePage(root) {
    if (isTranslating) return;
    isTranslating = true;

    try {
      root = root || document.body;
      const textNodes = collectTextNodes(root);
      const placeholders = collectPlaceholders(root);

      if (textNodes.length === 0 && placeholders.length === 0) return;

      // 文本节点翻译
      if (textNodes.length > 0) {
        const texts = textNodes.map(n => n.textContent.trim());
        const results = await batchTranslate(texts);

        for (let i = 0; i < textNodes.length; i++) {
          if (!results[i]) continue;
          const node = textNodes[i];
          const parent = node.parentElement;
          if (!parent) continue;
          if (!parent.dataset.originalText) {
            parent.dataset.originalText = node.textContent;
          }
          parent.dataset.translated = '1';
          node.textContent = results[i];
        }
      }

      // placeholder翻译
      if (placeholders.length > 0) {
        const phTexts = placeholders.map(el => el.placeholder.trim());
        const phResults = await batchTranslate(phTexts);
        for (let i = 0; i < placeholders.length; i++) {
          if (!phResults[i]) continue;
          placeholders[i].dataset.originalPlaceholder = placeholders[i].placeholder;
          placeholders[i].placeholder = phResults[i];
          placeholders[i].dataset.translated = '1';
        }
      }
    } finally {
      isTranslating = false;
    }
  }

  // ── 还原原文 ──
  function restorePage() {
    document.querySelectorAll('[data-translated]').forEach(el => {
      if (el.dataset.originalText) {
        for (const child of el.childNodes) {
          if (child.nodeType === Node.TEXT_NODE) {
            child.textContent = el.dataset.originalText;
            break;
          }
        }
        delete el.dataset.originalText;
      }
      if (el.dataset.originalPlaceholder) {
        el.placeholder = el.dataset.originalPlaceholder;
        delete el.dataset.originalPlaceholder;
      }
      delete el.dataset.translated;
    });
  }

  // ── 动态内容监听 ──
  let scrollTimer = null;
  let lastHeight = document.documentElement.scrollHeight;

  function onScroll() {
    if (scrollTimer) clearTimeout(scrollTimer);
    scrollTimer = setTimeout(() => {
      const h = document.documentElement.scrollHeight;
      if (h > lastHeight) {
        lastHeight = h;
        if (autoMode) translatePage();
      }
    }, 800);
  }

  let mutationTimer = null;
  const observer = new MutationObserver((mutations) => {
    if (!autoMode) return;
    if (mutationTimer) clearTimeout(mutationTimer);
    mutationTimer = setTimeout(() => {
      const roots = new Set();
      for (const m of mutations) {
        for (const node of m.addedNodes) {
          if (node.nodeType === Node.ELEMENT_NODE && !shouldSkip(node)) {
            roots.add(node);
          }
        }
      }
      roots.forEach(root => translatePage(root));
    }, 1000);
  });

  // ══════════════════════════════════════════════
  //  UI - 支持249种语言的完整选择器
  // ══════════════════════════════════════════════

  // 构建语言选项HTML
  function buildLangOptions() {
    let html = '';
    for (const [group, codes] of Object.entries(LANG_GROUPS)) {
      html += ``;
      for (const code of codes) {
        const name = ALL_LANGUAGES[code] || code;
        const selected = code === targetLang ? ' selected' : '';
        html += ``;
      }
      html += '';
    }
    return html;
  }

  GM_addStyle(`
    .translate-ui{position:fixed;bottom:20px;right:20px;z-index:999999;font-family:system-ui,-apple-system,sans-serif}
    .translate-ui *{box-sizing:border-box;margin:0;padding:0}
    .tu-btn{width:42px;height:42px;border-radius:50%;border:none;background:rgba(0,0,0,0.5);color:#fff;cursor:pointer;
      display:flex;align-items:center;justify-content:center;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
      box-shadow:0 2px 8px rgba(0,0,0,0.15);transition:transform .2s,background .2s;touch-action:manipulation}
    .tu-btn:active{transform:scale(0.9)}
    .tu-btn.active{background:rgba(34,128,255,0.8)}
    .tu-panel{position:absolute;bottom:52px;right:0;width:240px;max-height:80vh;overflow-y:auto;
      background:rgba(255,255,255,0.97);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
      border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,0.12);padding:12px;display:none;color:#333;font-size:13px}
    .tu-panel.show{display:block}
    .tu-panel label{display:block;margin:8px 0 4px;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:.5px}
    .tu-panel select{width:100%;padding:6px 8px;border:1px solid #ddd;border-radius:6px;font-size:12px;
      background:#fff;color:#333;outline:none;appearance:auto}
    .tu-panel select:focus{border-color:#4a9eff}
    .tu-panel optgroup{font-weight:bold;color:#666;font-size:11px}
    .tu-panel option{font-weight:normal;color:#333;font-size:12px}
    .tu-status{margin-top:8px;padding:6px;background:#f8f8f8;border-radius:6px;font-size:11px;color:#666;text-align:center}
    .tu-row{display:flex;gap:6px;margin-top:10px}
    .tu-row button{flex:1;padding:7px 0;border:none;border-radius:6px;font-size:12px;cursor:pointer;
      transition:background .2s;touch-action:manipulation}
    .tu-row .tu-restore{background:#f0f0f0;color:#555}
    .tu-row .tu-restore:active{background:#ddd}
    .tu-row .tu-go{background:#4a9eff;color:#fff}
    .tu-row .tu-go:active{background:#3080dd}
    .tu-row .tu-exclude{background:#ff6b6b;color:#fff;font-size:11px}
    .tu-row .tu-exclude:active{background:#e55}
    @media(prefers-color-scheme:dark){
      .tu-panel{background:rgba(30,30,30,0.97);color:#eee}
      .tu-panel select{background:#2a2a2a;color:#eee;border-color:#444}
      .tu-panel optgroup{color:#aaa}
      .tu-panel option{color:#eee}
      .tu-row .tu-restore{background:#333;color:#ccc}
      .tu-status{background:#222;color:#999}
    }
  `);

  const ui = document.createElement('div');
  ui.className = 'translate-ui';
  ui.innerHTML = `
    
Ready
`; document.body.appendChild(ui); const btn = document.getElementById('tuBtn'); const panel = document.getElementById('tuPanel'); const engineSelect = document.getElementById('tuEngine'); const langSelect = document.getElementById('tuLang'); const statusEl = document.getElementById('tuStatus'); engineSelect.value = currentEngine; langSelect.value = targetLang; function setStatus(msg) { if (statusEl) statusEl.textContent = msg; } btn.addEventListener('click', (e) => { e.stopPropagation(); panel.classList.toggle('show'); }); document.addEventListener('click', (e) => { if (!ui.contains(e.target)) panel.classList.remove('show'); }); engineSelect.addEventListener('change', () => { currentEngine = engineSelect.value; GM_setValue('engine', currentEngine); cache.clear(); setStatus(`Engine: ${Engine[currentEngine]?.name || currentEngine}`); }); langSelect.addEventListener('change', () => { targetLang = langSelect.value; GM_setValue('targetLang', targetLang); cache.clear(); setStatus(`Target: ${ALL_LANGUAGES[targetLang] || targetLang}`); }); document.getElementById('tuGo').addEventListener('click', async () => { panel.classList.remove('show'); btn.classList.add('active'); autoMode = true; GM_setValue('autoMode', true); restorePage(); cache.clear(); lastHeight = document.documentElement.scrollHeight; setStatus('翻译中...'); const start = Date.now(); await translatePage(); setStatus(`完成 (${((Date.now() - start) / 1000).toFixed(1)}s)`); }); document.getElementById('tuRestore').addEventListener('click', () => { panel.classList.remove('show'); btn.classList.remove('active'); autoMode = false; GM_setValue('autoMode', false); restorePage(); setStatus('已还原'); }); document.getElementById('tuExclude').addEventListener('click', () => { if (!excludedHosts.includes(location.host)) { excludedHosts.push(location.host); GM_setValue('excludedHosts', JSON.stringify(excludedHosts)); } restorePage(); ui.remove(); observer.disconnect(); window.removeEventListener('scroll', onScroll); }); // ── 菜单命令 ── GM_registerMenuCommand('翻译当前页面', () => translatePage()); GM_registerMenuCommand('还原当前页面', () => restorePage()); GM_registerMenuCommand('切换Google引擎', () => { currentEngine = 'google'; engineSelect.value = 'google'; GM_setValue('engine', 'google'); cache.clear(); }); GM_registerMenuCommand('切换Microsoft引擎', () => { currentEngine = 'microsoft'; engineSelect.value = 'microsoft'; GM_setValue('engine', 'microsoft'); cache.clear(); }); // ── 启动 ── function isPageInTargetLang() { const lang = (document.documentElement.lang || '').split('-')[0].toLowerCase(); const target = targetLang.split('-')[0].toLowerCase(); return lang === target; } window.addEventListener('scroll', onScroll, { passive: true }); observer.observe(document.body, { childList: true, subtree: true }); // 预初始化Google Auth(后台静默执行) GoogleHelper_v2.findAuth().catch(() => {}); if (autoMode && !isPageInTargetLang()) { setTimeout(async () => { setStatus('自动翻译中...'); const start = Date.now(); await translatePage(); setStatus(`完成 (${((Date.now() - start) / 1000).toFixed(1)}s)`); }, 1500); } })();