Greasy Fork is available in English.
将推文翻译为简体中文,并在下方显示
// ==UserScript==
// @name X 翻译姬
// @namespace https://github.com/SIXiaolong1117/Rules
// @version 0.3
// @description 将推文翻译为简体中文,并在下方显示
// @license MIT
// @icon https://x.com/favicon.ico
// @author SI Xiaolong
// @match https://twitter.com/*
// @match https://x.com/*
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @connect translate.googleapis.com
// @connect api.mymemory.translated.net
// ==/UserScript==
(function () {
'use strict';
// 已翻译的元素集合,避免重复翻译
const translatedElements = new WeakSet();
// 默认设置
const DEFAULT_SETTINGS = {
translationSource: 'google',
sourceLang: 'auto',
targetLang: 'zh-CN'
};
// 获取当前设置
function getSettings() {
return {
translationSource: GM_getValue('translationSource', DEFAULT_SETTINGS.translationSource),
sourceLang: GM_getValue('sourceLang', DEFAULT_SETTINGS.sourceLang),
targetLang: GM_getValue('targetLang', DEFAULT_SETTINGS.targetLang)
};
}
// 保存设置
function saveSetting(key, value) {
GM_setValue(key, value);
location.reload();
}
// 翻译源配置
const TRANSLATION_SOURCES = {
google: 'Google 翻译',
mymemory: 'MyMemory 翻译'
};
// 语言配置
const LANGUAGES = {
'auto': '自动检测',
'zh-CN': '简体中文',
'zh-TW': '繁体中文',
'en': '英语',
'ja': '日语',
'ko': '韩语',
'es': '西班牙语',
'fr': '法语',
'de': '德语',
'ru': '俄语',
'ar': '阿拉伯语',
'pt': '葡萄牙语',
'it': '意大利语',
'th': '泰语',
'vi': '越南语'
};
// 检测当前主题(深色或浅色)
function detectTheme() {
const bgColor = window.getComputedStyle(document.body).backgroundColor;
const rgb = bgColor.match(/\d+/g);
if (rgb) {
const brightness = (parseInt(rgb[0]) + parseInt(rgb[1]) + parseInt(rgb[2])) / 3;
return brightness > 128 ? 'light' : 'dark';
}
return 'dark';
}
// 获取主题相关的颜色
function getThemeColors() {
const theme = detectTheme();
if (theme === 'light') {
return {
background: 'rgba(247, 249, 249, 0.8)',
loadingText: '#536471',
translatedText: '#0f1419',
errorText: '#f4212e',
headerText: '#536471',
border: 'rgba(207, 217, 222, 0.3)'
};
} else {
return {
background: 'rgba(32, 35, 39, 0.8)',
loadingText: '#8b949e',
translatedText: '#e7e9ea',
errorText: '#ff6b6b',
headerText: '#8b949e',
border: 'rgba(47, 51, 54, 0.3)'
};
}
}
// 注册菜单命令
function registerMenuCommands() {
const settings = getSettings();
// 翻译源选择
GM_registerMenuCommand(`🌐 翻译源: ${TRANSLATION_SOURCES[settings.translationSource]}`, () => {
const sources = Object.keys(TRANSLATION_SOURCES);
const currentIndex = sources.indexOf(settings.translationSource);
const nextIndex = (currentIndex + 1) % sources.length;
const nextSource = sources[nextIndex];
saveSetting('translationSource', nextSource);
});
// 源语言选择
GM_registerMenuCommand(`📤 源语言: ${LANGUAGES[settings.sourceLang]}`, () => {
showLanguageSelector('sourceLang', '选择源语言');
});
// 目标语言选择
GM_registerMenuCommand(`📥 目标语言: ${LANGUAGES[settings.targetLang]}`, () => {
showLanguageSelector('targetLang', '选择目标语言');
});
// 重置设置
GM_registerMenuCommand('🔄 重置为默认设置', () => {
if (confirm('确定要重置所有设置为默认值吗?')) {
saveSetting('translationSource', DEFAULT_SETTINGS.translationSource);
saveSetting('sourceLang', DEFAULT_SETTINGS.sourceLang);
saveSetting('targetLang', DEFAULT_SETTINGS.targetLang);
}
});
}
// 显示语言选择对话框
function showLanguageSelector(settingKey, title) {
const languages = Object.keys(LANGUAGES);
const languageList = languages.map((code, index) =>
`${index + 1}. ${LANGUAGES[code]} (${code})`
).join('\n');
const input = prompt(`${title}\n\n${languageList}\n\n请输入语言代码(如 zh-CN, en, ja 等):`);
if (input && LANGUAGES[input]) {
saveSetting(settingKey, input);
} else if (input) {
alert('无效的语言代码!');
}
}
// Google翻译API
async function translateWithGoogle(text, sourceLang, targetLang) {
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function (response) {
try {
const result = JSON.parse(response.responseText);
const translatedText = result[0].map(item => item[0]).join('');
resolve(translatedText);
} catch (e) {
reject(e);
}
},
onerror: function (error) {
reject(error);
}
});
});
}
// MyMemory翻译API
async function translateWithMyMemory(text, sourceLang, targetLang) {
const langPair = `${sourceLang === 'auto' ? 'en' : sourceLang}|${targetLang}`;
const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${langPair}`;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function (response) {
try {
const result = JSON.parse(response.responseText);
if (result.responseStatus === 200) {
resolve(result.responseData.translatedText);
} else {
reject(new Error('Translation failed'));
}
} catch (e) {
reject(e);
}
},
onerror: function (error) {
reject(error);
}
});
});
}
// 统一翻译接口
async function translateText(text) {
const settings = getSettings();
const { translationSource, sourceLang, targetLang } = settings;
switch (translationSource) {
case 'google':
return await translateWithGoogle(text, sourceLang, targetLang);
case 'mymemory':
return await translateWithMyMemory(text, sourceLang, targetLang);
default:
return await translateWithGoogle(text, sourceLang, targetLang);
}
}
// 保护文本中的特殊元素(@mentions, #hashtags, URLs)
function protectSpecialElements(text) {
const protectedElements = [];
let protectedText = text;
// 保护 URL(优先处理,因为URL可能包含其他特殊字符)
protectedText = protectedText.replace(/https?:\/\/[^\s]+/g, (match) => {
const placeholder = `__URL_${protectedElements.length}__`;
protectedElements.push(match);
return placeholder;
});
// 保护 @mentions
protectedText = protectedText.replace(/@[\w]+/g, (match) => {
const placeholder = `__MENTION_${protectedElements.length}__`;
protectedElements.push(match);
return placeholder;
});
// 保护 #hashtags
protectedText = protectedText.replace(/#[\w\u4e00-\u9fa5]+/g, (match) => {
const placeholder = `__HASHTAG_${protectedElements.length}__`;
protectedElements.push(match);
return placeholder;
});
return { protectedText, protectedElements };
}
// 恢复保护的特殊元素
function restoreSpecialElements(text, protectedElements) {
let restoredText = text;
protectedElements.forEach((element, index) => {
if (element.startsWith('@')) {
restoredText = restoredText.replace(`__MENTION_${index}__`, element);
} else if (element.startsWith('#')) {
restoredText = restoredText.replace(`__HASHTAG_${index}__`, element);
} else if (element.startsWith('http')) {
restoredText = restoredText.replace(`__URL_${index}__`, element);
}
});
return restoredText;
}
// 提取纯文本内容(不提取链接信息)
function extractPlainText(element) {
let textParts = [];
function traverse(node) {
// 跳过某些不需要翻译的元素(如按钮、SVG等)
if (node.nodeType === Node.ELEMENT_NODE) {
const tagName = node.tagName.toLowerCase();
if (tagName === 'button' || tagName === 'svg' || tagName === 'path') {
return;
}
}
for (let child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
const text = child.textContent.trim();
if (text) textParts.push(text);
} else if (child.nodeType === Node.ELEMENT_NODE) {
// 对于链接元素,直接提取文本内容,保持位置
if (child.tagName === 'A') {
const linkText = child.textContent.trim();
if (linkText) textParts.push(linkText);
} else {
// 递归处理其他元素
traverse(child);
}
}
}
}
traverse(element);
return textParts.join(' ');
}
// 创建占位翻译框
function createPlaceholderBox() {
const settings = getSettings();
const sourceName = TRANSLATION_SOURCES[settings.translationSource];
const colors = getThemeColors();
const box = document.createElement('div');
box.className = 'x-translator-box';
box.style.cssText = `
margin-top: 12px;
padding: 12px 16px;
background-color: ${colors.background};
border: 1px solid ${colors.border};
border-radius: 12px;
font-size: 15px;
line-height: 1.5;
transition: background-color 0.2s ease, border-color 0.2s ease;
`;
// 创建标题
const header = document.createElement('div');
header.className = 'x-translator-header';
header.style.cssText = `
font-size: 13px;
color: ${colors.headerText};
margin-bottom: 8px;
font-weight: 500;
transition: color 0.2s ease;
`;
header.textContent = `📝 ${sourceName}`;
// 创建加载中的内容容器
const content = document.createElement('div');
content.className = 'x-translator-content';
content.style.cssText = `
color: ${colors.loadingText};
white-space: pre-wrap;
word-wrap: break-word;
transition: color 0.2s ease;
`;
content.textContent = '翻译中...';
box.appendChild(header);
box.appendChild(content);
return box;
}
// 更新翻译框内容(成功)
function updateTranslationBox(box, translatedText) {
const colors = getThemeColors();
const content = box.querySelector('.x-translator-content');
content.style.color = colors.translatedText;
content.textContent = translatedText;
}
// 更新翻译框内容(失败)
function updateTranslationBoxError(box, errorMessage) {
const colors = getThemeColors();
const content = box.querySelector('.x-translator-content');
content.style.color = colors.errorText;
content.textContent = `❌ 翻译失败: ${errorMessage}`;
}
// 更新所有翻译框的主题
function updateAllBoxesTheme() {
const colors = getThemeColors();
const boxes = document.querySelectorAll('.x-translator-box');
boxes.forEach(box => {
box.style.backgroundColor = colors.background;
box.style.borderColor = colors.border;
const header = box.querySelector('.x-translator-header');
if (header) {
header.style.color = colors.headerText;
}
const content = box.querySelector('.x-translator-content');
if (content) {
const text = content.textContent;
if (text === '翻译中...') {
content.style.color = colors.loadingText;
} else if (text.startsWith('❌')) {
content.style.color = colors.errorText;
} else {
content.style.color = colors.translatedText;
}
}
});
}
// 处理翻译
async function processTranslation(element, isTrend = false) {
// 如果已经翻译过,跳过
if (translatedElements.has(element)) {
return;
}
// 标记为已处理
translatedElements.add(element);
// 提取纯文本内容
const originalText = extractPlainText(element);
if (originalText.length === 0) {
return;
}
// 对于热搜,进行额外过滤
if (isTrend) {
// 排除"xx的趋势"这类描述性文字
if (originalText.match(/的趋势|条推文|Trending|posts?$/i)) {
return;
}
// 如果文本太短(少于2个字符),也跳过
if (originalText.length < 2) {
return;
}
}
// 创建占位框并立即插入到页面
const placeholderBox = createPlaceholderBox();
const parentContainer = element.parentElement;
if (parentContainer) {
parentContainer.insertBefore(placeholderBox, element.nextSibling);
}
try {
// 保护特殊元素
const { protectedText, protectedElements } = protectSpecialElements(originalText);
// 翻译文本
const rawTranslatedText = await translateText(protectedText);
// 恢复特殊元素
const translatedText = restoreSpecialElements(rawTranslatedText, protectedElements);
// 如果翻译结果与原文相同,说明可能已经是目标语言,移除占位框
if (translatedText === originalText) {
if (placeholderBox.parentNode) {
placeholderBox.parentNode.removeChild(placeholderBox);
}
return;
}
// 更新占位框内容为翻译结果
updateTranslationBox(placeholderBox, translatedText);
} catch (e) {
console.error('翻译失败:', e);
// 更新占位框内容为错误信息
updateTranslationBoxError(placeholderBox, e.message || '未知错误');
}
}
// 查找并处理所有推文
function findAndTranslateTweets() {
const tweetDivs = document.querySelectorAll('[data-testid="tweetText"]');
tweetDivs.forEach(div => {
processTranslation(div, false);
});
}
// 查找并处理用户简介
function findAndTranslateUserDescription() {
const userDescriptions = document.querySelectorAll('[data-testid="UserDescription"]');
userDescriptions.forEach(div => {
processTranslation(div, false);
});
}
// 查找并处理热搜趋势(优化版)
function findAndTranslateTrends() {
const trends = document.querySelectorAll('[data-testid="trend"]');
trends.forEach(trendDiv => {
const divs = trendDiv.querySelectorAll('div[dir="ltr"]');
let trendTextElement = null;
for (let div of divs) {
const text = div.textContent.trim();
const classList = div.className || '';
if (!text) continue;
if (text.match(/趋势|Trending/i)) continue;
if (text.match(/条帖子|posts?$/i)) continue;
if (text.match(/^[\d,]+$/)) continue;
if (classList.includes('r-b88u0q') ||
(!trendTextElement && text.length > 0)) {
trendTextElement = div;
break;
}
}
if (trendTextElement) {
processTranslation(trendTextElement, true);
}
});
}
// 查找并翻译所有内容
function findAndTranslateAll() {
findAndTranslateTweets();
findAndTranslateUserDescription();
findAndTranslateTrends();
}
// 监听DOM变化,处理动态加载的内容
const observer = new MutationObserver((mutations) => {
findAndTranslateAll();
});
// 监听主题变化
const themeObserver = new MutationObserver(() => {
updateAllBoxesTheme();
});
// 注册菜单
registerMenuCommands();
// 开始观察
observer.observe(document.body, {
childList: true,
subtree: true
});
// 监听body的属性变化(主题切换通常会改变body的class或style)
themeObserver.observe(document.body, {
attributes: true,
attributeFilter: ['class', 'style']
});
// 监听HTML元素的属性变化(有些网站会在html元素上设置主题)
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'style', 'data-theme']
});
// 初始翻译
findAndTranslateAll();
// 定期检查新内容(作为备用)
setInterval(findAndTranslateAll, 2000);
console.log('Twitter翻译脚本已加载,当前设置:', getSettings());
})();