Greasy Fork

Greasy Fork is available in English.

外来语翻译显示

在网页中的外来语上方显示中文翻译

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         外来语翻译显示
// @namespace    http://tampermonkey.net/
// @license     MIT
// @version      1.0
// @description  在网页中的外来语上方显示中文翻译
// @author       Your Name
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

var _ = document;
var queue = {};  // {"外来语": [rtNodeA, rtNodeB]}
var cachedTranslations = {};  // {"Terminator": "终结者"}
var newNodes = [_.body];

// 递归遍历给定节点及其子节点(深度优先搜索)
function scanTextNodes(node) {
    if (!node.parentNode || !_.body.contains(node)) {
        return;
    }
    var excludeTags = {ruby: true, script: true, select: true, textarea: true};
    switch (node.nodeType) {
        case Node.ELEMENT_NODE:
            if (node.tagName.toLowerCase() in excludeTags || node.isContentEditable) {
                return;
            }
            return node.childNodes.forEach(scanTextNodes);
        case Node.TEXT_NODE:
            while ((node = addRuby(node)));
    }
}

// 递归为文本节点添加 ruby 标签
function addRuby(node) {
    var foreignWordPattern = /\b([A-Za-z]+)\b/g; // 匹配英文单词
    var match;
    if (!node.nodeValue || !(match = foreignWordPattern.exec(node.nodeValue))) {
        return false;
    }
    var ruby = _.createElement('ruby');
    ruby.appendChild(_.createTextNode(match[0]));
    var rt = _.createElement('rt');
    rt.classList.add('foreign-word-rt');
    ruby.appendChild(rt);

    // 将待翻译的外来语加入队列
    queue[match[0]] = queue[match[0]] || [];
    queue[match[0]].push(rt);

    var after = node.splitText(match.index);
    node.parentNode.insertBefore(ruby, after);
    after.nodeValue = after.nodeValue.substring(match[0].length);
    return after;
}

// 翻译文本节点
function translateTextNodes() {
    var apiRequestCount = 0;
    var phraseCount = 0;
    var chunkSize = 200;
    var chunk = [];
    for (var phrase in queue) {
        phraseCount++;
        if (phrase in cachedTranslations) {
            updateRubyByCachedTranslations(phrase);
            continue;
        }
        chunk.push(phrase);
        if (chunk.length >= chunkSize) {
            apiRequestCount++;
            googleTranslate('en', 'zh-CN', chunk);
            chunk = [];
        }
    }
    if (chunk.length) {
        apiRequestCount++;
        googleTranslate('en', 'zh-CN', chunk);
    }
    if (phraseCount) {
        console.debug('外来语翻译:', phraseCount, '个短语在', apiRequestCount, '个请求中翻译,页面', window.location.href);
    }
}

// 构建查询字符串
function buildQueryString(params) {
    return '?' + Object.keys(params).map(function(k) {
        return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
    }).join('&');
}

// Google翻译API
function googleTranslate(srcLang, destLang, phrases) {
    phrases.forEach(function(phrase) {
        cachedTranslations[phrase] = null;
    });
    var joinedText = phrases.join('\n').replace(/\s+$/, ''),
        api = 'https://translate.googleapis.com/translate_a/single',
        params = {
            client: 'gtx',
            dt: 't',
            sl: srcLang,
            tl: destLang,
            q: joinedText,
        };
    GM_xmlhttpRequest({
        method: "GET",
        url: api + buildQueryString(params),
        onload: function(dom) {
            try {
                var resp = JSON.parse(dom.responseText.replace("'", '\u2019'));
            } catch (err) {
                console.error('外来语翻译: 无效的响应', dom.responseText);
                return;
            }
            resp[0].forEach(function(item) {
                var translated = item[0].replace(/\s+$/, ''),
                    original   = item[1].replace(/\s+$/, '');
                cachedTranslations[original] = translated;
                updateRubyByCachedTranslations(original);
            });
        },
        onerror: function(dom) {
            console.error('外来语翻译: 请求错误', dom.statusText);
        },
    });
}

// 更新ruby标签
function updateRubyByCachedTranslations(phrase) {
    if (!cachedTranslations[phrase]) {
        return;
    }
    (queue[phrase] || []).forEach(function(node) {
        node.dataset.rt = cachedTranslations[phrase];
    });
    delete queue[phrase];
}

// 监视新添加的DOM节点
function mutationHandler(mutationList) {
    mutationList.forEach(function(mutationRecord) {
        mutationRecord.addedNodes.forEach(function(node) {
            newNodes.push(node);
        });
    });
}

function main() {
    GM_addStyle("rt.foreign-word-rt::before { content: attr(data-rt); }");
    var observer = new MutationObserver(mutationHandler);
    observer.observe(_.body, {childList: true, subtree: true});

    function rescanTextNodes() {
        mutationHandler(observer.takeRecords());
        if (!newNodes.length) {
            return;
        }
        console.debug('外来语翻译:', newNodes.length, '个新节点被添加,页面', window.location.href);
        newNodes.forEach(scanTextNodes);
        newNodes.length = 0;
        translateTextNodes();
    }

    rescanTextNodes();
    setInterval(rescanTextNodes, 500);
}

if (typeof GM_xmlhttpRequest === 'undefined' &&
    typeof GM === 'object' && typeof GM.xmlHttpRequest === 'function') {
    GM_xmlhttpRequest = GM.xmlHttpRequest;
}
if (typeof GM_addStyle === 'undefined') {
    GM_addStyle = function(css) {
        var head = _.getElementsByTagName('head')[0];
        if (!head) {
            return null;
        }
        var style = _.createElement('style');
        style.setAttribute('type', 'text/css');
        style.textContent = css;
        head.appendChild(style);
        return style;
    };
}
if (typeof NodeList.prototype.forEach === 'undefined') {
    NodeList.prototype.forEach = function(callback, thisArg) {
        thisArg = thisArg || window;
        for (var i = 0; i < this.length; i++) {
            callback.call(thisArg, this[i], i, this);
        }
    };
}
main();