axiom 推文翻译

自动翻译 axiom.trade 全站推文内容、用户简介,含主贴+预览+引用推文

当前为 2025-07-20 提交的版本,查看 最新版本

// ==UserScript==
// @name         axiom 推文翻译
// @namespace    https://x.com/Crawford886
// @version      2.3
// @author       https://x.com/Crawford886
// @description  自动翻译 axiom.trade 全站推文内容、用户简介,含主贴+预览+引用推文
// @match        https://axiom.trade/pulse*
// @match        https://axiom.trade/meme/*
// @match        https://axiom.trade/discover*
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // ✅ Google Translate 非官方接口
    async function translateToChinese(text) {
        const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=zh-CN&dt=t&q=${encodeURIComponent(text)}`;
        try {
            const res = await fetch(url);
            const json = await res.json();
            const translated = json[0].map(item => item[0]).join('');
            if (translated.length < text.length / 2) {
                console.warn("翻译结果可能不完整:", text, "=>", translated);
            }
            return translated;
        } catch (err) {
            console.error("翻译失败:", err);
            return '[翻译失败]';
        }
    }

    function isChinese(text) {
        return /[\u4e00-\u9fa5]/.test(text);
    }

    const processedSet = new WeakSet(); // 避免重复翻译的容器

    // ✅ 1. 翻译新版推文预览弹窗(优化防重复和多行支持)
    async function processPreviewPopup(popupElement) {
        if (popupElement.getAttribute('data-popup-processed')) return;

        const processWithRetry = async (retryCount = 0) => {
            const maxRetries = 3;
            const delay = 200;

            const mainTweetSpans = popupElement.querySelectorAll('span.text-\\[18px\\].text-wrap.break-words.break-all.text-white:not([data-translated])');
            const quoteTweetSpans = popupElement.querySelectorAll('span.text-\\[16px\\].text-wrap.break-words.break-all.text-white:not([data-translated])');

            const totalSpans = mainTweetSpans.length + quoteTweetSpans.length;

            if (totalSpans > 0) {
                popupElement.setAttribute('data-popup-processed', 'true');

                for (const span of mainTweetSpans) {
                    await processTextSpan(span, '18px');
                }
                for (const span of quoteTweetSpans) {
                    await processTextSpan(span, '16px');
                }
                return true;
            }

            if (retryCount < maxRetries) {
                setTimeout(() => processWithRetry(retryCount + 1), delay);
                return false;
            }

            popupElement.setAttribute('data-popup-processed', 'true');
            return false;
        };

        await processWithRetry();
    }

    // 处理文本span的翻译(支持长文本分割)
    async function processTextSpan(span, fontSize) {
        if (processedSet.has(span)) return;

        const originalText = span.innerText.trim();
        if (!originalText || isChinese(originalText)) {
            span.setAttribute('data-translated', 'true');
            return;
        }

        // 检查是否已有翻译元素
        const nextSibling = span.nextSibling;
        if (nextSibling && nextSibling.nodeType === 1 && nextSibling.textContent.includes('🈯️')) {
            span.setAttribute('data-translated', 'true');
            processedSet.add(span);
            return;
        }

        // 标记为正在处理
        processedSet.add(span);

        // 分割长文本为句子或短段
        const sentences = originalText.split(/(?<=[.!?])\s+/).filter(s => s.trim());
        for (const sentence of sentences) {
            if (!sentence.trim() || isChinese(sentence.trim())) continue;
            const translated = await translateToChinese(sentence.trim());

            const translatedDiv = document.createElement("div");
            translatedDiv.style.color = "rgb(154, 167, 176)";
            translatedDiv.style.fontSize = "13px";
            translatedDiv.style.marginTop = "6px";
            translatedDiv.style.wordBreak = "break-word";
            translatedDiv.style.overflowWrap = "anywhere";
            translatedDiv.style.whiteSpace = "pre-wrap";
            translatedDiv.textContent = "🈯️ " + translated;
            translatedDiv.setAttribute('data-translation', 'true');

            span.parentElement.insertBefore(translatedDiv, span.nextSibling);
        }

        span.setAttribute('data-translated', 'true');
    }

    // ✅ 2. 翻译旧版推文预览区域(保持向下兼容)
    async function processPreviewTweet(tweetElement) {
        const tweetTextP = tweetElement.querySelector("p.tweet-body_root__ChzUj");
        if (!tweetTextP || tweetTextP.getAttribute('data-translated')) return;

        const originalText = tweetTextP.innerText.trim();
        if (!originalText) return;

        const translated = await translateToChinese(originalText);

        const translatedP = document.createElement("p");
        translatedP.style.color = "rgb(154, 167, 176)";
        translatedP.style.fontSize = "13px";
        translatedP.style.marginTop = "4px";
        translatedP.textContent = "🈯️ " + translated;

        tweetTextP.parentElement.insertBefore(translatedP, tweetTextP.nextSibling);
        tweetTextP.setAttribute('data-translated', 'true');
    }

    // ✅ 3. 翻译主贴/引用推文正文(优化推文监控支持)
    async function processInlineTweet(p) {
        if (processedSet.has(p) || p.getAttribute('data-translated')) return;

        const text = p.innerText.trim();
        if (!text || isChinese(text)) return;

        if (p.querySelector('.translated-inline') || p.nextElementSibling?.classList?.contains('translated-inline')) {
            processedSet.add(p);
            p.setAttribute('data-translated', 'true');
            return;
        }

        processedSet.add(p);
        p.setAttribute('data-translated', 'true');

        const translated = await translateToChinese(text);
        const span = document.createElement('span');
        span.className = 'translated-inline';
        span.innerText = `🈯️ ${translated}`;
        span.style.display = 'block';
        span.style.color = 'rgb(154, 167, 176)';
        span.style.fontSize = '13px';
        span.style.marginTop = '3px';

        p.insertAdjacentElement('afterend', span);
    }

    // ✅ 新增:专门处理推文监控界面
    async function processTwitterAlert(alertElement) {
        const mainTweetPs = alertElement.querySelectorAll('p.text-textSecondary.mt-1.whitespace-pre-wrap:not([data-translated])');
        for (const p of mainTweetPs) {
            await processInlineTweet(p);
        }

        const quoteTweetPs = alertElement.querySelectorAll('div.border.border-secondaryStroke p.text-textSecondary.mt-1:not([data-translated])');
        for (const p of quoteTweetPs) {
            await processInlineTweet(p);
        }
    }

    // ✅ 4. 翻译用户简介(保留滚动功能)
    async function processUserBio(container) {
        const bioElement = container.querySelector("p.break-words, span.break-words");
        if (!bioElement || bioElement.getAttribute("data-translated")) return;

        const text = bioElement.innerText.trim();
        if (!text) return;

        const translated = await translateToChinese(text);

        const translatedP = document.createElement("p");
        translatedP.style.color = "rgb(154, 167, 176)";
        translatedP.style.fontSize = "13px";
        translatedP.style.marginTop = "4px";
        translatedP.style.whiteSpace = "pre-wrap";
        translatedP.textContent = "🈯️ " + translated;

        bioElement.parentElement.appendChild(translatedP);
        bioElement.setAttribute("data-translated", 'true');

        const wrapper = bioElement.closest("div[style], div.relative");
        if (wrapper) {
            const gradient = wrapper.querySelector("div[class*='bg-gradient-to-b']");
            if (gradient) gradient.style.display = "none";
        }
    }

    // ✅ 统一 MutationObserver(添加推文监控支持)
    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType !== 1 || !node.querySelector) continue;

                if (node.classList && node.classList.contains('fixed') && node.classList.contains('z-[9999]')) {
                    setTimeout(() => processPreviewPopup(node), 100);
                }

                const popups = node.querySelectorAll('div.fixed.z-\\[9999\\].pointer-events-auto');
                popups.forEach(popup => {
                    setTimeout(() => processPreviewPopup(popup), 100);
                });

                if (node.classList && (node.classList.contains('cursor-pointer') || node.querySelector('p.text-textSecondary.mt-1'))) {
                    processTwitterAlert(node);
                }

                const alertElements = node.querySelectorAll('div[role="button"].cursor-pointer');
                alertElements.forEach(processTwitterAlert);

                node.querySelectorAll("article.tweet-container_article__0ERPK").forEach(processPreviewTweet);

                const traditionalTweets = node.querySelectorAll("p.text-textSecondary.mt-1:not(.whitespace-pre-wrap)");
                traditionalTweets.forEach(p => {
                    if (!p.closest('div[role="button"].cursor-pointer')) {
                        processInlineTweet(p);
                    }
                });

                const bioElement = node.querySelector("p.break-words, span.break-words");
                if (bioElement) processUserBio(node);
            }
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // ✅ 初始扫描(添加推文监控支持)
    function init() {
        document.querySelectorAll('div.fixed.z-\\[9999\\].pointer-events-auto').forEach(processPreviewPopup);
        document.querySelectorAll('div[role="button"].cursor-pointer').forEach(processTwitterAlert);
        document.querySelectorAll("article.tweet-container_article__0ERPK").forEach(processPreviewTweet);
        document.querySelectorAll("p.text-textSecondary.mt-1:not(.whitespace-pre-wrap)").forEach(p => {
            if (!p.closest('div[role="button"].cursor-pointer')) {
                processInlineTweet(p);
            }
        });
        document.querySelectorAll("p.break-words, span.break-words").forEach(element => {
            const container = element.closest("div");
            if (container) processUserBio(container);
        });
    }

    setTimeout(init, 1000); // 延迟初始加载
})();