Greasy Fork is available in English.
通过菜单配置中文字体大小和Debug 日志开关
当前为
// ==UserScript==
// @name YouTube 双语字幕
// @version 3.7
// @author 4Aiur
// @namespace http://greasyfork.icu/zh-CN/users/394849-4aiur
// @description 通过菜单配置中文字体大小和Debug 日志开关
// @match *://www.youtube.com/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// @run-at document-idle
// @icon https://www.youtube.com/s/desktop/b9bfb983/img/favicon_32x32.png
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const TARGET_LANG = 'zh-CN';
const translationCache = new Map();
const timers = new Map();
// -------------------- 0. 中文与相似度检测函数 --------------------
function isPureChinese(text){
const cleaned=text.replace(/[\s\d\p{P}]/gu,'');
if(!cleaned) return false;
const chineseChars=cleaned.match(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/g)||[];
return (chineseChars.length/cleaned.length)>0.8;
}
function isTooSimilar(s1,s2){
const clean=s=>s.replace(/[\s\p{P}]/gu,'').toLowerCase();
const c1=clean(s1),c2=clean(s2);
return c1===c2||c1.includes(c2)||c2.includes(c1);
}
// -------------------- 1. 配置 & 菜单 --------------------
let fontSize = GM_getValue('subtitle_font_size', 1.5);
let isDebug = GM_getValue('debug_mode', false); // 默认关闭日志
// 封装日志输出,增加 debug 判断
const log = {
info: (msg, ...args) => {
if (isDebug) console.log(`%c[YouTube双语字幕]%c ${msg}`, 'color: #00bcd4; font-weight: bold', '', ...args);
},
warn: (msg, ...args) => {
if (isDebug) console.warn(`[YouTube双语字幕] ${msg}`, ...args);
},
error: (msg, ...args) => {
// Error 级别日志通常建议始终保留,或根据需求也加上 isDebug 判断
console.error(`[YouTube双语字幕] ${msg}`, ...args);
}
};
log.info('[YouTube双语字幕] 脚本已执行', location.href);
function registerMenu() {
// 清除之前的菜单(某些脚本管理器支持,若不支持则需刷新页面)
// 1. 设置字体大小
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();
}
}
}
);
// 2. Debug 日志开关
GM_registerMenuCommand(
isDebug ? '🚫 关闭调试日志 (当前: 开启)' : '🐞 开启调试日志 (当前: 关闭)',
() => {
isDebug = !isDebug;
GM_setValue('debug_mode', isDebug);
// 为了让逻辑重新加载,建议刷新页面
location.reload();
}
);
// 3. 反馈建议
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. Trusted Types 兼容 --------------------
let ttPolicy;
if (window.trustedTypes && window.trustedTypes.createPolicy) {
ttPolicy = window.trustedTypes.createPolicy(
'youtube-dual-subtitles-policy',
{ createHTML: s => s }
);
}
function setSafeContent(el, content) {
if (!el) return;
el.innerText = content;
}
// -------------------- 3. CSS (强力居中修正版) --------------------
function updateStyle() {
document.getElementById('my-subtitle-style')?.remove();
const css = `
/* 1. 强制最外层字幕窗口撑满播放器宽度,并取消 YouTube 的默认左偏定位 */
.ytp-caption-window-container .ytp-caption-window-bottom {
width: 100% !important;
left: 0 !important;
margin-left: 0 !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
}
/* 2. 强制每一行字幕容器居中 */
.ytp-caption-window-container .caption-visual-line {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
left: 0 !important;
position: relative !important;
background: transparent !important;
text-align: center !important;
}
/* 3. 原始字幕片段也强制居中 */
.ytp-caption-segment {
display: inline-block !important;
white-space: pre-wrap !important;
text-align: center !important;
position: relative !important;
}
/* 4. 我们的翻译行居中 */
.my-trans-line {
display: block !important; /* 改回 block */
width: fit-content !important; /* 宽度随内容 */
margin: 4px auto !important; /* 靠 auto 实现块级居中 */
order: -1 !important;
color: #FFCC00 !important;
font-size: ${fontSize}em !important;
font-weight: bold !important;
line-height: 1.2 !important;
min-height: 1.2em !important;
white-space: nowrap !important; /* 保持你的不换行需求 */
max-width: 95vw !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;
}
`;
const style = document.createElement('style');
style.id = 'my-subtitle-style';
style.innerHTML = ttPolicy ? ttPolicy.createHTML(css) : css;
document.head.appendChild(style);
log.info('样式已加载/更新');
}
// -------------------- 4. 翻译 --------------------
async function translate(text) {
if (!text || text.length < 2) return null;
if (translationCache.has(text)) {
log.info(`命中缓存: "${text.substring(0, 15)}..."`);
return translationCache.get(text);
}
try {
log.info(`发起请求: "${text.substring(0, 15)}..."`);
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?.[0]) {
const result = data[0].map(x => x[0]).join('');
translationCache.set(text, result);
return result;
}
} catch (e) {
log.error('翻译异常:', e);
}
return null;
}
// -------------------- 5. 核心逻辑 --------------------
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;
// ✅ 新增检测:如果原生字幕已经是中文,直接清理翻译层并中止执行
if (isPureChinese(fullText)) {
const existingTransDiv = line.querySelector('.my-trans-line');
if (existingTransDiv) {
existingTransDiv.remove();
}
return;
}
let transDiv = line.querySelector('.my-trans-line');
if (!transDiv) {
transDiv = document.createElement('div');
transDiv.className = 'my-trans-line';
transDiv.innerText = '';
line.prepend(transDiv);
log.info('DOM: 创建占位行');
}
if (translationCache.has(fullText)) {
const cached = translationCache.get(fullText);
// ✅ 新增检测:如果缓存结果和原文太相似 (已被YouTube翻译为中文)
if (isTooSimilar(cached, fullText)) {
transDiv.innerText = ''; // 清空内容,不显示翻译层
return;
}
if (transDiv.innerText !== cached) {
setSafeContent(transDiv, cached);
}
return;
}
clearTimeout(timers.get(line));
const timer = setTimeout(async () => {
if (!line.isConnected) return;
const result = await translate(fullText);
if (result && line.isConnected) {
// ✅ 新增检测:如果翻译结果和原文太相似 (已被YouTube翻译为中文)
if (isTooSimilar(result, fullText)) {
transDiv.innerText = ''; // 清空内容,不显示翻译层
return;
}
setSafeContent(transDiv, result);
}
}, 200);
timers.set(line, timer);
}
// -------------------- 6. 初始化 --------------------
const observer = new MutationObserver(() => {
document.querySelectorAll('.caption-visual-line').forEach(processLine);
});
let observerStarted = false;
let initRetry = 0;
function init() {
const container = document.querySelector('.ytp-caption-window-container');
if (!container) {
if (initRetry++ < 20) {
setTimeout(init, 500);
} else {
log.warn('字幕容器未找到,停止重试');
}
return;
}
initRetry = 0;
if (!observerStarted) {
log.info('初始化: 字幕容器已就绪');
observer.observe(container, { childList: true, subtree: true });
observerStarted = true;
}
}
registerMenu();
updateStyle();
init();
['yt-navigate-finish', 'yt-page-data-updated'].forEach(evt => {
window.addEventListener(evt, () => {
log.info(`YouTube 事件: ${evt}`);
observer.disconnect();
observerStarted = false;
timers.clear();
init();
});
});
})();