GitHub 中文化插件

中文化 GitHub 界面的部分菜单及内容。原作者为楼教主(http://www.52cik.com/)。

  1. // ==UserScript==
  2. // @name GitHub 中文化插件
  3. // @namespace https://github.com/maboloshi/github-chinese
  4. // @description 中文化 GitHub 界面的部分菜单及内容。原作者为楼教主(http://www.52cik.com/)。
  5. // @copyright 2021, 沙漠之子 (https://maboloshi.github.io/Blog)
  6. // @icon https://github.githubassets.com/pinned-octocat.svg
  7. // @version 1.9.2-2025-03-09
  8. // @author 沙漠之子
  9. // @license GPL-3.0
  10. // @match https://github.com/*
  11. // @match https://skills.github.com/*
  12. // @match https://gist.github.com/*
  13. // @match https://www.githubstatus.com/*
  14. // @require https://greasyfork.org/scripts/435207-github-%E4%B8%AD%E6%96%87%E5%8C%96%E6%8F%92%E4%BB%B6-%E4%B8%AD%E6%96%87%E8%AF%8D%E5%BA%93%E8%A7%84%E5%88%99/code/GitHub%20%E4%B8%AD%E6%96%87%E5%8C%96%E6%8F%92%E4%BB%B6%20-%20%E4%B8%AD%E6%96%87%E8%AF%8D%E5%BA%93%E8%A7%84%E5%88%99.js?v1.9.2-2025-03-09
  15. // @run-at document-end
  16. // @grant GM_xmlhttpRequest
  17. // @grant GM_getValue
  18. // @grant GM_setValue
  19. // @grant GM_registerMenuCommand
  20. // @grant GM_unregisterMenuCommand
  21. // @grant GM_notification
  22. // @connect www.iflyrec.com
  23. // @supportURL https://github.com/maboloshi/github-chinese/issues
  24. // ==/UserScript==
  25.  
  26. (function (window, document, undefined) {
  27. 'use strict';
  28.  
  29. const lang = I18N.zh ? 'zh' : 'zh-CN'; // 设置默认语言
  30. let page;
  31. let enable_RegExp = GM_getValue("enable_RegExp", 1);
  32.  
  33. /**
  34. * watchUpdate 函数:监视页面变化,根据变化的节点进行翻译
  35. */
  36. function watchUpdate() {
  37. // 检测浏览器是否支持 MutationObserver
  38. const MutationObserver =
  39. window.MutationObserver ||
  40. window.WebKitMutationObserver ||
  41. window.MozMutationObserver;
  42.  
  43. // 获取当前页面的 URL
  44. const getCurrentURL = () => location.href;
  45. getCurrentURL.previousURL = getCurrentURL();
  46.  
  47. // 创建 MutationObserver 实例,监听 DOM 变化
  48. const observer = new MutationObserver((mutations, observer) => {
  49. const currentURL = getCurrentURL();
  50.  
  51. // 如果页面的 URL 发生变化
  52. if (currentURL !== getCurrentURL.previousURL) {
  53. getCurrentURL.previousURL = currentURL;
  54. page = getPage(); // 当页面地址发生变化时,更新全局变量 page
  55. console.log(`链接变化 page= ${page}`);
  56.  
  57. transTitle(); // 翻译页面标题
  58.  
  59. if (page) {
  60. setTimeout(() => {
  61. // 使用 CSS 选择器找到页面上的元素,并将其文本内容替换为预定义的翻译
  62. transBySelector();
  63. if (page === "repository") { //仓库简介翻译
  64. transDesc(".f4.my-3");
  65. } else if (page === "gist") { // Gist 简介翻译
  66. transDesc(".gist-content [itemprop='about']");
  67. }
  68. }, 500);
  69. }
  70. }
  71.  
  72. if (page) {
  73. // 使用 filter 方法对 mutations 数组进行筛选,
  74. // 返回 `节点增加、文本更新 或 属性更改的 mutation` 组成的新数组 filteredMutations。
  75. const filteredMutations = mutations.filter(mutation => mutation.addedNodes.length > 0 || mutation.type === 'attributes' || mutation.type === 'characterData');
  76.  
  77. // 处理每个变化
  78. filteredMutations.forEach(mutation => traverseNode(mutation.target));
  79. }
  80. });
  81.  
  82. // 配置 MutationObserver
  83. const config = {
  84. characterData: true,
  85. subtree: true,
  86. childList: true,
  87. attributeFilter: ['value', 'placeholder', 'aria-label', 'data-confirm'], // 仅观察特定属性变化
  88. };
  89.  
  90. // 开始观察 document.body 的变化
  91. observer.observe(document.body, config);
  92. }
  93.  
  94. /**
  95. * traverseNode 函数:遍历指定的节点,并对节点进行翻译。
  96. * @param {Node} node - 需要遍历的节点。
  97. */
  98. function traverseNode(node) {
  99. // 跳过忽略
  100. if (I18N.conf.reIgnoreId.test(node.id) ||
  101. I18N.conf.reIgnoreClass.test(node.className) ||
  102. I18N.conf.reIgnoreTag.includes(node.tagName) ||
  103. (node.getAttribute && I18N.conf.reIgnoreItemprop.test(node.getAttribute("itemprop")))
  104. ) {
  105. return;
  106. }
  107.  
  108. if (node.nodeType === Node.ELEMENT_NODE) { // 元素节点处理
  109.  
  110. // 翻译时间元素
  111. if (
  112. ["RELATIVE-TIME", "TIME-AGO", "TIME", "LOCAL-TIME"].includes(node.tagName)
  113. ) {
  114. if (node.shadowRoot) {
  115. transTimeElement(node.shadowRoot);
  116. watchTimeElement(node.shadowRoot);
  117. } else {
  118. transTimeElement(node);
  119. }
  120. return;
  121. }
  122.  
  123. // 元素节点属性翻译
  124. if (["INPUT", "TEXTAREA"].includes(node.tagName)) { // 输入框 按钮 文本域
  125. if (["button", "submit", "reset"].includes(node.type)) {
  126. if (node.hasAttribute('data-confirm')) { // 翻译 浏览器 提示对话框
  127. transElement(node, 'data-confirm', true);
  128. }
  129. transElement(node, 'value');
  130. } else {
  131. transElement(node, 'placeholder');
  132. }
  133. } else if (node.tagName === 'BUTTON') {
  134. if (node.hasAttribute('aria-label') && /tooltipped/.test(node.className)) {
  135. transElement(node, 'aria-label', true); // 翻译 浏览器 提示对话框
  136. }
  137. if (node.hasAttribute('title')) {
  138. transElement(node, 'title', true); // 翻译 浏览器 提示对话框
  139. }
  140. if (node.hasAttribute('data-confirm')) {
  141. transElement(node, 'data-confirm', true); // 翻译 浏览器 提示对话框 ok
  142. }
  143. if (node.hasAttribute('data-confirm-text')) {
  144. transElement(node, 'data-confirm-text', true); // 翻译 浏览器 提示对话框 ok
  145. }
  146. if (node.hasAttribute('data-confirm-cancel-text')) {
  147. transElement(node, 'data-confirm-cancel-text', true); // 取消按钮 提醒
  148. }
  149. if (node.hasAttribute('cancel-confirm-text')) {
  150. transElement(node, 'cancel-confirm-text', true); // 取消按钮 提醒
  151. }
  152. if (node.hasAttribute('data-disable-with')) { // 按钮等待提示
  153. transElement(node, 'data-disable-with', true);
  154. }
  155. } else if (node.tagName === 'OPTGROUP') { // 翻译 <optgroup> 的 label 属性
  156. transElement(node, 'label');
  157. } else if (/tooltipped/.test(node.className)) { // 仅当 元素存在'tooltipped'样式 aria-label 才起效果
  158. transElement(node, 'aria-label', true); // 带提示的元素,类似 tooltip 效果的
  159. } else if (node.tagName === 'A') {
  160. if (node.hasAttribute('title')) {
  161. transElement(node, 'title', true); // 翻译 浏览器 提示对话框
  162. }
  163. if (node.hasAttribute('data-hovercard-type')) {
  164. return; // 不翻译
  165. }
  166. }
  167.  
  168. let childNodes = node.childNodes;
  169. childNodes.forEach(traverseNode); // 遍历子节点
  170.  
  171. } else if (node.nodeType === Node.TEXT_NODE) { // 文本节点翻译
  172. if (node.length <= 500) { // 修复 许可证编辑框初始化载入内容被翻译
  173. transElement(node, 'data');
  174. }
  175. }
  176. }
  177.  
  178. /**
  179. * getPage 函数:获取当前页面的类型。
  180. * @returns {string|boolean} 当前页面的类型,如果无法确定类型,那么返回 false。
  181. */
  182. function getPage() {
  183.  
  184. // 站点,如 gist, developer, help 等,默认主站是 github
  185. const siteMapping = {
  186. 'gist.github.com': 'gist',
  187. 'www.githubstatus.com': 'status',
  188. 'skills.github.com': 'skills'
  189. };
  190. const site = siteMapping[location.hostname] || 'github'; // 站点
  191. const pathname = location.pathname; // 当前路径
  192.  
  193. // 是否登录
  194. const isLogin = document.body.classList.contains("logged-in");
  195.  
  196. // 用于确定 个人首页,组织首页,仓库页 然后做判断
  197. const analyticsLocation = (document.getElementsByName('analytics-location')[0] || {}).content || '';
  198. // 组织页
  199. const isOrganization = /\/<org-login>/.test(analyticsLocation) || /^\/(?:orgs|organizations)/.test(pathname);
  200. // 仓库页
  201. const isRepository = /\/<user-name>\/<repo-name>/.test(analyticsLocation);
  202.  
  203. // 优先匹配 body 的 class
  204. let page, t = document.body.className.match(I18N.conf.rePageClass);
  205. if (t) {
  206. if (t[1] === 'page-profile') {
  207. let matchResult = location.search.match(/tab=(\w+)/);
  208. if (matchResult) {
  209. page = 'page-profile/' + matchResult[1];
  210. } else {
  211. page = pathname.match(/\/(stars)/) ? 'page-profile/stars' : 'page-profile';
  212. }
  213. } else {
  214. page = t[1];
  215. }
  216. } else if (site === 'gist') { // Gist 站点
  217. page = 'gist';
  218. } else if (site === 'status') { // GitHub Status 页面
  219. page = 'status';
  220. } else if (site === 'skills') { // GitHub Skills 页面
  221. page = 'skills';
  222. } else if (pathname === '/' && site === 'github') { // github.com 首页
  223. page = isLogin ? 'page-dashboard' : 'homepage';
  224. } else if (isRepository) { // 仓库页
  225. t = pathname.match(I18N.conf.rePagePathRepo);
  226. page = t ? 'repository/' + t[1] : 'repository';
  227. } else if (isOrganization) { // 组织页
  228. t = pathname.match(I18N.conf.rePagePathOrg);
  229. page = t ? 'orgs/' + (t[1] || t.slice(-1)[0]) : 'orgs';
  230. } else {
  231. t = pathname.match(I18N.conf.rePagePath);
  232. page = t ? (t[1] || t.slice(-1)[0]) : false; // 取页面 key
  233. }
  234.  
  235. if (!page || !I18N[lang][page]) {
  236. console.log(`请注意对应 page ${page} 词库节点不存在`);
  237. page = false;
  238. }
  239. return page;
  240. }
  241.  
  242. /**
  243. * transTitle 函数:翻译页面标题
  244. */
  245. function transTitle() {
  246. let key = document.title; // 标题文本内容
  247. let str = I18N[lang]['title']['static'][key] || '';
  248. if (!str) {
  249. let res = I18N[lang]['title'].regexp || [];
  250. for (let [a, b] of res) {
  251. str = key.replace(a, b);
  252. if (str !== key) {
  253. break;
  254. }
  255. }
  256. }
  257. document.title = str;
  258. }
  259.  
  260. /**
  261. * transTimeElement 函数:翻译时间元素文本内容。
  262. * @param {Element} el - 需要翻译的元素。
  263. */
  264. function transTimeElement(el) {
  265. let key = el.childNodes.length > 0 ? el.lastChild.textContent : el.textContent;
  266. let res = I18N[lang]['public']['time-regexp']; // 时间正则规则
  267.  
  268. for (let [a, b] of res) {
  269. let str = key.replace(a, b);
  270. if (str !== key) {
  271. el.textContent = str;
  272. break;
  273. }
  274. }
  275. }
  276.  
  277. /**
  278. * watchTimeElement 函数:监视时间元素变化, 触发和调用时间元素翻译
  279. * @param {Element} el - 需要监视的元素。
  280. */
  281. function watchTimeElement(el) {
  282. const MutationObserver =
  283. window.MutationObserver ||
  284. window.WebKitMutationObserver ||
  285. window.MozMutationObserver;
  286.  
  287. new MutationObserver(mutations => {
  288. transTimeElement(mutations[0].addedNodes[0]);
  289. }).observe(el, {
  290. childList: true
  291. });
  292. }
  293.  
  294. /**
  295. * transElement 函数:翻译指定元素的文本内容或属性。
  296. * @param {Element} el - 需要翻译的元素。
  297. * @param {string} field - 需要翻译的文本内容或属性的名称。
  298. * @param {boolean} isAttr - 是否需要翻译属性。
  299. */
  300. function transElement(el, field, isAttr = false) {
  301. let text = isAttr ? el.getAttribute(field) : el[field]; // 需要翻译的文本
  302. let str = translateText(text); // 翻译后的文本
  303.  
  304. // 替换翻译后的内容
  305. if (str) {
  306. if (!isAttr) {
  307. el[field] = str;
  308. } else {
  309. el.setAttribute(field, str);
  310. }
  311. }
  312. }
  313.  
  314. /**
  315. * translateText 函数:翻译文本内容。
  316. * @param {string} text - 需要翻译的文本内容。
  317. * @returns {string|boolean} 翻译后的文本内容,如果没有找到对应的翻译,那么返回 false。
  318. */
  319. function translateText(text) { // 翻译
  320.  
  321. // 内容为空, 空白字符和或数字, 不存在英文字母和符号,. 跳过
  322. if (!isNaN(text) || !/[a-zA-Z,.]+/.test(text)) {
  323. return false;
  324. }
  325.  
  326. let _key = text.trim(); // 去除首尾空格的 key
  327. let _key_neat = _key.replace(/\xa0|[\s]+/g, ' ') // 去除多余空白字符(&nbsp; 空格 换行符)
  328.  
  329. let str = fetchTranslatedText(_key_neat); // 翻译已知页面 (局部优先)
  330.  
  331. if (str && str !== _key_neat) { // 已知页面翻译完成
  332. return text.replace(_key, str); // 替换原字符,保留首尾空白部分
  333. }
  334.  
  335. return false;
  336. }
  337.  
  338. /**
  339. * fetchTranslatedText 函数:从特定页面的词库中获得翻译文本内容。
  340. * @param {string} key - 需要翻译的文本内容。
  341. * @returns {string|boolean} 翻译后的文本内容,如果没有找到对应的翻译,那么返回 false。
  342. */
  343. function fetchTranslatedText(key) {
  344.  
  345. // 静态翻译
  346. let str = I18N[lang][page]['static'][key] || I18N[lang]['public']['static'][key]; // 默认翻译 公共部分
  347.  
  348. if (typeof str === 'string') {
  349. return str;
  350. }
  351.  
  352. // 正则翻译
  353. if (enable_RegExp) {
  354. let res = (I18N[lang][page].regexp || []).concat(I18N[lang]['public'].regexp || []); // 正则数组
  355.  
  356. for (let [a, b] of res) {
  357. str = key.replace(a, b);
  358. if (str !== key) {
  359. return str;
  360. }
  361. }
  362. }
  363.  
  364. return false; // 没有翻译条目
  365. }
  366.  
  367. /**
  368. * transDesc 函数:为指定的元素添加一个翻译按钮,并为该按钮添加点击事件。
  369. * @param {string} el - CSS选择器,用于选择需要添加翻译按钮的元素。
  370. */
  371. function transDesc(el) {
  372. // 使用 CSS 选择器选择元素
  373. let element = document.querySelector(el);
  374.  
  375. // 如果元素不存在 或者 translate-me 元素已存在,那么直接返回
  376. if (!element || document.getElementById('translate-me')) {
  377. return false;
  378. }
  379.  
  380. // 在元素后面插入一个翻译按钮
  381. const buttonHTML = `<div id='translate-me' style='color: rgb(27, 149, 224); font-size: small; cursor: pointer'>翻译</div>`;
  382. element.insertAdjacentHTML('afterend', buttonHTML);
  383. let button = element.nextSibling;
  384.  
  385. // 为翻译按钮添加点击事件
  386. button.addEventListener('click', () => {
  387. // 获取元素的文本内容
  388. const desc = element.textContent.trim();
  389.  
  390. // 如果文本内容为空,那么直接返回
  391. if (!desc) {
  392. return false;
  393. }
  394.  
  395. // 调用 translateDescText 函数进行翻译
  396. translateDescText(desc, text => {
  397. // 翻译完成后,隐藏翻译按钮,并在元素后面插入翻译结果
  398. button.style.display = "none";
  399. const translationHTML = `<span style='font-size: small'>由 <a target='_blank' style='color:rgb(27, 149, 224);' href='https://www.iflyrec.com/html/translate.html'>讯飞听见</a> 翻译👇</span><br/>${text}`;
  400. element.insertAdjacentHTML('afterend', translationHTML);
  401. });
  402. });
  403. }
  404.  
  405. /**
  406. * translateDescText 函数:将指定的文本发送到讯飞的翻译服务进行翻译。
  407. * @param {string} text - 需要翻译的文本。
  408. * @param {function} callback - 翻译完成后的回调函数,该函数接受一个参数,即翻译后的文本。
  409. */
  410. function translateDescText(text, callback) {
  411. // 使用 GM_xmlhttpRequest 函数发送 HTTP 请求
  412. GM_xmlhttpRequest({
  413. method: "POST", // 请求方法为 POST
  414. url: "https://www.iflyrec.com/TranslationService/v1/textTranslation", // 请求的 URL
  415. headers: { // 请求头
  416. 'Content-Type': 'application/json',
  417. 'Origin': 'https://www.iflyrec.com',
  418. },
  419. data: JSON.stringify({
  420. "from": "2",
  421. "to": "1",
  422. "contents": [{
  423. "text": text,
  424. "frontBlankLine": 0
  425. }]
  426. }), // 请求的数据
  427. responseType: "json", // 响应的数据类型为 JSON
  428. onload: (res) => {
  429. try {
  430. const { status, response } = res;
  431. const translatedText = (status === 200) ? response.biz[0].translateResult : "翻译失败";
  432. callback(translatedText);
  433. } catch (error) {
  434. console.error('翻译失败', error);
  435. callback("翻译失败");
  436. }
  437. },
  438. onerror: (error) => {
  439. console.error('网络请求失败', error);
  440. callback("网络请求失败");
  441. }
  442. });
  443. }
  444.  
  445. /**
  446. * transBySelector 函数:通过 CSS 选择器找到页面上的元素,并将其文本内容替换为预定义的翻译。
  447. */
  448. function transBySelector() {
  449. // 获取当前页面的翻译规则,如果没有找到,那么使用公共的翻译规则
  450. let res = (I18N[lang][page]?.selector || []).concat(I18N[lang]['public'].selector || []); // 数组
  451.  
  452. // 如果找到了翻译规则
  453. if (res.length > 0) {
  454. // 遍历每个翻译规则
  455. for (let [selector, translation] of res) {
  456. // 使用 CSS 选择器找到对应的元素
  457. let element = document.querySelector(selector)
  458. // 如果找到了元素,那么将其文本内容替换为翻译后的文本
  459. if (element) {
  460. element.textContent = translation;
  461. }
  462. }
  463. }
  464. }
  465.  
  466. function registerMenuCommand() {
  467. const toggleRegExp = () => {
  468. enable_RegExp = !enable_RegExp;
  469. GM_setValue("enable_RegExp", enable_RegExp);
  470. GM_notification(`已${enable_RegExp ? '开启' : '关闭'}正则功能`);
  471. if (enable_RegExp) {
  472. location.reload();
  473. }
  474. GM_unregisterMenuCommand(id);
  475. id = GM_registerMenuCommand(`${enable_RegExp ? '关闭' : '开启'}正则功能`, toggleRegExp);
  476. };
  477.  
  478. let id = GM_registerMenuCommand(`${enable_RegExp ? '关闭' : '开启'}正则功能`, toggleRegExp);
  479. }
  480.  
  481. /**
  482. * init 函数:初始化翻译功能。
  483. */
  484. function init() {
  485. // 获取当前页面的翻译规则
  486. page = getPage();
  487. console.log(`开始page= ${page}`);
  488.  
  489. // 翻译页面标题
  490. transTitle();
  491.  
  492. if (page) {
  493. // 立即翻译页面
  494. traverseNode(document.body);
  495.  
  496. setTimeout(() => {
  497. // 使用 CSS 选择器找到页面上的元素,并将其文本内容替换为预定义的翻译
  498. transBySelector();
  499. if (page === "repository") { //仓库简介翻译
  500. transDesc(".f4.my-3");
  501. } else if (page === "gist") { // Gist 简介翻译
  502. transDesc(".gist-content [itemprop='about']");
  503. }
  504. }, 100);
  505. }
  506. // 监视页面变化
  507. watchUpdate();
  508. }
  509.  
  510. // 执行初始化
  511. registerMenuCommand();
  512. init();
  513.  
  514. })(window, document);