Skip to content

Instantly share code, notes, and snippets.

@aoaim
Created October 3, 2025 11:00
Show Gist options
  • Select an option

  • Save aoaim/9f2673e31238f2a547aac1444939be92 to your computer and use it in GitHub Desktop.

Select an option

Save aoaim/9f2673e31238f2a547aac1444939be92 to your computer and use it in GitHub Desktop.
文献标题翻译 | Academic Paper Title Translator
// ==UserScript==
// @name 文献标题翻译 | Academic Paper Title Translator
// @namespace http://tampermonkey.net/
// @version 1.0.1
// @description 自动翻译 PubMed、Europe PMC、Semantic Scholar、Google Scholar 等学术网站上的论文标题和摘要为中文 | Auto-translate academic paper titles and abstracts to Chinese
// @description:zh-CN 自动翻译学术网站上的论文标题和摘要为中文,支持多种翻译 API,智能并发控制
// @description:en Auto-translate academic paper titles and abstracts to Chinese with multiple translation APIs and intelligent concurrency control
// @author Miao & Claude Sonnet 4.5
// @homepage https://github.com/yourusername/academic-translator
// @supportURL https://github.com/yourusername/academic-translator/issues
// @license MIT
// @match https://pubmed.ncbi.nlm.nih.gov/*
// @match https://europepmc.org/*
// @match https://www.semanticscholar.org/*
// @match https://scholar.google.com/*
// @match https://scholar.google.co.*/*
// @grant GM_xmlhttpRequest
// @connect translate.googleapis.com
// @connect edge.microsoft.com
// @connect api.cognitive.microsofttranslator.com
// @connect api-free.deepl.com
// @connect api.deepl.com
// @connect api.niutrans.com
// @connect *
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
/**
* ╔══════════════════════════════════════════════════════════════════════════════╗
* ║ 文献标题翻译 v1.0 ║
* ║ Academic Paper Title Translator for Tampermonkey ║
* ║ Build by Miao and Claude Sonnet 4.5 with ♥ ║
* ╚══════════════════════════════════════════════════════════════════════════════╝
*
* ╔══════════════════════════════════════════════════════════════════════════════╗
* ║ 使用说明(新手必读) ║
* ╚══════════════════════════════════════════════════════════════════════════════╝
*
* 【快速开始 - 零基础也能用】
*
* [1] 第一步:安装 Tampermonkey 浏览器插件
* - Chrome/Edge: 在扩展商店搜索 "Violentmonkey" 并安装
* - Firefox: 在附加组件商店搜索 "Violentmonkey" 并安装
* - Safari: 在 App Store 搜索 "Userscripts" 或者 "Tampermonkey" 并安装
*
* [2] 第二步:安装本脚本
* - 点击 Violentmonkey 图标 → + Creat a new script → 粘贴代码 → Save & Close
* - 点击 Userscripts 图标 → Open Extension Page → + New JS → 粘贴代码 → Save
* - 或者:新建脚本,复制粘贴本脚本全部内容,按 Ctrl+S 保存
*
* [3] 第三步:开始使用(无需配置!)
* - 打开 PubMed、Google Scholar 等学术网站
* - 脚本会自动翻译论文标题为中文
* - 翻译结果显示在原标题下方,斜体灰色
*
* 【默认配置 - 开箱即用】
*
* + 使用微软免费翻译(无需注册,无需 API Key)
* + 自动翻译所有论文标题
* + 不翻译摘要(避免干扰阅读)
* + 智能跳过中文内容
*
* 【进阶配置(可选)】
*
* 如果你想自定义配置,请往下滚动到 "配置区域",修改 CONFIG 对象:
*
* [*] 修改翻译服务商:
* titleProvider: 'microsoft' // 改成 'google' 或 'deepl' 或 'openai'
*
* [*] 启用摘要翻译:
* translateAbstract: true // 改成 true 启用,false 关闭(默认)
*
* [*] 使用大模型翻译(需要 API Key):
* 第 1 步:申请 API Key(任选其一):
* - OpenAI: https://platform.openai.com/api-keys
* - DeepSeek: https://platform.deepseek.com/ (推荐,国内便宜)
* - Gemini: https://makersuite.google.com/app/apikey
*
* 第 2 步:填入配置(找到下面的 openai 配置项):
* openai: {
* apiKey: '这里填入你的 API Key', // 把引号里的内容替换成你的 Key
* model: 'gpt-5-mini', // 根据服务商选择模型
* baseURL: 'https://api.openai.com/v1' // 根据服务商修改地址
* }
*
* 第 3 步:修改翻译提供商:
* titleProvider: 'openai' // 改成 'openai'
*
* 【常见问题】
*
* Q: 为什么没有翻译?
* A: 1) 检查 Tampermonkey 是否启用
* 2) 检查脚本是否启用(Tampermonkey 图标显示数字)
* 3) 刷新页面重试
*
* Q: 可以同时翻译多个网站吗?
* A: 可以!脚本自动识别 PubMed、Europe PMC、Semantic Scholar、Google Scholar
*
* Q: 翻译速度慢怎么办?
* A: 1) 免费 API 有速率限制,这是正常的
* 2) 可以修改 maxConcurrent(并发数)和 requestDelay(延迟)
* 3) 建议:免费 API 使用 maxConcurrent: 1, requestDelay: 500
*
* Q: 可以翻译成其他语言吗?
* A: 目前只支持翻译成中文,如需其他语言请修改翻译 API 参数
*
* Q: 如何关闭脚本?
* A: 点击 Tampermonkey 图标,找到本脚本,点击开关即可
*
* 【详细功能说明】
*
* 本脚本自动翻译学术网站上的论文标题为中文,支持以下网站和页面:
*
* 【PubMed】
* 1. 主页 - Trending Articles 论文标题
* 2. 搜索结果页 - 论文标题列表
* 3. 文章详情页 - 主标题
* 4. 文章详情页 - Similar articles 论文标题(动态加载)
* 5. 文章详情页 - Cited by 论文标题(动态加载)
*
* 【Europe PMC】
* 1. 文章详情页 - 主标题
* 2. 搜索结果页 - 论文标题列表
* 3. 文章详情页 - Similar articles 论文标题
*
* 【Semantic Scholar】
* 1. 搜索结果页 - 论文标题列表
* 2. 文章详情页 - 主标题
* 3. 文章详情页 - Citations 论文标题
* 4. 文章详情页 - References 论文标题
* 5. 个人主页 - 论文标题列表
*
* 【Google Scholar】
* 1. 搜索结果页 - 论文标题列表
*
* 【摘要翻译】可选功能
* - PubMed、Europe PMC 文章详情页的摘要
* - 默认关闭,需要在 CONFIG 中设置 translateAbstract: true 启用
* - 翻译结果显示在摘要下方,带有独立的样式区块
* - 可单独配置摘要翻译提供商(推荐使用大模型 API)
*
* 【翻译样式】
* - 标题翻译:斜体显示(font-style: italic)、透明度 60%(opacity: 0.6)
* - 摘要翻译:独立区块显示,带有浅色背景和左侧边框
* - 继承父元素颜色
* - 位于原文下方,自动换行
*
* 【智能特性】
* - 自动检测语言,跳过中文内容
* - 防止重复翻译
* - 支持动态加载内容(MutationObserver)
* - 多层防护机制确保翻译质量
*/
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 配置区域(从这里开始修改) ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
//
// [!] 提示:如果你不懂代码,只需要:
// 1. 保持默认设置,无需修改任何内容
// 2. 如果想启用摘要翻译,把 translateAbstract 改成 true
// 3. 如果想用大模型,填写 apiKey 并修改 titleProvider
//
const CONFIG = {
// ========== 翻译提供商设置 ==========
// 【标题翻译提供商】可选值:
// 免费:'google', 'microsoft'
// 付费:'deepl', 'niutrans'
// 大模型:'openai', 'deepseek', 'gemini', 'siliconflow', 'groq'
// 自定义:'openai-compatible'
//
// > 新手推荐:'microsoft'(微软免费翻译,无需配置)
titleProvider: 'microsoft',
// 【摘要翻译开关】是否翻译论文摘要
// [说明]
// - false:只翻译标题(推荐,不影响阅读)
// - true:同时翻译标题和摘要(翻译较慢,消耗 API 额度)
translateAbstract: false,
// 【摘要翻译提供商】可选值同上
//
// > 留空 '' 表示和标题使用相同的提供商
// > 如果想让摘要用大模型,可以改成:'deepseek' 等(记得填 API Key)
abstractProvider: '', // 留空则使用 titleProvider
// ========== 免费翻译 API 配置 ==========
// 【Google 翻译】免费,无需任何配置
// [使用方法] 把上面的 titleProvider 改成 'google'
google: {
// + 完全免费,无需注册
// + 翻译质量:****
// + 速度:快
},
// 【Microsoft 翻译】免费,无需任何配置(默认)
// [使用方法] 默认已启用,无需修改
microsoft: {
// + 完全免费,无需注册
// + 翻译质量:****
// + 速度:快
// + 稳定性:高
},
// 【DeepL 翻译】需要注册,需要 API Key
// [使用方法]
// 第 1 步:访问 https://www.deepl.com/pro-api 注册账号
// 第 2 步:复制你的 API Key
// 第 3 步:把 Key 粘贴到下面 apiKey 的引号里
// 第 4 步:把上面的 titleProvider 改成 'deepl'
deepl: {
apiKey: '', // <-- 把你的 DeepL API Key 粘贴到这里(替换两个引号之间的内容)
useFreeApi: true, // true = 免费版(每月 50 万字符),false = 付费版
// + 翻译质量:*****(最高)
// + 专业术语准确
},
// 【小牛翻译】需要注册,需要 API Key
// [使用方法]
// 第 1 步:访问 https://niutrans.com/documents/contents/trans_text 注册账号
// 第 2 步:复制你的 API Key
// 第 3 步:把 Key 粘贴到下面 apiKey 的引号里
// 第 4 步:把上面的 titleProvider 改成 'niutrans'
niutrans: {
apiKey: '', // <-- 把你的小牛翻译 API Key 粘贴到这里
// + 每日 100 积分
// + 文本翻译:1 积分/2000 字符
},
// ========== 大模型翻译 API 配置 ==========
// 【OpenAI】官方服务
// 注册地址:https://platform.openai.com/api-keys
openai: {
apiKey: '',
model: 'gpt-4.5-mini',
temperature: 0.3,
},
// 【DeepSeek】国内推荐,便宜好用
// 注册地址:https://platform.deepseek.com/
deepseek: {
apiKey: '',
model: 'deepseek-chat',
temperature: 0.3,
},
// 【Google Gemini】速度快,免费额度大
// 注册地址:https://makersuite.google.com/app/apikey
gemini: {
apiKey: '',
model: 'gemini-2.5-flash',
temperature: 0.1,
},
// 【硅基流动】支持多种开源模型
// 注册地址:https://siliconflow.cn/
siliconflow: {
apiKey: '',
model: 'deepseek-ai/DeepSeek-V3',
temperature: 0.3,
},
// 【Groq】超快推理速度
// 注册地址:https://console.groq.com/
groq: {
apiKey: '',
model: 'openai/gpt-oss-120b',
temperature: 0.3,
},
// 【OpenAI 兼容接口】自定义配置
// 适用于任何支持 OpenAI API 格式的服务
'openai-compatible': {
apiKey: '',
model: '',
baseURL: '', // 例如:https://api.example.com/v1
temperature: 0.3,
},
// ========== 并发和速率控制 ==========
// 【并发数量】同时进行的翻译请求数
// [说明] 数字越大,翻译越快,但可能被限速
//
// > 新手推荐设置:
// - 如果用免费 API(Google/Microsoft):设为 1
// - 如果用大模型 API:设为 3-5
maxConcurrent: 1,
// 【请求延迟】翻译请求之间的等待时间(毫秒)
// [说明] 数字越大,翻译越慢,但不容易被限速
//
// > 新手推荐设置:
// - 如果用免费 API:设为 500(半秒)
// - 如果用大模型 API:设为 200(0.2 秒)
requestDelay: 500,
// ========== 翻译设置 ==========
targetLanguage: 'zh-CN', // 目标语言(建议不要改)
translationStyle: '简体中文', // 翻译风格(建议不要改)
};
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 以下不要修改,除非你明确知道你在做什么! ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 工具函数 | Utilities ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
/**
* 翻译队列管理器
* 实现并发控制和延迟控制,避免触发 API 速率限制
*
* @class TranslationQueue
* @description 管理翻译请求的队列,控制并发数量和请求间隔
*/
class TranslationQueue {
constructor(maxConcurrent, delay) {
this.maxConcurrent = maxConcurrent;
this.delay = delay;
this.queue = [];
this.activeCount = 0;
}
async add(translateFunc) {
return new Promise((resolve, reject) => {
this.queue.push({ translateFunc, resolve, reject });
this.process();
});
}
async process() {
if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) {
return;
}
const { translateFunc, resolve, reject } = this.queue.shift();
this.activeCount++;
try {
const result = await translateFunc();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.activeCount--;
// 添加延迟后处理下一个任务
if (this.queue.length > 0) {
setTimeout(() => this.process(), this.delay);
}
}
}
}
// 创建翻译队列实例
const translationQueue = new TranslationQueue(CONFIG.maxConcurrent, CONFIG.requestDelay);
// 失败计数器
const failureCount = {};
const MAX_FAILURES = 3;
let scriptStopped = false;
/**
* 检测文本是否主要是中文
*/
function isChinese(text) {
const chineseRegex = /[\u4e00-\u9fa5]/g;
const chineseChars = text.match(chineseRegex);
if (!chineseChars) return false;
// 如果中文字符占比超过 30%,认为是中文文本
const chineseRatio = chineseChars.length / text.length;
return chineseRatio > 0.3;
}
/**
* 调用 Google 翻译(免费)
*/
function translateWithGoogle(text) {
return new Promise((resolve, reject) => {
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=zh-CN&dt=t&q=${encodeURIComponent(text)}`;
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
onload: function(response) {
try {
if (response.status !== 200) {
reject(`Google 翻译请求失败:HTTP ${response.status}`);
return;
}
const result = JSON.parse(response.responseText);
if (result && result[0]) {
let translation = '';
result[0].forEach(item => {
if (item[0]) {
translation += item[0];
}
});
resolve(translation.trim());
} else {
reject('Google 翻译返回格式错误');
}
} catch (e) {
reject(`Google 翻译解析失败:${e.message}`);
}
},
onerror: function(error) {
reject('Google 翻译网络请求失败:' + error);
}
});
});
}
/**
* 调用 Microsoft 翻译(免费)
* 使用 Microsoft Edge 翻译接口
*/
function translateWithMicrosoft(text) {
return new Promise((resolve, reject) => {
// 第一步:获取授权令牌
GM_xmlhttpRequest({
method: 'GET',
url: 'https://edge.microsoft.com/translate/auth',
onload: function(authResponse) {
try {
if (authResponse.status !== 200) {
reject(`Microsoft 授权失败:HTTP ${authResponse.status}`);
return;
}
const authToken = authResponse.responseText;
// 第二步:使用授权令牌进行翻译
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=zh-Hans&from=en',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
data: JSON.stringify([{ text: text }]),
onload: function(translateResponse) {
try {
if (translateResponse.status !== 200) {
reject(`Microsoft 翻译请求失败:HTTP ${translateResponse.status}`);
return;
}
const result = JSON.parse(translateResponse.responseText);
if (result && result[0] && result[0].translations && result[0].translations[0]) {
const translation = result[0].translations[0].text.trim();
resolve(translation);
} else {
reject('Microsoft 翻译返回格式错误');
}
} catch (e) {
reject(`Microsoft 翻译解析失败:${e.message}`);
}
},
onerror: function(error) {
reject('Microsoft 翻译请求失败:' + error);
}
});
} catch (e) {
reject(`Microsoft 授权解析失败:${e.message}`);
}
},
onerror: function(error) {
reject('Microsoft 授权请求失败:' + error);
}
});
});
}
/**
* 调用 DeepL 翻译(需要 API Key)
*/
function translateWithDeepL(text) {
return new Promise((resolve, reject) => {
if (!CONFIG.deepl.apiKey) {
reject('DeepL API Key 未配置');
return;
}
const baseUrl = CONFIG.deepl.useFreeApi
? 'https://api-free.deepl.com/v2/translate'
: 'https://api.deepl.com/v2/translate';
const params = new URLSearchParams({
text: text,
target_lang: 'ZH',
auth_key: CONFIG.deepl.apiKey
});
GM_xmlhttpRequest({
method: 'POST',
url: baseUrl,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: params.toString(),
onload: function(response) {
try {
const result = JSON.parse(response.responseText);
if (result.translations && result.translations[0]) {
resolve(result.translations[0].text);
} else {
reject('DeepL 翻译响应格式错误');
}
} catch (e) {
reject('DeepL 翻译解析失败:' + e.message);
}
},
onerror: function(error) {
reject('DeepL 翻译网络请求失败:' + error);
}
});
});
}
/**
* 调用小牛翻译(需要 API Key)
*/
function translateWithNiutrans(text) {
return new Promise((resolve, reject) => {
if (!CONFIG.niutrans.apiKey) {
reject('小牛翻译 API Key 未配置');
return;
}
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.niutrans.com/NiuTransServer/translation',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({
src_text: text,
from: 'en',
to: 'zh',
apikey: CONFIG.niutrans.apiKey
}),
onload: function(response) {
try {
if (response.status !== 200) {
reject(`小牛翻译请求失败:HTTP ${response.status}`);
return;
}
const result = JSON.parse(response.responseText);
if (result.tgt_text) {
resolve(result.tgt_text.trim());
} else if (result.error_msg) {
reject(`小牛翻译错误:${result.error_msg}`);
} else {
reject('小牛翻译返回格式错误');
}
} catch (e) {
reject(`小牛翻译解析失败:${e.message}`);
}
},
onerror: function(error) {
reject('小牛翻译网络请求失败:' + error);
}
});
});
}
/**
* 调用 Gemini 翻译(使用 OpenAI 兼容格式)
*/
function translateWithGemini(text) {
return new Promise((resolve, reject) => {
const config = CONFIG.gemini;
if (!config || !config.apiKey) {
reject('Gemini API Key 未配置');
return;
}
const messages = [
{
role: 'system',
content: '你是一个专业的学术论文翻译助手。你的任务是将英文翻译成简体中文。重要规则:只输出翻译结果,不要输出任何解释、说明、注释或其他内容。'
},
{
role: 'user',
content: text
}
];
GM_xmlhttpRequest({
method: 'POST',
url: `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({
contents: [{
role: 'user',
parts: [{
text: `你是一个专业的学术论文翻译助手。请将以下英文翻译成简体中文。
重要要求:
- 只输出中文翻译
- 不要输出英文原文
- 不要添加任何解释、说明或其他内容
- 保持学术术语的准确性
英文原文:
${text}
中文翻译:`
}]
}],
generationConfig: {
temperature: config.temperature || 0.1,
maxOutputTokens: 4096,
}
}),
onload: function(response) {
try {
if (response.status !== 200) {
reject(`Gemini 翻译请求失败:HTTP ${response.status}`);
return;
}
const result = JSON.parse(response.responseText);
if (result.candidates && result.candidates[0] &&
result.candidates[0].content && result.candidates[0].content.parts &&
result.candidates[0].content.parts[0]) {
const translation = result.candidates[0].content.parts[0].text.trim();
resolve(translation);
} else {
console.error('[Gemini 错误] 响应数据:', result);
reject('Gemini 翻译返回格式错误');
}
} catch (e) {
reject(`Gemini 翻译解析失败:${e.message}`);
}
},
onerror: function(error) {
reject('Gemini 翻译网络请求失败:' + error);
}
});
});
}
/**
* 调用大模型翻译(通用函数)
*
* @description 支持所有 OpenAI Chat Completions API 格式的服务
* @param {string} text - 要翻译的文本
* @param {string} provider - 服务商名称
* @returns {Promise<string>} 翻译结果
*/
function translateWithLLM(text, provider) {
return new Promise((resolve, reject) => {
const config = CONFIG[provider];
if (!config || !config.apiKey) {
reject(`${provider} API Key 未配置`);
return;
}
// 服务商对应的 baseURL
const baseURLMap = {
'openai': 'https://api.openai.com/v1',
'deepseek': 'https://api.deepseek.com',
'siliconflow': 'https://api.siliconflow.cn/v1',
'groq': 'https://api.groq.com/openai/v1',
'openai-compatible': config.baseURL || ''
};
const baseURL = baseURLMap[provider];
if (!baseURL) {
reject(`未知的服务商:${provider}`);
return;
}
const messages = [
{
role: 'system',
content: '你是一个专业的学术论文翻译助手。你的任务是将英文翻译成简体中文。重要规则:只输出翻译结果,不要输出任何解释、说明、注释或其他内容。'
},
{
role: 'user',
content: `请将以下英文翻译成简体中文,只输出翻译结果:\n\n${text}`
}
];
GM_xmlhttpRequest({
method: 'POST',
url: `${baseURL}/chat/completions`,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`
},
data: JSON.stringify({
model: config.model,
messages: messages,
temperature: config.temperature || 0.3
}),
onload: function(response) {
try {
if (response.status !== 200) {
reject(`${provider} 翻译请求失败:HTTP ${response.status}`);
return;
}
const result = JSON.parse(response.responseText);
if (result.choices && result.choices[0] && result.choices[0].message) {
const translation = result.choices[0].message.content.trim();
resolve(translation);
} else {
reject(`${provider} 翻译返回格式错误`);
}
} catch (e) {
reject(`${provider} 翻译解析失败:${e.message}`);
}
},
onerror: function(error) {
reject(`${provider} 翻译网络请求失败:` + error);
}
});
});
}
/**
* 根据配置选择翻译 API
* @param {string} text - 要翻译的文本
* @param {string} type - 翻译类型:'title' 或 'abstract'
*/
function translate(text, type = 'title') {
// 检查脚本是否已停止
if (scriptStopped) {
return Promise.reject('脚本已因连续失败而停止');
}
// 根据类型选择提供商
let provider;
if (type === 'abstract' && CONFIG.abstractProvider) {
provider = CONFIG.abstractProvider;
} else {
provider = CONFIG.titleProvider;
}
// 控制台日志:开始翻译
const typeText = type === 'abstract' ? '摘要' : '标题';
const preview = text.length > 50 ? text.substring(0, 50) + '...' : text;
console.log(`%c[翻译${typeText}] %c使用 ${provider} 翻译:%c${preview}`,
'color: #4CAF50; font-weight: bold',
'color: #2196F3',
'color: #666');
// 选择对应的翻译函数
let translateFunc;
// 通用大模型服务商列表(使用 OpenAI 兼容格式)
const llmProviders = ['openai', 'deepseek', 'siliconflow', 'groq', 'openai-compatible'];
if (llmProviders.includes(provider)) {
// 使用通用大模型翻译函数
translateFunc = () => translateWithLLM(text, provider);
} else {
// 使用专用翻译函数
switch(provider) {
case 'google':
translateFunc = () => translateWithGoogle(text);
break;
case 'microsoft':
translateFunc = () => translateWithMicrosoft(text);
break;
case 'deepl':
translateFunc = () => translateWithDeepL(text);
break;
case 'niutrans':
translateFunc = () => translateWithNiutrans(text);
break;
case 'gemini':
translateFunc = () => translateWithGemini(text);
break;
default:
console.error(`%c[翻译错误] 未知的 API 提供商:${provider}`, 'color: #f44336; font-weight: bold');
return Promise.reject(`未知的 API 提供商:${provider}`);
}
}
// 通过队列管理器执行翻译,并记录结果
return translationQueue.add(translateFunc)
.then(result => {
// 翻译成功,重置该提供商的失败计数
failureCount[provider] = 0;
console.log(`%c[翻译成功] %c${result}`,
'color: #4CAF50; font-weight: bold',
'color: #333');
return result;
})
.catch(error => {
// 增加失败计数
failureCount[provider] = (failureCount[provider] || 0) + 1;
console.error(`%c[翻译失败] %c${provider} 翻译失败(${failureCount[provider]}/${MAX_FAILURES}):%c${error}`,
'color: #f44336; font-weight: bold',
'color: #2196F3',
'color: #f44336');
// 检查是否达到最大失败次数
if (failureCount[provider] >= MAX_FAILURES) {
scriptStopped = true;
console.error(`%c[严重错误] %c${provider} 连续失败 ${MAX_FAILURES} 次,脚本已停止运行`,
'color: #f44336; font-weight: bold; font-size: 14px',
'color: #f44336; font-size: 14px');
alert(`翻译服务 ${provider} 连续失败 ${MAX_FAILURES} 次,脚本已停止运行。\n\n请检查:\n1. API Key 是否正确\n2. 网络连接是否正常\n3. API 额度是否充足\n\n刷新页面后脚本将重新启动。`);
}
throw error;
});
}
/**
* 为摘要元素添加翻译
* 摘要翻译与标题翻译的区别:
* 1. 翻译文本较长,需要分段处理
* 2. 翻译结果显示在摘要下方,作为独立的翻译区块
* 3. 需要保留原文格式(段落、斜体等)
*/
async function addAbstractTranslation(abstractElement, getTextFunc) {
// 检查配置开关
if (!CONFIG.translateAbstract) {
return;
}
// 检查脚本是否已停止
if (scriptStopped) {
return;
}
// 检查是否已翻译或正在翻译
if (abstractElement.dataset.translated === 'true' || abstractElement.dataset.translating === 'true') {
return;
}
// 检查是否已存在翻译
if (abstractElement.querySelector('.abstract-translation-container') ||
abstractElement.parentElement.querySelector('.abstract-translation-container')) {
abstractElement.dataset.translated = 'true';
return;
}
const originalText = getTextFunc(abstractElement);
if (!originalText || originalText.trim().length === 0) {
return;
}
// 检测是否是中文
if (isChinese(originalText)) {
abstractElement.dataset.translated = 'true';
return;
}
// 创建加载动画(使用与翻译结果相同的样式)
const loadingDiv = document.createElement('div');
loadingDiv.className = 'abstract-translation-loading';
loadingDiv.style.cssText = 'margin-top: 16px; padding: 12px; background-color: rgba(0, 0, 0, 0.02); border-left: 3px solid rgba(0, 0, 0, 0.1); border-radius: 4px; line-height: 1.6; opacity: 0.8;';
loadingDiv.textContent = '正在翻译摘要...';
// 添加脉冲动画
const style = document.createElement('style');
if (!document.getElementById('translation-loading-style')) {
style.id = 'translation-loading-style';
style.textContent = `
@keyframes translation-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
.translation-loading, .abstract-translation-loading {
animation: translation-pulse 1.5s ease-in-out infinite;
}
`;
document.head.appendChild(style);
}
// 插入加载动画
abstractElement.parentElement.insertBefore(loadingDiv, abstractElement.nextSibling);
try {
// 标记为正在翻译
abstractElement.dataset.translating = 'true';
// 调用翻译 API(使用摘要专用提供商)
const translation = await translate(originalText, 'abstract');
// 移除加载动画
if (loadingDiv && loadingDiv.parentElement) {
loadingDiv.remove();
}
// 再次检查是否已存在翻译
if (abstractElement.querySelector('.abstract-translation-container') ||
abstractElement.parentElement.querySelector('.abstract-translation-container')) {
abstractElement.dataset.translated = 'true';
abstractElement.dataset.translating = 'false';
return;
}
// 处理段落:智能分段(支持多种换行格式)
// 尝试按双换行分段,如果没有则按单换行分段,如果还是只有一段则按句号分段
let paragraphs = translation.split(/\n\n+/).map(p => p.trim()).filter(p => p.length > 0);
// 如果只有一个段落,尝试按单换行分
if (paragraphs.length === 1) {
const singleNewlineSplit = translation.split(/\n/).map(p => p.trim()).filter(p => p.length > 0);
if (singleNewlineSplit.length > 1) {
paragraphs = singleNewlineSplit;
}
}
// 创建折叠容器
const collapseContainer = document.createElement('div');
collapseContainer.className = 'abstract-translation-container';
collapseContainer.style.cssText = 'margin-top: 12px;';
// 创建折叠按钮
const toggleButton = document.createElement('button');
toggleButton.className = 'abstract-translation-toggle';
toggleButton.style.cssText = 'background: none; border: none; color: #0071bc; cursor: pointer; padding: 4px 0; font-size: 14px; text-decoration: underline; opacity: 0.8;';
toggleButton.textContent = '▶ 显示中文翻译';
// 创建翻译内容容器
const translationDiv = document.createElement('div');
translationDiv.className = 'abstract-translation-zh';
translationDiv.style.cssText = 'display: none; margin-top: 4px; padding: 8px 12px; background-color: rgba(0, 0, 0, 0.02); border-left: 2px solid rgba(0, 0, 0, 0.15); opacity: 0.85;';
// 如果只有一个段落,直接显示文本
if (paragraphs.length === 1) {
translationDiv.textContent = paragraphs[0];
} else {
// 多个段落时,用 <p> 标签包裹每个段落
paragraphs.forEach(para => {
const p = document.createElement('p');
p.style.cssText = 'margin: 0 0 0.75em 0;';
p.textContent = para;
translationDiv.appendChild(p);
});
// 移除最后一个段落的底部边距
const lastP = translationDiv.lastChild;
if (lastP) lastP.style.marginBottom = '0';
}
// 添加点击事件
toggleButton.addEventListener('click', function() {
const isVisible = translationDiv.style.display !== 'none';
if (isVisible) {
translationDiv.style.display = 'none';
toggleButton.textContent = '▶ 显示中文翻译';
} else {
translationDiv.style.display = 'block';
toggleButton.textContent = '▼ 收起中文翻译';
}
});
// 组装元素
collapseContainer.appendChild(toggleButton);
collapseContainer.appendChild(translationDiv);
// 插入到摘要后面
abstractElement.parentElement.insertBefore(collapseContainer, abstractElement.nextSibling);
// 标记为已翻译
abstractElement.dataset.translated = 'true';
abstractElement.dataset.translating = 'false';
} catch (error) {
// 移除加载动画
if (loadingDiv && loadingDiv.parentElement) {
loadingDiv.remove();
}
console.error(`%c[摘要翻译失败] %c${error}`,
'color: #f44336; font-weight: bold',
'color: #f44336');
abstractElement.dataset.translating = 'false';
}
}
/**
* 为标题元素添加翻译
*/
async function addTranslation(titleElement, getTextFunc, createTranslationFunc) {
// 检查脚本是否已停止
if (scriptStopped) {
return;
}
// 检查是否已翻译或正在翻译
if (titleElement.dataset.translated === 'true' || titleElement.dataset.translating === 'true') {
return;
}
// 检查是否已经存在翻译元素(防止重复翻译)
// 检查标题元素内部和父元素
const existingTranslation = titleElement.querySelector('.translation-zh') ||
titleElement.parentElement?.querySelector('.translation-zh');
if (existingTranslation) {
titleElement.dataset.translated = 'true';
return;
}
const originalText = getTextFunc(titleElement);
if (!originalText || originalText.trim().length === 0) {
return;
}
// 检测是否是中文,如果是则不翻译
if (isChinese(originalText)) {
titleElement.dataset.translated = 'true';
return;
}
// 创建加载动画(使用与翻译结果相同的样式)
const loadingElement = document.createElement('span');
loadingElement.className = 'translation-loading';
loadingElement.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;';
loadingElement.textContent = '翻译中...';
// 添加脉冲动画
const style = document.createElement('style');
if (!document.getElementById('translation-loading-style')) {
style.id = 'translation-loading-style';
style.textContent = `
@keyframes translation-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
.translation-loading, .abstract-translation-loading {
animation: translation-pulse 1.5s ease-in-out infinite;
}
`;
document.head.appendChild(style);
}
// 将加载动画插入到标题内部(与翻译结果位置一致)
titleElement.appendChild(loadingElement);
try {
// 标记为正在翻译
titleElement.dataset.translating = 'true';
// 调用翻译 API(使用标题提供商)
const translation = await translate(originalText, 'title');
// 移除加载动画
if (loadingElement && loadingElement.parentElement) {
loadingElement.remove();
}
// 再次检查是否已存在翻译(防止并发问题)
const checkExisting = titleElement.querySelector('.translation-zh') ||
titleElement.parentElement?.querySelector('.translation-zh');
if (checkExisting) {
titleElement.dataset.translated = 'true';
titleElement.dataset.translating = 'false';
return;
}
// 创建并插入翻译元素
createTranslationFunc(titleElement, translation);
// 标记为已翻译
titleElement.dataset.translated = 'true';
titleElement.dataset.translating = 'false';
} catch (error) {
// 移除加载动画
if (loadingElement && loadingElement.parentElement) {
loadingElement.remove();
}
console.error(`%c[标题翻译失败] %c${error}`,
'color: #f44336; font-weight: bold',
'color: #f44336');
titleElement.dataset.translating = 'false';
// 可选:显示错误提示
// createTranslationFunc(titleElement, `[翻译失败:${error}]`);
}
}
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 网站特定处理 | Site Processors ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
/**
* PubMed 处理器
*
* @description 处理 PubMed 网站的论文标题和摘要翻译
* @website https://pubmed.ncbi.nlm.nih.gov/
* @supports
* - 主页 (Home): Trending Articles
* - 搜索结果页 (Search): 论文标题列表
* - 文章详情页 (Detail): 主标题、Similar articles、Cited by、摘要
*/
async function processPubMed() {
// 收集所有需要翻译的标题元素
const titleElements = [];
// 1. 主页 - Trending Articles 区域的论文标题
const trendingArticles = document.querySelectorAll('.homepage-trending-and-latest .full-docsum a[href^="/"]');
trendingArticles.forEach(titleElement => {
if (!titleElement.querySelector('.docsum-authors') && !titleElement.classList.contains('see-more')) {
titleElements.push({
element: titleElement,
getText: (el) => {
const clone = el.cloneNode(true);
const existingTranslation = clone.querySelector('.translation-zh');
if (existingTranslation) existingTranslation.remove();
return clone.textContent.trim();
},
createTranslation: (el, translation) => {
if (el.querySelector('.translation-zh')) return;
const translationSpan = document.createElement('span');
translationSpan.className = 'translation-zh';
translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;';
translationSpan.textContent = translation;
el.appendChild(translationSpan);
}
});
}
});
// 2. 搜索结果页 + Similar articles + Cited by
const docsumTitles = document.querySelectorAll('.docsum-title');
docsumTitles.forEach(titleElement => {
titleElements.push({
element: titleElement,
getText: (el) => {
const clone = el.cloneNode(true);
const existingTranslation = clone.querySelector('.translation-zh');
if (existingTranslation) existingTranslation.remove();
return clone.textContent.trim();
},
createTranslation: (el, translation) => {
if (el.querySelector('.translation-zh')) return;
const translationSpan = document.createElement('span');
translationSpan.className = 'translation-zh';
translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;';
translationSpan.textContent = translation;
el.appendChild(translationSpan);
}
});
});
// 3. 文章详情页 - 主标题
const detailTitleSelectors = [
'h1.heading-title',
'.heading-title',
'h1[class*="heading"]',
'main h1',
'article h1',
];
let detailTitle = null;
for (const selector of detailTitleSelectors) {
const elements = document.querySelectorAll(selector);
for (const el of elements) {
if (el.textContent.trim().length > 0 &&
!el.closest('.similar-articles') &&
!el.closest('.articles-from-the-same-journal') &&
!el.classList.contains('docsum-title')) {
detailTitle = el;
break;
}
}
if (detailTitle) break;
}
if (detailTitle) {
titleElements.push({
element: detailTitle,
getText: (el) => {
const clone = el.cloneNode(true);
const existingTranslation = clone.querySelector('.translation-zh');
if (existingTranslation) existingTranslation.remove();
return clone.textContent.trim();
},
createTranslation: (el, translation) => {
if (el.querySelector('.translation-zh')) return;
const translationSpan = document.createElement('span');
translationSpan.className = 'translation-zh';
translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;';
translationSpan.textContent = translation;
el.appendChild(translationSpan);
}
});
}
// 翻译所有标题(并发)
await Promise.all(titleElements.map(item =>
addTranslation(item.element, item.getText, item.createTranslation)
));
// 标题全部翻译完成后,再翻译摘要
if (CONFIG.translateAbstract) {
const abstractElement = document.querySelector('div.abstract#abstract');
if (abstractElement) {
await addAbstractTranslation(
abstractElement,
(el) => {
const clone = el.cloneNode(true);
const title = clone.querySelector('h2.title');
if (title) title.remove();
const keywords = clone.querySelector('p strong.sub-title');
if (keywords && keywords.textContent.includes('Keywords')) {
keywords.parentElement.remove();
}
const existingTranslation = clone.querySelector('.abstract-translation-zh');
if (existingTranslation) existingTranslation.remove();
return clone.textContent.trim();
}
);
}
}
}
/**
* Europe PMC 处理
* 支持的页面类型:
* - 文章详情页 (Detail): 主标题、Similar articles
* - 搜索结果页 (Search): 论文标题列表(与 Similar articles 使用相同选择器)
*
* 注意:Europe PMC 的搜索结果页和 Similar articles 使用相同的 HTML 结构,
* 因此使用统一的选择器 h3.citation-title a 即可覆盖两种情况
*/
async function processEuropePMC() {
// 1. 文章详情页 - 主标题
// 选择器:h1#article--current--title, h1.article-metadata-title
// 示例 HTML:
// <h1 id="article--current--title" class="article-metadata-title">TRIM13 prevents...</h1>
const detailTitleSelectors = [
'h1#article--current--title',
'h1.article-metadata-title',
'h1[id*="article"][id*="title"]',
];
let detailTitle = null;
for (const selector of detailTitleSelectors) {
detailTitle = document.querySelector(selector);
if (detailTitle && detailTitle.textContent.trim().length > 0) {
break;
}
detailTitle = null;
}
if (detailTitle) {
addTranslation(
detailTitle,
(el) => {
const clone = el.cloneNode(true);
const existingTranslation = clone.querySelector('.translation-zh');
if (existingTranslation) existingTranslation.remove();
return clone.textContent.trim();
},
(el, translation) => {
if (el.querySelector('.translation-zh')) return;
const translationSpan = document.createElement('span');
translationSpan.className = 'translation-zh';
translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;';
translationSpan.textContent = translation;
el.appendChild(translationSpan);
}
);
}
// 2. 搜索结果页 + Similar articles - 统一使用 h3.citation-title a
const titleElements = [];
const citationTitles = document.querySelectorAll('h3.citation-title a');
citationTitles.forEach(titleElement => {
titleElements.push({
element: titleElement,
getText: (el) => {
const clone = el.cloneNode(true);
const existingTranslation = clone.querySelector('.translation-zh');
if (existingTranslation) existingTranslation.remove();
return clone.textContent.trim();
},
createTranslation: (el, translation) => {
if (el.querySelector('.translation-zh')) return;
const translationSpan = document.createElement('span');
translationSpan.className = 'translation-zh';
translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;';
translationSpan.textContent = translation;
el.appendChild(translationSpan);
}
});
});
// 翻译所有标题
await Promise.all(titleElements.map(item =>
addTranslation(item.element, item.getText, item.createTranslation)
));
// 标题翻译完成后,再翻译摘要
if (CONFIG.translateAbstract) {
const abstractSelectors = [
'div#article--abstract--content.abstract',
'div.abstract-content#eng-abstract',
];
let abstractElement = null;
for (const selector of abstractSelectors) {
abstractElement = document.querySelector(selector);
if (abstractElement && abstractElement.textContent.trim().length > 0) {
break;
}
abstractElement = null;
}
if (abstractElement) {
await addAbstractTranslation(
abstractElement,
(el) => {
const clone = el.cloneNode(true);
const existingTranslation = clone.querySelector('.abstract-translation-zh');
if (existingTranslation) existingTranslation.remove();
return clone.textContent.trim();
}
);
}
}
}
// ==================== 主函数 ====================
/**
* Semantic Scholar 处理
* 支持的页面类型:
* - 搜索结果页 (Search): 论文标题列表
* - 文章详情页 (Detail): 主标题、Citations、References
* - 个人主页 (Profile): 论文标题列表
*
* 注意:除了详情页主标题外,其他所有标题都使用 .cl-paper-title 类
*/
async function processSemanticScholar() {
// 1. 文章详情页 - 主标题
// 选择器:h1[data-test-id="paper-detail-title"]
// 示例 HTML:
// <h1 data-test-id="paper-detail-title">Trim and Fill: A Simple Funnel...</h1>
const detailTitle = document.querySelector('h1[data-test-id="paper-detail-title"]');
if (detailTitle && detailTitle.textContent.trim().length > 0) {
addTranslation(
detailTitle,
(el) => {
const clone = el.cloneNode(true);
const existingTranslation = clone.querySelector('.translation-zh');
if (existingTranslation) existingTranslation.remove();
return clone.textContent.trim();
},
(el, translation) => {
if (el.querySelector('.translation-zh')) return;
const translationSpan = document.createElement('span');
translationSpan.className = 'translation-zh';
translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;';
translationSpan.textContent = translation;
el.appendChild(translationSpan);
}
);
}
// 2. 搜索结果页 + Citations + References + 个人主页
const titleElements = [];
const paperTitles = document.querySelectorAll('.cl-paper-title');
paperTitles.forEach(titleElement => {
titleElements.push({
element: titleElement,
getText: (el) => {
const clone = el.cloneNode(true);
const existingTranslation = clone.querySelector('.translation-zh');
if (existingTranslation) existingTranslation.remove();
return clone.textContent.trim();
},
createTranslation: (el, translation) => {
if (el.querySelector('.translation-zh')) return;
const translationSpan = document.createElement('span');
translationSpan.className = 'translation-zh';
translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;';
translationSpan.textContent = translation;
el.appendChild(translationSpan);
}
});
});
// 翻译所有标题
await Promise.all(titleElements.map(item =>
addTranslation(item.element, item.getText, item.createTranslation)
));
}
/**
* Google Scholar 处理
* 支持的页面类型:
* - 搜索结果页 (Search): 论文标题列表
*
* 注意:Google Scholar 只有搜索功能,没有详情页
*/
async function processGoogleScholar() {
const titleElements = [];
const searchTitles = document.querySelectorAll('h3.gs_rt');
searchTitles.forEach(titleElement => {
titleElements.push({
element: titleElement,
getText: (el) => {
const clone = el.cloneNode(true);
const existingTranslation = clone.querySelector('.translation-zh');
if (existingTranslation) existingTranslation.remove();
return clone.textContent.trim();
},
createTranslation: (el, translation) => {
if (el.querySelector('.translation-zh')) return;
const translationSpan = document.createElement('span');
translationSpan.className = 'translation-zh';
translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;';
translationSpan.textContent = translation;
el.appendChild(translationSpan);
}
});
});
// 翻译所有标题
await Promise.all(titleElements.map(item =>
addTranslation(item.element, item.getText, item.createTranslation)
));
}
/**
* 根据当前网站选择对应的处理函数
*/
function processCurrentSite() {
const hostname = window.location.hostname;
if (hostname.includes('pubmed.ncbi.nlm.nih.gov')) {
processPubMed();
} else if (hostname.includes('europepmc.org')) {
processEuropePMC();
} else if (hostname.includes('semanticscholar.org')) {
processSemanticScholar();
} else if (hostname.includes('scholar.google')) {
processGoogleScholar();
}
}
/**
* 使用 MutationObserver 监听 DOM 变化
*/
function observeChanges() {
const observer = new MutationObserver((mutations) => {
// 延迟执行,避免频繁触发
setTimeout(() => {
processCurrentSite();
}, 500);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// ==================== 初始化 ====================
// 输出启动信息
console.log('%c文献标题翻译 v1.0', 'color: #4CAF50; font-size: 16px; font-weight: bold');
console.log('%cAcademic Paper Title Translator', 'color: #2196F3; font-size: 12px');
console.log('%cBuild by Miao and Claude Sonnet 4.5 with ♥', 'color: #FF5722; font-size: 12px');
console.log('');
// 输出配置信息
const providerNameMap = {
'google': 'Google 翻译',
'microsoft': 'Microsoft 翻译',
'deepl': 'DeepL 翻译',
'niutrans': '小牛翻译',
'openai': 'OpenAI',
'deepseek': 'DeepSeek',
'gemini': 'Google Gemini',
'siliconflow': '硅基流动',
'groq': 'Groq',
'openai-compatible': 'OpenAI 兼容接口'
};
const titleProviderName = providerNameMap[CONFIG.titleProvider] || CONFIG.titleProvider;
const abstractProviderName = CONFIG.abstractProvider
? (providerNameMap[CONFIG.abstractProvider] || CONFIG.abstractProvider)
: '与标题相同';
console.log('%c[配置信息]', 'color: #FF9800; font-weight: bold');
console.log(` 标题翻译提供商:%c${titleProviderName}`, 'color: #2196F3; font-weight: bold');
console.log(` 摘要翻译提供商:%c${abstractProviderName}`, 'color: #2196F3; font-weight: bold');
console.log(` 摘要翻译开关:%c${CONFIG.translateAbstract ? '已启用' : '已关闭'}`,
CONFIG.translateAbstract ? 'color: #4CAF50; font-weight: bold' : 'color: #999');
console.log(` 并发数量:${CONFIG.maxConcurrent}`);
console.log(` 请求延迟:${CONFIG.requestDelay}ms`);
// 显示大模型配置信息
const allLlmProviders = ['openai', 'deepseek', 'gemini', 'siliconflow', 'groq', 'openai-compatible'];
const usedProviders = [CONFIG.titleProvider, CONFIG.abstractProvider].filter(p => allLlmProviders.includes(p));
if (usedProviders.length > 0) {
usedProviders.forEach(provider => {
const config = CONFIG[provider];
if (config && config.model) {
console.log(` ${providerNameMap[provider]} 模型:${config.model}`);
}
});
}
console.log('');
// 页面加载完成后执行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
processCurrentSite();
observeChanges();
});
} else {
processCurrentSite();
observeChanges();
}
console.log('%c[脚本已启动] 正在监听页面变化...', 'color: #4CAF50; font-weight: bold');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment