Twitter (X) 中文汉化插件
// ==UserScript==
// @name Twitter (x) 中文汉化插件
// @namespace https://github.com/wjm13206/x-i18n-plugin/
// @version 1.0
// @description Twitter (X) 中文汉化插件
// @author k1995
// @author wjm13206
// @match https://twitter.com/*
// @match https://x.com/*
// @grant GM_getResourceText
// @resource zh-CN https://raw.githubusercontent.com/wjm13206/x-i18n-plugin/master/locales/zh-CN.json
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const lang = "zh-CN"; // 只支持中文
const locales = JSON.parse(GM_getResourceText(lang));
// 初始化翻译
translatePage();
// 监听DOM变化
observeDOMChanges();
function translateElement(el) {
if (!el || !el.nodeValue) return;
const txtSrc = el.nodeValue.trim();
if (!txtSrc) return;
const key = txtSrc.toLowerCase()
.replace(/\xa0/g, ' ') // 替换
.replace(/\s{2,}/g, ' ');
if (locales.dict[key]) {
el.nodeValue = el.nodeValue.replace(txtSrc, locales.dict[key]);
}
}
function shouldTranslateEl(el) {
const blockTags = ["SCRIPT", "STYLE", "CODE", "PRE", "TEXTAREA"];
if (blockTags.includes(el.tagName)) {
return false;
}
// 跳过特定类名
if (el.classList) {
const blockClasses = ["emoji", "icon", "username", "handle"];
for (let cls of blockClasses) {
if (el.classList.contains(cls)) {
return false;
}
}
}
return true;
}
function traverseElement(el) {
if (!shouldTranslateEl(el)) return;
if (el.nodeType === Node.TEXT_NODE) {
translateElement(el);
return;
}
for (const child of el.childNodes) {
traverseElement(child);
}
}
function observeDOMChanges() {
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
traverseElement(node);
});
});
});
observer.observe(document.body, {
subtree: true,
childList: true,
characterData: true
});
}
function translatePage() {
traverseElement(document.body);
applyCSSOverrides();
}
function applyCSSOverrides() {
if (locales.css) {
for (const css of locales.css) {
if ($(css.selector).length > 0) {
if (css.key === '!html') {
$(css.selector).html(css.replacement);
} else {
$(css.selector).attr(css.key, css.replacement);
}
}
}
}
}
})();