Skip to content

Instantly share code, notes, and snippets.

@Backsoon0
Last active May 24, 2026 04:13
Show Gist options
  • Select an option

  • Save Backsoon0/6d9ea3def62b61e5796f10efde6528d1 to your computer and use it in GitHub Desktop.

Select an option

Save Backsoon0/6d9ea3def62b61e5796f10efde6528d1 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name OpenCC 繁简转换器
// @name:en OpenCC Chinese Converter
// @namespace https://github.com/Backsoon0
// @version 1.1
// @description 多向繁简转换工具:支持繁→简 / 简→繁 / 短语转换,可通过菜单随时切换,偏好自动保存。基于 opencc-js 1.3.1。
// @description:en Multi-directional Chinese converter: Traditional↔Simplified with phrase support. Switch modes via menu, preferences auto-saved. Powered by opencc-js 1.3.1.
// @author Backsoon0
// @license MIT
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/full.js
// @downloadURL https://gist.github.com/Backsoon0/6d9ea3def62b61e5796f10efde6528d1/raw/OpenCC%20%E7%B9%81%E7%AE%80%E8%BD%AC%E6%8D%A2%E5%99%A8.user.js
// @updateURL https://gist.github.com/Backsoon0/6d9ea3def62b61e5796f10efde6528d1/raw/OpenCC%20%E7%B9%81%E7%AE%80%E8%BD%AC%E6%8D%A2%E5%99%A8.user.js
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
GM_addStyle('#opencc-overlay{all:initial;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:2147483647;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}#opencc-dialog{background:#fff;border-radius:12px;padding:24px;min-width:280px;box-shadow:0 8px 32px rgba(0,0,0,0.2);color:#333}.opencc-title{font-size:18px;font-weight:600;margin-bottom:16px;text-align:center;user-select:none}.opencc-option{padding:10px 16px;margin:4px 0;border-radius:8px;cursor:pointer;font-size:15px;color:#555;user-select:none;transition:background 0.15s}.opencc-option:hover{background:#f0f4ff}.opencc-option.opencc-active{background:#e8f0fe;color:#1a73e8;font-weight:600}.opencc-close{display:block;margin:16px auto 0;padding:8px 32px;border:1px solid #ddd;border-radius:6px;background:#f8f8f8;cursor:pointer;font-size:14px;color:#666}.opencc-close:hover{background:#eee}');
// 检查 OpenCC 是否成功加载
if (typeof OpenCC === 'undefined') {
console.error('OpenCC-js 加载失败。');
return;
}
// 支持的语言对
const langOptions = [
{ label: '繁 (台湾)→简', from: 'tw', to: 'cn' },
{ label: '繁 (香港)→简', from: 'hk', to: 'cn' },
{ label: '简→繁 (台湾)', from: 'cn', to: 'tw' },
{ label: '简→繁 (香港)', from: 'cn', to: 'hk' },
{ label: '繁 (台湾+短语)→简', from: 'twp', to: 'cn' },
];
let currentFrom, currentTo, converter;
function initConverter(from, to) {
currentFrom = from;
currentTo = to;
converter = OpenCC.Converter({ from, to });
}
initConverter(GM_getValue('opencc_from', 'tw'), GM_getValue('opencc_to', 'cn'));
// 定义需要忽略的 HTML 标签,防止破坏网页原有功能或修改代码
const ignoreTags = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'CODE', 'PRE']);
// 检查节点的父元素和祖先链是否需要跳过转换
function isNodeSkippable(node) {
let el = node.nodeType === Node.TEXT_NODE ? node.parentNode : node;
while (el && el.nodeType === Node.ELEMENT_NODE) {
if (ignoreTags.has(el.tagName.toUpperCase())) return true;
if (el.isContentEditable) return true;
if (el.classList && el.classList.contains('ignore-opencc')) return true;
if (el.hasAttribute('data-opencc-converted')) return true;
el = el.parentNode;
}
return false;
}
// 核心转换函数:遍历 DOM 树
function convertNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
if (isNodeSkippable(node)) return;
const text = node.nodeValue;
// 简单正则判断:如果包含非 ASCII 字符再进行转换,避免无意义的性能损耗
if (/[^\x00-\xff]/.test(text)) {
node.nodeValue = converter(text);
if (node.parentNode) node.parentNode.setAttribute('data-opencc-converted', '');
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (ignoreTags.has(node.tagName.toUpperCase())) {
return; // 跳过输入框、代码块等特定标签
}
// 跳过 contenteditable 元素(富文本编辑器),避免干扰用户输入
if (node.isContentEditable) {
return;
}
// 支持 opencc-js 的 ignore-opencc CSS 类,允许网页主动标记不转换的区域
if (node.classList && node.classList.contains('ignore-opencc')) {
return;
}
// 转换 HTML 属性中的占位符和悬浮提示
if (node.hasAttribute('placeholder')) {
node.setAttribute('placeholder', converter(node.getAttribute('placeholder')));
}
if (node.hasAttribute('title')) {
node.setAttribute('title', converter(node.getAttribute('title')));
}
// 递归遍历所有子节点
node.childNodes.forEach(convertNode);
}
}
// 1. 网页首次加载时,转换现有的内容和网页标题
convertNode(document.body);
if (document.title) {
document.title = converter(document.title);
}
// 3. 获取当前模式标签
function currentLabel() {
const opt = langOptions.find(o => o.from === currentFrom && o.to === currentTo);
return opt ? opt.label : currentFrom + '→' + currentTo;
}
// 4. 切换转换模式后全页重新转换
function applyLanguage(from, to) {
GM_setValue('opencc_from', from);
GM_setValue('opencc_to', to);
initConverter(from, to);
observer.disconnect();
convertNode(document.body);
if (document.title) document.title = converter(document.title);
startObserving();
}
// 5. 创建转换模式选择弹窗
function showSettingsDialog() {
const existing = document.getElementById('opencc-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.id = 'opencc-overlay';
overlay.addEventListener('click', function(e) {
if (e.target === overlay) overlay.remove();
});
const dialog = document.createElement('div');
dialog.id = 'opencc-dialog';
dialog.innerHTML = '<div class="opencc-title">选择转换模式</div>';
langOptions.forEach(function(opt) {
const isActive = currentFrom === opt.from && currentTo === opt.to;
const item = document.createElement('div');
item.className = 'opencc-option' + (isActive ? ' opencc-active' : '');
item.textContent = (isActive ? '● ' : ' ') + opt.label;
item.addEventListener('click', function() {
applyLanguage(opt.from, opt.to);
const opts = dialog.querySelectorAll('.opencc-option');
langOptions.forEach(function(o, i) {
const act = currentFrom === o.from && currentTo === o.to;
opts[i].className = 'opencc-option' + (act ? ' opencc-active' : '');
opts[i].textContent = (act ? '● ' : ' ') + o.label;
});
});
dialog.appendChild(item);
});
const closeBtn = document.createElement('button');
closeBtn.className = 'opencc-close';
closeBtn.textContent = '关闭';
closeBtn.addEventListener('click', function() { overlay.remove(); });
dialog.appendChild(closeBtn);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
}
// 6. 注册原生菜单
GM_registerMenuCommand('切换转换模式...', showSettingsDialog);
// 4. 使用 MutationObserver 监听动态加载的新内容
const observer = new MutationObserver((mutations) => {
// 暂时断开监听,防止我们在修改 DOM 时触发死循环
observer.disconnect();
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
convertNode(node);
});
} else if (mutation.type === 'characterData') {
if (isNodeSkippable(mutation.target)) return;
const text = mutation.target.nodeValue;
if (/[^\x00-\xff]/.test(text)) {
mutation.target.nodeValue = converter(text);
if (mutation.target.parentNode) mutation.target.parentNode.setAttribute('data-opencc-converted', '');
}
}
});
// 转换完毕,恢复监听
startObserving();
});
function startObserving() {
observer.observe(document.body, {
childList: true, // 监听子节点的变动
subtree: true, // 监听所有后代节点
characterData: true // 监听文本节点内容的变动
});
}
startObserving();
})();
@Backsoon0
Copy link
Copy Markdown
Author

OpenCC 繁简转换器

基于 网页繁体转简体 (OpenCC)(作者 mayunqing1230)的二改增强版。

与原版的对比

继承的原版特性

  • 精准词库转换:基于 opencc-js,支持字形转换 + 符合语言习惯的词汇转换(如"軟體"→"软件")
  • 动态内容支持:内置 MutationObserver,对 SPA、无限滚动等动态加载内容自动生效
  • 智能黑名单:自动跳过 <script><style><input><textarea><code><pre> 标签
  • 本地处理,保护隐私:全部转换在浏览器本地完成,不上传数据

增强特性

特性 原版 本版
opencc-js 版本 1.0.5 1.3.1
转换方向 仅繁→简 (tw→cn) 5 组:繁→简 / 简→繁,覆盖台湾、香港
切换方式 无,修改代码 菜单弹窗:点击 Tampermonkey 菜单即可切换
偏好保存 不支持 自动持久化,刷新页面后保持上次选择
富文本编辑器保护 自动跳过 contenteditable 元素,不干扰打字
动态文本安全 characterData 无条件转换 检查父节点/祖先链再决定是否转换
ignore-opencc 支持 支持 ignore-opencc CSS 类标记跳过区域
placeholder / title 转换 自动转换 HTML 属性中的繁体文本

支持的转换模式

  • 繁 (台湾)→简
  • 繁 (香港)→简
  • 简→繁 (台湾)
  • 简→繁 (香港)
  • 繁 (台湾+短语)→简

使用方式

  1. 安装 Tampermonkey 等用户脚本管理器
  2. 导入本脚本
  3. 点击 Tampermonkey 菜单中的「切换转换模式...」打开设置弹窗
  4. 选择所需的转换方向,即时生效

许可证

MIT,与原始项目保持一致。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment