Greasy Fork

Greasy Fork is available in English.

YouTube 直播聊天实时翻译

Same as the name

当前为 2024-12-12 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        YouTube 直播聊天实时翻译
// @version     1.1
// @author      lslqtz
// @license     GPL
// @grant       none
// @inject-into page
// @run-at      document-end
// @match       *://*.youtube.com/live_chat*
// @namespace http://greasyfork.icu/users/155581
// @description Same as the name
// ==/UserScript==

// 启动翻译脚本.
function startYouTubeLiveChatTranslator() {
	console.log("启动 YouTube 直播聊天翻译脚本");

	if (window.ytLiveChatInterval) {
		clearInterval(window.ytLiveChatInterval);
		console.log("已清除之前的计时器");
	}
	if (window.ytObserver) {
		window.ytObserver.disconnect();
	}

	// 启动定时器.
	window.ytLiveChatInterval = setInterval(checkAndObserveChatContainer, 1000);
}

// Google Translate API 调用.
async function translateText(text, targetLang = 'zh-CN') {
	try {
		console.log("翻译消息: " + text);
		var response = await fetch('https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=' + targetLang + '&dt=t&q=' + encodeURIComponent(text));
		if (!response.ok) {
			console.error("翻译 API 请求失败", response.statusText);
			return "[翻译失败]";
		}
		var result = await response.json();
		return result[0][0][0];
	} catch (error) {
		console.error("翻译出错", error);
		return "[翻译错误]";
	}
}

// 解析消息内容, 过滤表情和图片, 并将它们替换为占位符.
function extractTextContent(element) {
	var text = '';
	var elements = element.childNodes;
	var placeholders = []; // 存储占位符与原始内容的对应关系.

	elements.forEach(function (node, index) {
		if (node.nodeType === Node.TEXT_NODE) {
			text += node.nodeValue.trim();
		} else if (node.nodeType === Node.ELEMENT_NODE) {
			if (node.tagName.toLowerCase() === 'img' || (node.tagName.toLowerCase() === 'span' && node.classList.contains('emoji'))) {
				var placeholder = `{{emoji_placeholder_${index}}}`; // 使用占位符.
				placeholders.push({ placeholder: placeholder, html: node.outerHTML });
				text += placeholder; // 将表情或图片替换为占位符.
			}
		}
	});

	return { text: text.trim(), placeholders: placeholders };
}

// 检查并观察聊天容器.
function checkAndObserveChatContainer() {
	var chatContainer = document.querySelector('#live-chat-item-list-panel');
	if (chatContainer) {
		console.log("聊天容器找到: ", chatContainer);

		if (window.ytObserver) {
		    window.ytObserver.disconnect();
                    console.log("解除之前监听聊天更新");
	        }

		observeChatUpdates(chatContainer);

                translateInitialMessages(chatContainer);
	}
}

// 翻译已有的最新 20 条消息.
function translateInitialMessages(chatContainer) {
    console.log("开始翻译已有消息...");

    // 获取所有聊天消息节点.
    var messages = chatContainer.querySelectorAll('yt-live-chat-text-message-renderer, yt-live-chat-paid-message-renderer');
    var totalMessages = messages.length;

    // 只处理最后的 20 条消息.
    for (var i = Math.max(0, totalMessages - 20); i < totalMessages; i++) {
        var messageNode = messages[i];
        var messageElement = messageNode.querySelector('#message');
        if (messageElement) {
            var { text, placeholders } = extractTextContent(messageElement);
            if (!text) {
                console.log("已有消息内容为空,跳过翻译");
                continue;
            }
            console.log("已有消息: " + text);
            var translatedMessage = translateText(text);
            var finalMessage = insertPlaceholdersIntoTranslation(translatedMessage, placeholders);
            insertTranslatedMessage(messageElement, finalMessage);
        }
    }
}

// 监听聊天消息更新.
function observeChatUpdates(chatContainer) {
	window.ytObserver = new MutationObserver(async function (mutations) {
		for (var i = 0; i < mutations.length; i++) {
			var mutation = mutations[i];
			mutation.addedNodes.forEach(async function (node) {
				// 检查是否为聊天消息.
				if (node.nodeType === 1 && (node.tagName.toLowerCase() === 'yt-live-chat-text-message-renderer' || node.tagName.toLowerCase() === 'yt-live-chat-paid-message-renderer')) {
					var messageElement = node.querySelector('#message');
					if (messageElement) {
						// 提取文本并替换表情或图片为占位符.
						var { text, placeholders } = extractTextContent(messageElement);
						if (!text) {
							console.log("消息内容为空, 跳过翻译");
							return;
						}
						console.log("检测到新消息: " + text);
						var translatedMessage = await translateText(text);

						// 将翻译后的文本和表情或图片组合.
						var finalMessage = insertPlaceholdersIntoTranslation(translatedMessage, placeholders);
						insertTranslatedMessage(messageElement, finalMessage);
					} else {
						console.warn("未找到消息内容元素");
					}
				}
			});
		}
	});

	console.log("开始监听聊天更新...");
	window.ytObserver.observe(chatContainer, { childList: true, subtree: true });
}

// 重新将占位符替换为表情和图片.
function insertPlaceholdersIntoTranslation(translatedMessage, placeholders) {
	placeholders.forEach(function (placeholder) {
		translatedMessage = translatedMessage.replace(placeholder.placeholder, placeholder.html);
	});
	return translatedMessage;
}

// 插入翻译后的消息.
function insertTranslatedMessage(messageElement, translatedMessage) {
	var translationElement = document.createElement('div');
	translationElement.style.color = 'gray';
	translationElement.style.fontSize = 'small';
	translationElement.innerHTML = "[翻译]: " + translatedMessage;

	// 在原消息下方插入翻译内容.
	messageElement.appendChild(translationElement);
}

startYouTubeLiveChatTranslator();