1 // ==UserScript== 2 // @name GitHub 汉化插件 3 // @description 汉化 GitHub 界面的部分菜单及内容。 4 // @copyright 2016, 楼教主 (http://www.52cik.com/) 5 // @icon https://assets-cdn.github.com/pinned-octocat.svg 6 // @version 1.6.4 7 // @author 楼教主 8 // @license MIT 9 // @homepageURL https://github.com/52cik/github-hans 10 // @match http://*.github.com/* 11 // @match https://*.github.com/* 12 // @require https://52cik.github.io/github-hans/locals.js?v1.6.4 13 // @run-at document-end 14 // @grant none 15 // ==/UserScript== 16 17 (function (window, document, undefined) { 18 \'use strict\'; 19 20 var lang = \'zh\'; // 中文 21 22 // 2016-04-18 github 将 jquery 以 amd 加载,不暴露到全局了。 23 // var $ = require(\'github/jquery\')[\'default\']; 24 25 // 要翻译的页面 26 var page = getPage(); 27 28 transTitle(); // 页面标题翻译 29 timeElement(); // 时间节点翻译 30 // setTimeout(contributions, 100); // 贡献日历翻译 (日历是内嵌或ajax的, 所以基于回调事件处理) 31 walk(document.body); // 立即翻译页面 32 33 // 2017-03-19 github 屏蔽 require 改为 Promise 形式的 ghImport 34 define(\'github-hans-ajax\', [\'./jquery\'], function($) { 35 $(document).ajaxComplete(function () { 36 transTitle(); 37 walk(document.body); // ajax 请求后再次翻译页面 38 }); 39 }); 40 ghImport(\'github-hans-ajax\')[\'catch\'](function(e) { 41 setTimeout(function() { throw e }); 42 }); 43 44 /** 45 * 遍历节点 46 * 47 * @param {Element} node 节点 48 */ 49 function walk(node) { 50 var nodes = node.childNodes; 51 52 for (var i = 0, len = nodes.length; i < len; i++) { 53 var el = nodes[i]; 54 // todo 1. 修复多属性翻译问题; 2. 添加事件翻译, 如论预览信息; 55 56 if (el.nodeType === Node.ELEMENT_NODE) { // 元素节点处理 57 58 // 元素节点属性翻译 59 if (el.tagName === \'INPUT\' || el.tagName === \'TEXTAREA\') { // 输入框 按钮 文本域 60 if (el.type === \'button\' || el.type === \'submit\') { 61 transElement(el, \'value\'); 62 } else { 63 transElement(el, \'placeholder\'); 64 } 65 } else if (el.hasAttribute(\'aria-label\')) { // 带提示的元素,类似 tooltip 效果的 66 transElement(el, \'aria-label\', true); 67 68 if (el.hasAttribute(\'data-copied-hint\')) { // 复制成功提示 69 transElement(el.dataset, \'copiedHint\'); 70 } 71 } else if (el.tagName === \'OPTGROUP\') { // 翻译 <optgroup> 的 label 属性 72 transElement(el, \'label\'); 73 } 74 75 if (el.hasAttribute(\'data-disable-with\')) { // 按钮等待提示 76 transElement(el.dataset, \'disableWith\'); 77 } 78 79 // 跳过 readme, 文件列表, 代码显示 80 if (el.id !== \'readme\' && !I18N.conf.reIgnore.test(el.className)) { 81 walk(el); // 遍历子节点 82 } 83 } else if (el.nodeType === Node.TEXT_NODE) { // 文本节点翻译 84 transElement(el, \'data\'); 85 } 86 87 } 88 } 89 90 /** 91 * 获取翻译页面 92 */ 93 function getPage() { 94 // 先匹配 body 的 class 95 var page = document.body.className.match(I18N.conf.rePageClass); 96 97 if (!page) { // 扩展 url 匹配 98 page = location.href.match(I18N.conf.rePageUrl); 99 } 100 101 if (!page) { // 扩展 pathname 匹配 102 page = location.pathname.match(I18N.conf.rePagePath); 103 } 104 105 return page ? page[1] || \'homepage\' : false; // 取页面 key 106 } 107 108 /** 109 * 翻译页面标题 110 */ 111 function transTitle() { 112 var title = translate(document.title, \'title\'); 113 114 if (title === false) { // 无翻译则退出 115 return false; 116 } 117 118 document.title = title; 119 } 120 121 122 /** 123 * 翻译节点对应属性内容 124 * 125 * @param {object} el 对象 126 * @param {string} field 属性字段 127 * @param {boolean} isAttr 是否是 attr 属性 128 * 129 * @returns {boolean} 130 */ 131 function transElement(el, field, isAttr) { 132 var transText = false; // 翻译后的文本 133 134 if (isAttr === undefined) { // 非属性翻译 135 transText = translate(el[field], page); 136 } else { 137 transText = translate(el.getAttribute(field), page); 138 } 139 140 if (transText === false) { // 无翻译则退出 141 return false; 142 } 143 144 // 替换翻译后的内容 145 if (isAttr === undefined) { 146 el[field] = transText; 147 } else { 148 el.setAttribute(field, transText); 149 } 150 } 151 152 153 /** 154 * 翻译文本 155 * 156 * @param {string} text 待翻译字符串 157 * @param {string} page 页面字段 158 * 159 * @returns {string|boolean} 160 */ 161 function translate(text, page) { // 翻译 162 var str; 163 var _key = text.trim(); // 去除首尾空格的 key 164 var _key_neat = _key 165 .replace(/\xa0/g, \' \') // 替换 空格导致的 bug 166 .replace(/\s{2,}/g, \' \'); // 去除多余换行空格等字符,(试验测试阶段,有问题再恢复) 167 168 if (_key_neat === \'\') { 169 return false; 170 } // 内容为空不翻译 171 172 str = transPage(\'pubilc\', _key_neat); // 公共翻译 173 174 if (str !== false && str !== _key_neat) { // 公共翻译完成 175 str = transPage(\'pubilc\', str) || str; // 二次公共翻译(为了弥补正则部分翻译的情况) 176 return text.replace(_key, str); // 替换原字符,保留空白部分 177 } 178 179 if (page === false) { 180 return false; 181 } // 未知页面不翻译 182 183 str = transPage(page, _key_neat); // 翻译已知页面 184 if (str === false || str === \'\') { 185 return false; 186 } // 未知内容不翻译 187 188 str = transPage(\'pubilc\', str) || str; // 二次公共翻译(为了弥补正则部分翻译的情况) 189 return text.replace(_key, str); // 替换原字符,保留空白部分 190 } 191 192 193 /** 194 * 翻译页面内容 195 * 196 * @param {string} page 页面 197 * @param {string} key 待翻译内容 198 * 199 * @returns {string|boolean} 200 */ 201 function transPage(page, key) { 202 var str; // 翻译结果 203 var res; // 正则数组 204 205 // 静态翻译 206 str = I18N[lang][page][\'static\'][key]; 207 if (str) { 208 return str; 209 } 210 211 // 正则翻译 212 res = I18N[lang][page].regexp; 213 if (res) { 214 for (var i = 0, len = res.length; i < len; i++) { 215 str = key.replace(res[i][0], res[i][1]); 216 if (str !== key) { 217 return str; 218 } 219 } 220 } 221 222 return false; // 没有翻译条目 223 } 224 225 226 /** 227 * 时间节点翻译 228 */ 229 function timeElement() { 230 if (!window.RelativeTimeElement) { // 防止报错 231 return; 232 } 233 234 var RelativeTimeElement$getFormattedDate = RelativeTimeElement.prototype.getFormattedDate; 235 var TimeAgoElement$getFormattedDate = TimeAgoElement.prototype.getFormattedDate; 236 // var LocalTimeElement$getFormattedDate = LocalTimeElement.prototype.getFormattedDate; 237 238 var RelativeTime = function (str, el) { // 相对时间解析 239 if (/^on ([\w ]+)$/.test(str)) { 240 return \'于 \' + el.title.replace(/ .+$/, \'\'); 241 } 242 243 // 使用字典公共翻译的第二个正则翻译相对时间 244 var time_ago = I18N[lang].pubilc.regexp[1]; 245 return str.replace(time_ago[0], time_ago[1]); 246 }; 247 248 RelativeTimeElement.prototype.getFormattedDate = function () { 249 var str = RelativeTimeElement$getFormattedDate.call(this); 250 return RelativeTime(str, this); 251 }; 252 253 TimeAgoElement.prototype.getFormattedDate = function () { 254 var str = TimeAgoElement$getFormattedDate.call(this); 255 return RelativeTime(str, this); 256 }; 257 258 LocalTimeElement.prototype.getFormattedDate = function () { 259 return this.title.replace(/ .+$/, \'\'); 260 }; 261 262 // 遍历 time 元素进行翻译 263 // 2016-04-16 github 改版,不再用 time 标签了。 264 var times = document.querySelectorAll(\'time, relative-time, time-ago, local-time\'); 265 Array.prototype.forEach.call(times, function (el) { 266 if (el.getFormattedDate) { // 跳过未注册的 time 元素 267 el.textContent = el.getFormattedDate(); 268 } 269 }); 270 } 271 272 273 /** 274 * 贡献日历 基于事件翻译 275 */ 276 function contributions() { 277 var tip = document.getElementsByClassName(\'svg-tip-one-line\'); 278 279 // 等待 IncludeFragmentElement 元素加载完毕后绑定事件 280 // var observe = require(\'github/observe\').observe; 281 282 define(\'github/hans-contributions\', [\'./observe\'], function (observe) { 283 observe(".js-calendar-graph-svg", function () { 284 setTimeout(function () { // 延时绑定 mouseover 事件,否则没法翻译 285 var $calendar = $(\'.js-calendar-graph\'); 286 walk($calendar[0]); // 翻译日历部分 287 288 $calendar.on(\'mouseover\', \'.day\', function () { 289 if (tip.length === 0) { // 没有 tip 元素时退出防止报错 290 return true; 291 } 292 293 var data = $(this).data(); // 获取节点上的 data 294 var $tip = $(tip[0]); 295 296 $tip.html(data.count + \' 次贡献 \' + data.date); 297 298 var rect = this.getBoundingClientRect(); // 获取元素位置 299 var left = rect.left + window.pageXOffset - tip[0].offsetWidth / 2 + 5.5; 300 301 $tip.css(\'left\', left); 302 }); 303 }, 999); 304 }); 305 }); 306 307 ghImport(\'github/hans-contributions\')[\'catch\'](function(e) { 308 setTimeout(function() { throw e }); 309 }); 310 } 311 312 })(window, document);
转载于楼教主GitHub:https://github.com/52cik/github-hans/blob/gh-pages/main.js