您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
将 GitHub 页面翻译为中文。采用字典驱动,按页面细分,不改变页面功能;自动处理 PJAX/动态内容。
当前为
// ==UserScript== // @name GitHub 中文翻译增强 // @namespace https://github.com/SychO3/github-i18n-plugin // @version 1.0.0 // @description 将 GitHub 页面翻译为中文。采用字典驱动,按页面细分,不改变页面功能;自动处理 PJAX/动态内容。 // @author SychO // @match https://github.com/* // @match https://gist.github.com/* // @run-at document-idle // @license MIT // @grant GM.getResourceText // @grant GM_getResourceText // @resource zhCN https://raw.githubusercontent.com/SychO3/github-i18n-plugin/master/locales/zh-CN.json // ==/UserScript== (function () { 'use strict'; // --------------------------- // 外部字典加载(JSON 文件) // --------------------------- /** * 词典 JSON 结构: * { * "global": { "sign in": "登录(不可用)", ... }, * "repo": { ... }, * "issues_list": { ... }, * ... 其他页面键 ... * } */ const RESOURCE_NAME = 'zhCN'; let loadedDictionaries = {}; function parseDictionaryJson(text) { try { const json = JSON.parse(text); if (json && typeof json === 'object') return json; } catch (_) {} return {}; } async function loadDictionaries() { // 仅使用 @resource(GreasyFork/Tampermonkey 推荐方式) try { // 兼容 GM.getResourceText / GM_getResourceText // eslint-disable-next-line no-undef const gmGet = (typeof GM !== 'undefined' && typeof GM.getResourceText === 'function') ? GM.getResourceText : (typeof GM_getResourceText === 'function' ? GM_getResourceText : null); if (gmGet) { const maybe = gmGet(RESOURCE_NAME); const text = (maybe && typeof maybe.then === 'function') ? await maybe : maybe; if (typeof text === 'string' && text) { return parseDictionaryJson(text); } } } catch (_) {} // 未配置资源时兜底为空对象(不翻译) return {}; } // --------------------------- // 工具函数 // --------------------------- function normalizeKey(text) { if (typeof text !== 'string') return ''; return text.replace(/\s+/g, ' ').trim().toLowerCase(); } function detectPageKeyFallback() { const p = location.pathname; // 仓库首页,如 /owner/repo if (/^\/[\w.-]+\/[\w.-]+$/.test(p)) return 'repo'; // Issues 列表或详情 if (/^\/[\w.-]+\/[\w.-]+\/issues(\/.*)?$/.test(p)) { return /\/issues\/\d+/.test(p) ? 'issue_detail' : 'issues_list'; } // PR 列表或详情 if (/^\/[\w.-]+\/[\w.-]+\/(pull|pulls)(\/.*)?$/.test(p)) { return /\/pull\/(\d+)/.test(p) ? 'pr_detail' : 'pulls_list'; } // 文件浏览 if (/^\/[\w.-]+\/[\w.-]+\/(tree|blob)\//.test(p)) return 'file_view'; // Commits 列表/详情 if (/^\/[\w.-]+\/[\w.-]+\/commits/.test(p)) return 'commits_list'; if (/^\/[\w.-]+\/[\w.-]+\/commit\//.test(p)) return 'commit_detail'; // 搜索/通知 if (/^\/search/.test(p)) return 'search'; if (/^\/notifications/.test(p)) return 'notifications'; return 'global'; } function matchRouteByRegex(pattern, url) { try { const re = new RegExp(pattern); return re.test(url); } catch (_) { return false; } } function matchRouteByPrefix(prefix, url) { if (typeof prefix !== 'string') return false; return url.startsWith(prefix); } function resolvePageKeyByRoutes(url) { const routes = Array.isArray(loadedDictionaries.routes) ? loadedDictionaries.routes : []; for (const route of routes) { if (!route || typeof route !== 'object') continue; const type = route.type || 'regex'; const pattern = route.pattern || route.match || ''; const key = route.key || route.pageKey || ''; if (!pattern || !key) continue; let ok = false; if (type === 'prefix') ok = matchRouteByPrefix(pattern, url); else ok = matchRouteByRegex(pattern, url); if (ok) return key; } return null; } function detectActivePageKey() { const url = location.href; const mapped = resolvePageKeyByRoutes(url); if (mapped) return mapped; return detectPageKeyFallback(); } function buildDictionaryForPage(pageKey) { const merged = Object.create(null); const globalDict = loadedDictionaries.global || {}; for (const [k, v] of Object.entries(globalDict)) { merged[normalizeKey(k)] = v; } const pageDict = loadedDictionaries[pageKey] || {}; for (const [k, v] of Object.entries(pageDict)) { merged[normalizeKey(k)] = v; } return merged; } // 需要跳过翻译的容器选择器 const SKIP_CONTAINER_SELECTOR = [ 'pre', 'code', 'kbd', 'samp', 'var', 'script', 'style', 'noscript', 'svg', 'math', // Markdown/代码内容区域 '.markdown-body', '.blob-code', '.diff-code', '.js-blob-code-container' ].join(','); function isSkippable(node) { if (!node) return true; if (node.nodeType !== Node.TEXT_NODE) return true; const parent = node.parentElement; if (!parent) return true; if (parent.closest(SKIP_CONTAINER_SELECTOR)) return true; // 忽略很长的文本(段落类),避免误伤内容文本 const text = node.nodeValue || ''; const norm = normalizeKey(text); if (!norm) return true; if (norm.length > 80) return true; return false; } function translateTextNode(node, dict) { if (!node || node.nodeType !== Node.TEXT_NODE) return 0; const original = node.nodeValue || ''; const leading = (original.match(/^\s*/)?.[0]) || ''; const trailing = (original.match(/\s*$/)?.[0]) || ''; const core = original.slice(leading.length, original.length - trailing.length); const key = normalizeKey(core); if (!key) return 0; const replacement = dict[key]; if (!replacement) return 0; const next = leading + replacement + trailing; if (next !== original) { node.nodeValue = next; return 1; } return 0; } function translateInTree(root, dict) { if (!root || !dict) return 0; let replacedCount = 0; // 仅遍历文本节点 const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); const toProcess = []; while (walker.nextNode()) { const t = walker.currentNode; toProcess.push(t); } for (const textNode of toProcess) { if (isSkippable(textNode)) continue; replacedCount += translateTextNode(textNode, dict); } return replacedCount; } // --------------------------- // 事件与观察 // --------------------------- let currentPageKey = null; let currentDict = null; let scheduled = false; function applyTranslation(reason) { try { const pageKey = detectActivePageKey(); if (pageKey !== currentPageKey || !currentDict) { currentPageKey = pageKey; currentDict = buildDictionaryForPage(pageKey); } translateInTree(document.body, currentDict); } catch (e) { // eslint-disable-next-line no-console console.debug('[GH i18n] translate error:', e, 'reason =', reason); } } function scheduleTranslate(reason) { if (scheduled) return; scheduled = true; requestAnimationFrame(() => { scheduled = false; applyTranslation(reason); }); } // 观察 DOM 变化(处理懒加载和交互新增的节点) const observer = new MutationObserver((mutations) => { for (const m of mutations) { if (m.type === 'childList') { if (m.addedNodes && m.addedNodes.length) { scheduleTranslate('mutation:childList'); break; } } else if (m.type === 'characterData') { scheduleTranslate('mutation:char'); break; } } }); function startObserver() { observer.observe(document.documentElement, { subtree: true, childList: true, characterData: true }); } // 监听 PJAX/前端路由跳转 function hookHistory() { const origPush = history.pushState; const origReplace = history.replaceState; history.pushState = function () { const ret = origPush.apply(this, arguments); scheduleTranslate('history.pushState'); return ret; }; history.replaceState = function () { const ret = origReplace.apply(this, arguments); scheduleTranslate('history.replaceState'); return ret; }; window.addEventListener('popstate', () => scheduleTranslate('history.popstate')); // GitHub pjax 事件 document.addEventListener('pjax:end', () => scheduleTranslate('pjax:end')); } // 初始化 async function init() { hookHistory(); startObserver(); loadedDictionaries = await loadDictionaries(); applyTranslation('init'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true }); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址