NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name GitHub 汉化插件 (indiff)修改 // @namespace https://gitee.com/qwop/github-hans // @description 汉化 GitHub 界面的部分菜单及内容,请关注我的开源项目 github.com/indiff/qttabbar 。 // @copyright 2016, 楼教主 (http://www.52cik.com/) // @icon https://github.githubassets.com/pinned-octocat.svg // @version 1.7.9 // @author indiff // @license MIT // @homepageURL https://gitee.com/qwop/github-hans // @match http://*.github.com/* // @match https://*.github.com/* // @require https://gitee.com/qwop/github-hans/raw/github-chinese/locals.js?d=20220823 // @run-at document-end // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_notification // @connect www.githubs.cn // ==/UserScript== (function (window, document, undefined) { 'use strict'; var RegExp = GM_getValue("RegExp", 1); var lang = 'zh'; // 中文 // 要翻译的页面 var page = getPage(); transTitle(); // 页面标题翻译 transBySelector(); // Selector 翻译 traverseNode(document.body); // 立即翻译页面 watchUpdate(); // 翻译描述 transDesc(".f4.my-3"); //仓库简介翻译 transDesc(".gist-content [itemprop='about']"); // Gist 简介翻译 /** * 监听节点变化, 触发和调用翻译函数 * * 2021-10-07 11:28:30 * 使用原生API 代替 jQuery 的 `ajaxComplete`函数 */ function watchUpdate() { const m = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; let currentURL = document.URL; // 监视 BODY 变化 const observer = new m(function (mutations, observer) { /** * 仅翻译变更部分 不在全局匹配 * * 且仅监听: * 1. 节点增加 * 2. 节点属性的变化 * **/ if(document.URL !== currentURL) { currentURL = document.URL; page = getPage(); // 仅当, 页面地址发生变化时运行 更新全局变量 page // 目前先跟随 url transTitle(); // 标题翻译 transBySelector(); // Selector 翻译 } for(let mutation of mutations) { // for速度比forEach快 if (mutation.addedNodes.length > 0 || mutation.type === 'attributes') { // 仅当节点增加 或者属性更改 // TURBO-FRAME TURBO 框架处理 if (mutation.target.tagName === 'TURBO-FRAME' && mutation.target.src) { page = getPage(mutation.target.src); //获取 TURBO 框架 对应 page } traverseNode(mutation.target); } } }); const config = { subtree: true, childList: true, attributeFilter: ['value', 'placeholder', 'aria-label', 'data-confirm'], // 仅观察特定属性变化(试验测试阶段,有问题再恢复) , 'datetime' } observer.observe(document.body, config); // 监视最顶层,仅当新增 BODY 时,重新翻译 BODY,并再次监视 BODY 的更新 new m(function(mutations) { for(let mutation of mutations) { if (mutation.addedNodes.length > 0) { // 仅当节点增加 for (const node of mutation.addedNodes){ if (node.nodeName === 'BODY') { // 增加的节点为 BODY page = getPage(); transTitle(); // 页面标题翻译 transBySelector(); // Selector 翻译 traverseNode(document.body); // 立即翻译页面 observer.observe(document.body, config); // 再次监视 BODY return; } } } else if(mutation.type === 'attributes') { if (mutation.target.className === 'translated-ltr') { observer.disconnect(); } else { observer.observe(document.body, config); } } } }).observe(document.documentElement, { childList: true, attributeFilter: ['class'] }); } /** * 遍历节点 * * @param {Element} node 节点 */ function traverseNode(node) { // 跳过忽略 if (I18N.conf.reIgnoreId.test(node.id) || I18N.conf.reIgnoreClass.test(node.className) || I18N.conf.reIgnoreTag.test(node.tagName) || (node.getAttribute && I18N.conf.reIgnoreItemprop.test(node.getAttribute("itemprop"))) ) { return; } if (node.nodeType === Node.ELEMENT_NODE) { // 元素节点处理 // 翻译时间元素 if (node.tagName === 'RELATIVE-TIME' || node.tagName === 'TIME-AGO'|| node.tagName === 'TIME') { transTimeElement(node); return; } // 元素节点属性翻译 if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') { // 输入框 按钮 文本域 if (node.type === 'button' || node.type === 'submit' || node.type === 'reset') { if (node.hasAttribute('data-confirm')) { // 翻译 浏览器 提示对话框 transElement(node, 'data-confirm', true); } transElement(node, 'value'); } else { transElement(node, 'placeholder'); } } else if (node.tagName === 'BUTTON'){ if (node.hasAttribute('aria-label') && /tooltipped/.test(node.className)) { transElement(node, 'aria-label', true); // 翻译 浏览器 提示对话框 } if (node.hasAttribute('title')) { transElement(node, 'title', true); // 翻译 浏览器 提示对话框 } if (node.hasAttribute('data-confirm')) { transElement(node, 'data-confirm', true); // 翻译 浏览器 提示对话框 ok } if (node.hasAttribute('data-confirm-text')) { transElement(node, 'data-confirm-text', true); // 翻译 浏览器 提示对话框 ok } if (node.hasAttribute('data-confirm-cancel-text')) { transElement(node, 'data-confirm-cancel-text', true); // 取消按钮 提醒 } if (node.hasAttribute('cancel-confirm-text')) { transElement(node, 'cancel-confirm-text', true); // 取消按钮 提醒 } if (node.hasAttribute('data-disable-with')) { // 按钮等待提示 transElement(node.dataset, 'disableWith'); } } else if (node.tagName === 'OPTGROUP') { // 翻译 <optgroup> 的 label 属性 transElement(node, 'label'); } else if (/tooltipped/.test(node.className)) { // 仅当 元素存在'tooltipped'样式 aria-label 才起效果 transElement(node, 'aria-label', true); // 带提示的元素,类似 tooltip 效果的 } if (node.childNodes.length >0) { for (const child of node.childNodes) { traverseNode(child); // 遍历子节点 } } } else if (node.nodeType === Node.TEXT_NODE) { // 文本节点翻译 if (node.length <= 500){ // 修复 许可证编辑框初始化载入内容被翻译 transElement(node, 'data'); } } } /** * 获取翻译页面 * * @param {string} TURBO 框架 src 地址 * * 2021-10-07 11:48:50 * 参考 v2.0 中规则 */ function getPage(src="") { var Location = location; var Document = document; // 解析 TURBO 框架 if(src){ Location = new URL(src); GM_xmlhttpRequest({ method: "GET", headers: {"Accept": "text/html, application/xhtml+xml"}, url: src, responseType: "document", // synchronous: true, onload: function(res) { if (res.status === 200) { Document = res.response; } else { Document = false; } } }); if (!Document) { return false; } } // 站点,如 gist, developer, help 等,默认主站是 github const site = Location.host.replace(/\.?github\.com$/, '') || 'github'; // 站点 const pathname = Location.pathname; // 当前路径 const isLogin = /logged-in/.test(Document.body.className); // 是否登录 // 用于确定 个人首页,组织首页,仓库页 然后做判断 const analyticsLocation = (Document.getElementsByName('analytics-location')[0] || 0).content || ''; //const isProfile = analyticsLocation === '/<user-name>'; // 仅个人首页 其标签页识别不了 优先使用Class 过滤 // 如 maboloshi?tab=repositories 等 const isOrganization = /\/<org-login>/.test(analyticsLocation); // 组织页 const isRepository = /\/<user-name>\/<repo-name>/.test(analyticsLocation); // 仓库页 // 优先匹配 body 的 class let page = Document.body.className.match(I18N.conf.rePageClass); if (page) { return page[1]; } if (site === 'gist') { // Gist 站点 return 'gist'; } if (pathname === '/' && site === 'github') { // github.com 首页 return isLogin ? 'page-dashboard' : 'homepage'; } //登录 或 未登录 // 仅个人首页 其标签页识别不了 优先使用 Class 过滤(/page-profile/) // if (isProfile) { // 个人首页 // return 'page-profile'; // } if (isRepository) { // 仓库页 let t = pathname.match(I18N.conf.rePagePathRepo); return t ? 'repository/'+t[1] : 'repository'; } if (isOrganization) { // 组织页 let t = pathname.match(I18N.conf.rePagePathOrg); return t ? 'orgs/'+t[1] : 'orgs'; } // 扩展 pathname 匹配 page = pathname.match(I18N.conf.rePagePath); return page ? page[1] : false; // 取页面 key } /** * 翻译页面标题 */ function transTitle() { let str; // 翻译结果 let key = document.title; // 静态翻译 str = I18N[lang]['title']['static'][key]; if (str) { document.title = str; return; } let res = I18N[lang]['title'].regexp; // 正则标题 for (let [a, b] of res) { str = key.replace(a, b); if (str !== key) { document.title = str; break; } } } /** * 时间元素翻译 * * @param {Element} node 节点 */ function transTimeElement(el) { let str; // 翻译结果 let key = el.textContent; let res = I18N[lang].pubilc.regexp; for (let i = 0; i < 3; i++) { // 公共正则中时间规则 str= key.replace(res[i][0], res[i][1]); if (str !== key) { el.textContent = str; break; } } } /** * 翻译节点对应属性内容 * * @param {object} el 对象 * @param {string} field 属性字段 * @param {boolean} isAttr 是否是 attr 属性 * * @returns {boolean} */ function transElement(el, field, isAttr=false) { let str; // 翻译后的文本 if (!isAttr) { // 非属性翻译 str = translate(el[field], page); } else { str = translate(el.getAttribute(field), page); } if (!str) { // 无翻译则退出 return false; } // 替换翻译后的内容 if (!isAttr) { el[field] = str; } else { el.setAttribute(field, str); } } /** * 翻译文本 * * @param {string} text 待翻译字符串 * @param {string} page 页面字段 * * @returns {string|boolean} */ function translate(text, page) { // 翻译 // 内容为空, 空白字符和或数字, 不存在英文字母和符号,. 跳过 if (!isNaN(text) || !/[a-zA-Z,.]+/.test(text)) { return false; } let str; let _key = text.trim(); // 去除首尾空格的 key let _key_neat = _key.replace(/\xa0|[\s]+/g, ' ') // 去除多余空白字符( 空格 换行符) if (page) { str = transPage(page, _key_neat); // 翻译已知页面 (局部优先) } // 未知页面不翻译 if (str && str !== _key_neat) { // 已知页面翻译完成 return text.replace(_key, str); // 替换原字符,保留首尾空白部分 } return false; } /** * 翻译页面内容 * * @param {string} page 页面 * @param {string} key 待翻译内容 * * @returns {string|boolean} */ function transPage(page, key) { let str; // 翻译结果 // 静态翻译 str = I18N[lang][page]['static'][key] || I18N[lang]['pubilc']['static'][key]; // 默认翻译 公共部分 if (typeof str === 'string') { return str; } // 正则翻译 if (RegExp){ let res = I18N[lang][page].regexp; // 正则数组 res.push(...I18N[lang]['pubilc'].regexp); // 追加公共正则 es6 if (res) { for (let [a, b] of res) { str = key.replace(a, b); if (str !== key) { return str; } } } } return false; // 没有翻译条目 } /** * 翻译描述 * * @param {string} JS 选择器 * * 2021-10-06 16:41:54 * 来自:k1995/github-i18n-plugin * 改写为原生代码 */ function transDesc(el) { let element = document.querySelector(el); if (!element) { return false; } element.insertAdjacentHTML('afterend', "<div id='translate-me' style='color: rgb(27, 149, 224); font-size: small; cursor: pointer'>翻译</div>"); let translate_me = document.getElementById('translate-me'); translate_me.onclick = function() { // get description text const desc = element.textContent.trim(); if(!desc) { return false; } GM_xmlhttpRequest({ method: "GET", url: `https://www.githubs.cn/translate?q=`+ encodeURIComponent(desc), onload: function(res) { if (res.status === 200) { translate_me.style.display="none"; // render result const text = res.responseText; element.insertAdjacentHTML('afterend', "<span style='font-size: small'>由 <a target='_blank' style='color:rgb(27, 149, 224);' href='https://www.githubs.cn'>GitHub中文社区</a> 翻译👇</span><br/>"+text); } else { alert("翻译失败"); } } }); }; } /** * js原生选择器 翻译元素 * * @param {string} JS 选择器或 CSS 选择器 * * 2022-02-04 19:46:44 * 灵感参考自:k1995/github-i18n-plugin */ function transBySelector() { let res = I18N[lang].selector; // 数组 if (res) { for (let [a, b] of res) { let element = document.querySelector(a) if (element) { element.textContent = b; } else if (document.getElementsByClassName(a).length > 0) { document.getElementsByClassName(a)[0].textContent = b; } } } } GM_registerMenuCommand("正则切换", () => { if (RegExp){ GM_setValue("RegExp", 0); RegExp = 0; GM_notification("已关闭正则功能"); } else { GM_setValue("RegExp", 1); GM_notification("已开启正则功能"); location.reload(); } }) })(window, document);