Greasy Fork

Greasy Fork is available in English.

YouTube 中英双语字幕

支持通过菜单配置中文字体大小

当前为 2026-02-26 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube 中英双语字幕
// @version      3.2
// @author       4Aiur
// @namespace    http://greasyfork.icu/zh-CN/users/394849-4aiur
// @description  支持通过菜单配置中文字体大小
// @match        *://www.youtube.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_openInTab
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const TARGET_LANG = 'zh-CN';
    const translationCache = new Map();
    const timers = new Map();

    // -------------------- 0. Trusted Types 兼容处理 --------------------
    let ttPolicy;
    if (window.trustedTypes && window.trustedTypes.createPolicy) {
        ttPolicy = window.trustedTypes.createPolicy('youtube-dual-subtitles-policy', {
            createHTML: (string) => string
        });
    }

    function setSafeContent(el, content) {
        if (!el) return;
        el.innerText = content;
    }

    // -------------------- 1. 配置管理 & 菜单 --------------------
    let fontSize = GM_getValue('subtitle_font_size', 1.5); // 默认 1.5 比较适中

    function registerMenu() {
        GM_registerMenuCommand(`⚙️ 设置中文字体大小 (当前: ${fontSize}em)`, () => {
            let newSize = prompt("请输入字体大小 (建议 1.1 - 2.0):", fontSize);
            if (newSize !== null) {
                newSize = parseFloat(newSize);
                if (!isNaN(newSize) && newSize > 0) {
                    fontSize = newSize;
                    GM_setValue('subtitle_font_size', newSize);
                    updateStyle();
                    location.reload();
                }
            }
        });
        GM_registerMenuCommand('💬 反馈 & 建议 (联系作者)', () => {
            GM_openInTab('http://greasyfork.icu/zh-CN/scripts/567512-youtube-%E4%B8%AD%E8%8B%B1%E5%8F%8C%E8%AF%AD%E5%AD%97%E5%B9%95/feedback', { active: true });
        });
    }

    // -------------------- 2. 注入 CSS (优化布局逻辑) --------------------
    function updateStyle() {
        const oldStyle = document.getElementById('my-subtitle-style');
        if (oldStyle) oldStyle.remove();

        const cssText = `
            /* 强制容器垂直排列 */
            .ytp-caption-window-container .caption-visual-line {
                display: flex !important;
                flex-direction: column !important;
                align-items: center !important;
                justify-content: flex-end !important;
                background: transparent !important; /* 去掉容器背景防止遮挡 */
            }

            /* 英文字幕样式微调 */
            .ytp-caption-segment {
                display: inline-block !important;
                white-space: pre-wrap !important;
            }

            /* 中文字幕样式 (显示在上方) */
            .my-trans-line {
                display: block !important; /* 强制独占一行 */
                color: #FFCC00 !important;
                font-size: ${fontSize}em !important;
                font-weight: bold !important;
                line-height: 1.2 !important;
                margin-top: 4px !important; /* 在中英文之间留出空隙 */
                padding: 2px 8px !important;
                background: rgba(0, 0, 0, 0.7) !important;
                border-radius: 4px !important;
                text-shadow: 2px 2px 3px rgba(0,0,0,1) !important;
                text-align: center !important;
                order: -1 !important; /* 确保在 flex 容器中排在最前面(最上方) */
            }
        `;

        const style = document.createElement('style');
        style.id = 'my-subtitle-style';
        if (ttPolicy && style.hasOwnProperty('innerHTML')) {
            style.innerHTML = ttPolicy.createHTML(cssText);
        } else {
            style.textContent = cssText;
        }
        document.head.appendChild(style);
    }

    // -------------------- 3. 翻译引擎 --------------------
    async function translate(text) {
        if (!text || text.length < 2) return null;
        if (translationCache.has(text)) return translationCache.get(text);

        try {
            const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${TARGET_LANG}&dt=t&q=${encodeURIComponent(text)}`;
            const res = await fetch(url);
            const data = await res.json();
            if (data && data[0]) {
                const result = data[0].map(x => x[0]).join('');
                translationCache.set(text, result);
                return result;
            }
        } catch (e) {
            console.error('Translation Error:', e);
        }
        return null;
    }

    // -------------------- 4. 处理逻辑 --------------------
    function processLine(line) {
        const segments = line.querySelectorAll('.ytp-caption-segment');
        if (!segments.length) return;

        const fullText = Array.from(segments)
            .map(s => s.innerText)
            .join(' ')
            .replace(/\s+/g, ' ')
            .trim();

        if (!fullText) return;

        let transDiv = line.querySelector('.my-trans-line');

        // 如果已有翻译且文本未变,直接确保位置正确
        if (translationCache.has(fullText)) {
            const cachedText = translationCache.get(fullText);
            if (!transDiv) {
                transDiv = document.createElement('div');
                transDiv.className = 'my-trans-line';
                line.prepend(transDiv); // 使用 prepend 插入到最上方
            }
            if (transDiv.innerText !== cachedText) setSafeContent(transDiv, cachedText);
            return;
        }

        // 防抖处理
        clearTimeout(timers.get(line));
        const timer = setTimeout(async () => {
            if (!line.isConnected) return;
            const result = await translate(fullText);
            if (result && line.isConnected) {
                let currentTransDiv = line.querySelector('.my-trans-line');
                if (!currentTransDiv) {
                    currentTransDiv = document.createElement('div');
                    currentTransDiv.className = 'my-trans-line';
                    line.prepend(currentTransDiv); // 翻译后也插入到最上方
                }
                setSafeContent(currentTransDiv, result);
            }
        }, 200);
        timers.set(line, timer);
    }

    // -------------------- 5. 初始化 --------------------
    const observer = new MutationObserver(() => {
        const lines = document.querySelectorAll('.caption-visual-line');
        lines.forEach(processLine);
    });

    function init() {
        const container = document.querySelector('.ytp-caption-window-container');
        if (container) {
            observer.observe(container, { childList: true, subtree: true });
        } else {
            setTimeout(init, 1000);
        }
    }

    registerMenu();
    updateStyle();
    init();

    window.addEventListener('yt-navigate-finish', () => {
        timers.clear();
        init();
    });
})();