Greasy Fork

来自缓存

Greasy Fork is available in English.

X 翻译姬

将推文翻译为简体中文,并在下方显示

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         X 翻译姬
// @namespace    https://github.com/SIXiaolong1117/Rules
// @version      0.3
// @description  将推文翻译为简体中文,并在下方显示
// @license      MIT
// @icon         https://x.com/favicon.ico
// @author       SI Xiaolong
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      translate.googleapis.com
// @connect      api.mymemory.translated.net
// ==/UserScript==

(function () {
    'use strict';

    // 已翻译的元素集合,避免重复翻译
    const translatedElements = new WeakSet();

    // 默认设置
    const DEFAULT_SETTINGS = {
        translationSource: 'google',
        sourceLang: 'auto',
        targetLang: 'zh-CN'
    };

    // 获取当前设置
    function getSettings() {
        return {
            translationSource: GM_getValue('translationSource', DEFAULT_SETTINGS.translationSource),
            sourceLang: GM_getValue('sourceLang', DEFAULT_SETTINGS.sourceLang),
            targetLang: GM_getValue('targetLang', DEFAULT_SETTINGS.targetLang)
        };
    }

    // 保存设置
    function saveSetting(key, value) {
        GM_setValue(key, value);
        location.reload();
    }

    // 翻译源配置
    const TRANSLATION_SOURCES = {
        google: 'Google 翻译',
        mymemory: 'MyMemory 翻译'
    };

    // 语言配置
    const LANGUAGES = {
        'auto': '自动检测',
        'zh-CN': '简体中文',
        'zh-TW': '繁体中文',
        'en': '英语',
        'ja': '日语',
        'ko': '韩语',
        'es': '西班牙语',
        'fr': '法语',
        'de': '德语',
        'ru': '俄语',
        'ar': '阿拉伯语',
        'pt': '葡萄牙语',
        'it': '意大利语',
        'th': '泰语',
        'vi': '越南语'
    };

    // 检测当前主题(深色或浅色)
    function detectTheme() {
        const bgColor = window.getComputedStyle(document.body).backgroundColor;
        const rgb = bgColor.match(/\d+/g);
        if (rgb) {
            const brightness = (parseInt(rgb[0]) + parseInt(rgb[1]) + parseInt(rgb[2])) / 3;
            return brightness > 128 ? 'light' : 'dark';
        }
        return 'dark';
    }

    // 获取主题相关的颜色
    function getThemeColors() {
        const theme = detectTheme();

        if (theme === 'light') {
            return {
                background: 'rgba(247, 249, 249, 0.8)',
                loadingText: '#536471',
                translatedText: '#0f1419',
                errorText: '#f4212e',
                headerText: '#536471',
                border: 'rgba(207, 217, 222, 0.3)'
            };
        } else {
            return {
                background: 'rgba(32, 35, 39, 0.8)',
                loadingText: '#8b949e',
                translatedText: '#e7e9ea',
                errorText: '#ff6b6b',
                headerText: '#8b949e',
                border: 'rgba(47, 51, 54, 0.3)'
            };
        }
    }

    // 注册菜单命令
    function registerMenuCommands() {
        const settings = getSettings();

        // 翻译源选择
        GM_registerMenuCommand(`🌐 翻译源: ${TRANSLATION_SOURCES[settings.translationSource]}`, () => {
            const sources = Object.keys(TRANSLATION_SOURCES);
            const currentIndex = sources.indexOf(settings.translationSource);
            const nextIndex = (currentIndex + 1) % sources.length;
            const nextSource = sources[nextIndex];
            saveSetting('translationSource', nextSource);
        });

        // 源语言选择
        GM_registerMenuCommand(`📤 源语言: ${LANGUAGES[settings.sourceLang]}`, () => {
            showLanguageSelector('sourceLang', '选择源语言');
        });

        // 目标语言选择
        GM_registerMenuCommand(`📥 目标语言: ${LANGUAGES[settings.targetLang]}`, () => {
            showLanguageSelector('targetLang', '选择目标语言');
        });

        // 重置设置
        GM_registerMenuCommand('🔄 重置为默认设置', () => {
            if (confirm('确定要重置所有设置为默认值吗?')) {
                saveSetting('translationSource', DEFAULT_SETTINGS.translationSource);
                saveSetting('sourceLang', DEFAULT_SETTINGS.sourceLang);
                saveSetting('targetLang', DEFAULT_SETTINGS.targetLang);
            }
        });
    }

    // 显示语言选择对话框
    function showLanguageSelector(settingKey, title) {
        const languages = Object.keys(LANGUAGES);
        const languageList = languages.map((code, index) =>
            `${index + 1}. ${LANGUAGES[code]} (${code})`
        ).join('\n');

        const input = prompt(`${title}\n\n${languageList}\n\n请输入语言代码(如 zh-CN, en, ja 等):`);

        if (input && LANGUAGES[input]) {
            saveSetting(settingKey, input);
        } else if (input) {
            alert('无效的语言代码!');
        }
    }

    // Google翻译API
    async function translateWithGoogle(text, sourceLang, targetLang) {
        const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: function (response) {
                    try {
                        const result = JSON.parse(response.responseText);
                        const translatedText = result[0].map(item => item[0]).join('');
                        resolve(translatedText);
                    } catch (e) {
                        reject(e);
                    }
                },
                onerror: function (error) {
                    reject(error);
                }
            });
        });
    }

    // MyMemory翻译API
    async function translateWithMyMemory(text, sourceLang, targetLang) {
        const langPair = `${sourceLang === 'auto' ? 'en' : sourceLang}|${targetLang}`;
        const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${langPair}`;

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: function (response) {
                    try {
                        const result = JSON.parse(response.responseText);
                        if (result.responseStatus === 200) {
                            resolve(result.responseData.translatedText);
                        } else {
                            reject(new Error('Translation failed'));
                        }
                    } catch (e) {
                        reject(e);
                    }
                },
                onerror: function (error) {
                    reject(error);
                }
            });
        });
    }

    // 统一翻译接口
    async function translateText(text) {
        const settings = getSettings();
        const { translationSource, sourceLang, targetLang } = settings;

        switch (translationSource) {
            case 'google':
                return await translateWithGoogle(text, sourceLang, targetLang);
            case 'mymemory':
                return await translateWithMyMemory(text, sourceLang, targetLang);
            default:
                return await translateWithGoogle(text, sourceLang, targetLang);
        }
    }

    // 保护文本中的特殊元素(@mentions, #hashtags, URLs)
    function protectSpecialElements(text) {
        const protectedElements = [];
        let protectedText = text;

        // 保护 URL(优先处理,因为URL可能包含其他特殊字符)
        protectedText = protectedText.replace(/https?:\/\/[^\s]+/g, (match) => {
            const placeholder = `__URL_${protectedElements.length}__`;
            protectedElements.push(match);
            return placeholder;
        });

        // 保护 @mentions
        protectedText = protectedText.replace(/@[\w]+/g, (match) => {
            const placeholder = `__MENTION_${protectedElements.length}__`;
            protectedElements.push(match);
            return placeholder;
        });

        // 保护 #hashtags
        protectedText = protectedText.replace(/#[\w\u4e00-\u9fa5]+/g, (match) => {
            const placeholder = `__HASHTAG_${protectedElements.length}__`;
            protectedElements.push(match);
            return placeholder;
        });

        return { protectedText, protectedElements };
    }

    // 恢复保护的特殊元素
    function restoreSpecialElements(text, protectedElements) {
        let restoredText = text;

        protectedElements.forEach((element, index) => {
            if (element.startsWith('@')) {
                restoredText = restoredText.replace(`__MENTION_${index}__`, element);
            } else if (element.startsWith('#')) {
                restoredText = restoredText.replace(`__HASHTAG_${index}__`, element);
            } else if (element.startsWith('http')) {
                restoredText = restoredText.replace(`__URL_${index}__`, element);
            }
        });

        return restoredText;
    }

    // 提取纯文本内容(不提取链接信息)
    function extractPlainText(element) {
        let textParts = [];

        function traverse(node) {
            // 跳过某些不需要翻译的元素(如按钮、SVG等)
            if (node.nodeType === Node.ELEMENT_NODE) {
                const tagName = node.tagName.toLowerCase();
                if (tagName === 'button' || tagName === 'svg' || tagName === 'path') {
                    return;
                }
            }

            for (let child of node.childNodes) {
                if (child.nodeType === Node.TEXT_NODE) {
                    const text = child.textContent.trim();
                    if (text) textParts.push(text);
                } else if (child.nodeType === Node.ELEMENT_NODE) {
                    // 对于链接元素,直接提取文本内容,保持位置
                    if (child.tagName === 'A') {
                        const linkText = child.textContent.trim();
                        if (linkText) textParts.push(linkText);
                    } else {
                        // 递归处理其他元素
                        traverse(child);
                    }
                }
            }
        }

        traverse(element);
        return textParts.join(' ');
    }

    // 创建占位翻译框
    function createPlaceholderBox() {
        const settings = getSettings();
        const sourceName = TRANSLATION_SOURCES[settings.translationSource];
        const colors = getThemeColors();

        const box = document.createElement('div');
        box.className = 'x-translator-box';
        box.style.cssText = `
            margin-top: 12px;
            padding: 12px 16px;
            background-color: ${colors.background};
            border: 1px solid ${colors.border};
            border-radius: 12px;
            font-size: 15px;
            line-height: 1.5;
            transition: background-color 0.2s ease, border-color 0.2s ease;
        `;

        // 创建标题
        const header = document.createElement('div');
        header.className = 'x-translator-header';
        header.style.cssText = `
            font-size: 13px;
            color: ${colors.headerText};
            margin-bottom: 8px;
            font-weight: 500;
            transition: color 0.2s ease;
        `;
        header.textContent = `📝 ${sourceName}`;

        // 创建加载中的内容容器
        const content = document.createElement('div');
        content.className = 'x-translator-content';
        content.style.cssText = `
            color: ${colors.loadingText};
            white-space: pre-wrap;
            word-wrap: break-word;
            transition: color 0.2s ease;
        `;
        content.textContent = '翻译中...';

        box.appendChild(header);
        box.appendChild(content);

        return box;
    }

    // 更新翻译框内容(成功)
    function updateTranslationBox(box, translatedText) {
        const colors = getThemeColors();
        const content = box.querySelector('.x-translator-content');

        content.style.color = colors.translatedText;
        content.textContent = translatedText;
    }

    // 更新翻译框内容(失败)
    function updateTranslationBoxError(box, errorMessage) {
        const colors = getThemeColors();
        const content = box.querySelector('.x-translator-content');

        content.style.color = colors.errorText;
        content.textContent = `❌ 翻译失败: ${errorMessage}`;
    }

    // 更新所有翻译框的主题
    function updateAllBoxesTheme() {
        const colors = getThemeColors();
        const boxes = document.querySelectorAll('.x-translator-box');

        boxes.forEach(box => {
            box.style.backgroundColor = colors.background;
            box.style.borderColor = colors.border;

            const header = box.querySelector('.x-translator-header');
            if (header) {
                header.style.color = colors.headerText;
            }

            const content = box.querySelector('.x-translator-content');
            if (content) {
                const text = content.textContent;
                if (text === '翻译中...') {
                    content.style.color = colors.loadingText;
                } else if (text.startsWith('❌')) {
                    content.style.color = colors.errorText;
                } else {
                    content.style.color = colors.translatedText;
                }
            }
        });
    }

    // 处理翻译
    async function processTranslation(element, isTrend = false) {
        // 如果已经翻译过,跳过
        if (translatedElements.has(element)) {
            return;
        }

        // 标记为已处理
        translatedElements.add(element);

        // 提取纯文本内容
        const originalText = extractPlainText(element);

        if (originalText.length === 0) {
            return;
        }

        // 对于热搜,进行额外过滤
        if (isTrend) {
            // 排除"xx的趋势"这类描述性文字
            if (originalText.match(/的趋势|条推文|Trending|posts?$/i)) {
                return;
            }
            // 如果文本太短(少于2个字符),也跳过
            if (originalText.length < 2) {
                return;
            }
        }

        // 创建占位框并立即插入到页面
        const placeholderBox = createPlaceholderBox();
        const parentContainer = element.parentElement;
        if (parentContainer) {
            parentContainer.insertBefore(placeholderBox, element.nextSibling);
        }

        try {
            // 保护特殊元素
            const { protectedText, protectedElements } = protectSpecialElements(originalText);

            // 翻译文本
            const rawTranslatedText = await translateText(protectedText);

            // 恢复特殊元素
            const translatedText = restoreSpecialElements(rawTranslatedText, protectedElements);

            // 如果翻译结果与原文相同,说明可能已经是目标语言,移除占位框
            if (translatedText === originalText) {
                if (placeholderBox.parentNode) {
                    placeholderBox.parentNode.removeChild(placeholderBox);
                }
                return;
            }

            // 更新占位框内容为翻译结果
            updateTranslationBox(placeholderBox, translatedText);

        } catch (e) {
            console.error('翻译失败:', e);
            // 更新占位框内容为错误信息
            updateTranslationBoxError(placeholderBox, e.message || '未知错误');
        }
    }

    // 查找并处理所有推文
    function findAndTranslateTweets() {
        const tweetDivs = document.querySelectorAll('[data-testid="tweetText"]');
        tweetDivs.forEach(div => {
            processTranslation(div, false);
        });
    }

    // 查找并处理用户简介
    function findAndTranslateUserDescription() {
        const userDescriptions = document.querySelectorAll('[data-testid="UserDescription"]');
        userDescriptions.forEach(div => {
            processTranslation(div, false);
        });
    }

    // 查找并处理热搜趋势(优化版)
    function findAndTranslateTrends() {
        const trends = document.querySelectorAll('[data-testid="trend"]');
        trends.forEach(trendDiv => {
            const divs = trendDiv.querySelectorAll('div[dir="ltr"]');
            let trendTextElement = null;

            for (let div of divs) {
                const text = div.textContent.trim();
                const classList = div.className || '';

                if (!text) continue;
                if (text.match(/趋势|Trending/i)) continue;
                if (text.match(/条帖子|posts?$/i)) continue;
                if (text.match(/^[\d,]+$/)) continue;

                if (classList.includes('r-b88u0q') ||
                    (!trendTextElement && text.length > 0)) {
                    trendTextElement = div;
                    break;
                }
            }

            if (trendTextElement) {
                processTranslation(trendTextElement, true);
            }
        });
    }

    // 查找并翻译所有内容
    function findAndTranslateAll() {
        findAndTranslateTweets();
        findAndTranslateUserDescription();
        findAndTranslateTrends();
    }

    // 监听DOM变化,处理动态加载的内容
    const observer = new MutationObserver((mutations) => {
        findAndTranslateAll();
    });

    // 监听主题变化
    const themeObserver = new MutationObserver(() => {
        updateAllBoxesTheme();
    });

    // 注册菜单
    registerMenuCommands();

    // 开始观察
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // 监听body的属性变化(主题切换通常会改变body的class或style)
    themeObserver.observe(document.body, {
        attributes: true,
        attributeFilter: ['class', 'style']
    });

    // 监听HTML元素的属性变化(有些网站会在html元素上设置主题)
    themeObserver.observe(document.documentElement, {
        attributes: true,
        attributeFilter: ['class', 'style', 'data-theme']
    });

    // 初始翻译
    findAndTranslateAll();

    // 定期检查新内容(作为备用)
    setInterval(findAndTranslateAll, 2000);

    console.log('Twitter翻译脚本已加载,当前设置:', getSettings());

})();