GitHub汉化插件

GitHub汉化插件,包含人机翻译

当前为 2023-07-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Internationalization
  3. // @name:zh-CN GitHub汉化插件
  4. // @name:ja GitHub日本語
  5. // @namespace https://github.com/k1995/github-i18n-plugin/
  6. // @version 0.25
  7. // @description Translate GitHub.com
  8. // @description:zh GitHub汉化插件,包含人机翻译
  9. // @description:zh-CN GitHub汉化插件,包含人机翻译
  10. // @description:ja GitHub日本語プラグイン
  11. // @author k1995
  12. // @match https://github.com/*
  13. // @match https://gist.github.com/*
  14. // @grant GM_xmlhttpRequest
  15. // @grant GM_getResourceText
  16. // @resource zh-CN https://www.githubs.cn/raw-githubusercontent/k1995/github-i18n-plugin/master/locales/zh-CN.json?v=20220131
  17. // @resource ja https://www.githubs.cn/raw-githubusercontent/k1995/github-i18n-plugin/master/locales/ja.json
  18. // @require https://cdn.staticfile.org/timeago.js/4.0.2/timeago.min.js
  19. // @require https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js
  20. // @license MIT
  21. // ==/UserScript==
  22.  
  23. (function() {
  24. 'use strict';
  25.  
  26. const SUPPORT_LANG = ["zh-CN", "ja"];
  27. const lang = (navigator.language || navigator.userLanguage);
  28. const locales = getLocales(lang)
  29.  
  30. translateByCssSelector();
  31. traverseElement(document.body);
  32. watchUpdate();
  33.  
  34. // 翻译描述
  35. if(window.location.pathname.split('/').length == 3) {
  36. translateDesc(".repository-content .f4"); //仓库简介翻译
  37. translateDesc(".gist-content [itemprop='about']"); // Gist 简介翻译
  38. }
  39.  
  40.  
  41. function getLocales(lang) {
  42. if(lang.startsWith("zh")) { // zh zh-TW --> zh-CN
  43. lang = "zh-CN";
  44. }
  45. if(SUPPORT_LANG.includes(lang)) {
  46. return JSON.parse(GM_getResourceText(lang));
  47. }
  48. return {
  49. css: [],
  50. dict: {}
  51. };
  52. }
  53.  
  54. function translateRelativeTimeEl(el) {
  55. const datetime = $(el).attr('datetime');
  56. //$(el).text(timeago.format(datetime, lang.replace('-', '_')));
  57. }
  58.  
  59. function translateElement(el) {
  60. // Get the text field name
  61. let k;
  62. if(el.tagName === "INPUT") {
  63. if (el.type === 'button' || el.type === 'submit') {
  64. k = 'value';
  65. } else {
  66. k = 'placeholder';
  67. }
  68. } else {
  69. k = 'data';
  70. }
  71.  
  72. if (isNaN(el[k])){
  73. const txtSrc = el[k].trim();
  74. const key = txtSrc.toLowerCase()
  75. .replace(/\xa0/g, ' ') // replace ' '
  76. .replace(/\s{2,}/g, ' ');
  77. if (locales.dict[key]) {
  78. el[k] = el[k].replace(txtSrc, locales.dict[key])
  79. }
  80. }
  81. translateElementAriaLabel(el)
  82. }
  83.  
  84. function translateElementAriaLabel(el) {
  85. if (el.ariaLabel) {
  86. const k = 'ariaLabel'
  87. const txtSrc = el[k].trim();
  88. const key = txtSrc.toLowerCase()
  89. .replace(/\xa0/g, ' ') // replace ' '
  90. .replace(/\s{2,}/g, ' ');
  91. if (locales.dict[key]) {
  92. el[k] = el[k].replace(txtSrc, locales.dict[key])
  93. }
  94. }
  95. }
  96.  
  97. function shouldTranslateEl(el) {
  98. const blockIds = [
  99. "readme",
  100. "file-name-editor-breadcrumb", "StickyHeader" // fix repo详情页文件路径breadcrumb
  101. ];
  102. const blockClass = [
  103. "CodeMirror",
  104. "js-navigation-container", // 过滤文件目录
  105. "blob-code",
  106. "topic-tag", // 过滤标签,
  107. // "text-normal", // 过滤repo name, 复现:https://github.com/search?q=explore
  108. "repo-list",//过滤搜索结果项目,解决"text-normal"导致的有些文字不翻译的问题,搜索结果以后可以考虑单独翻译
  109. "js-path-segment","final-path", //过滤目录,文件位置栏
  110. "markdown-body", // 过滤wiki页面,
  111. "search-input-container", //搜索框
  112. "search-match", //fix搜索结果页,repo name被翻译
  113. "cm-editor", //代码编辑框
  114. "PRIVATE_TreeView-item", // 文件树
  115. ];
  116. const blockTags = ["CODE", "SCRIPT", "LINK", "IMG", "svg", "TABLE", "ARTICLE", "PRE"];
  117. const blockItemprops = ["name"];
  118.  
  119. if (blockTags.includes(el.tagName)) {
  120. return false;
  121. }
  122.  
  123. if (el.id && blockIds.includes(el.id)) {
  124. return false;
  125. }
  126.  
  127. if (el.classList) {
  128. for (let clazz of blockClass) {
  129. if (el.classList.contains(clazz)) {
  130. return false;
  131. }
  132. }
  133. }
  134.  
  135. if (el.getAttribute) {
  136. let itemprops = el.getAttribute("itemprop");
  137. if (itemprops) {
  138. itemprops = itemprops.split(" ");
  139. for (let itemprop of itemprops) {
  140. if (blockItemprops.includes(itemprop)) {
  141. return false;
  142. }
  143. }
  144. }
  145. }
  146.  
  147. return true;
  148. }
  149.  
  150. function traverseElement(el) {
  151. translateElementAriaLabel(el)
  152. if (!shouldTranslateEl(el)) {
  153. return
  154. }
  155.  
  156. if (el.childNodes.length === 0) {
  157. if (el.nodeType === Node.TEXT_NODE) {
  158. translateElement(el);
  159. return;
  160. }
  161. else if(el.nodeType === Node.ELEMENT_NODE) {
  162. if (el.tagName === "INPUT") {
  163. translateElement(el);
  164. return;
  165. }
  166. }
  167. }
  168.  
  169. for (const child of el.childNodes) {
  170. if (["RELATIVE-TIME", "TIME-AGO"].includes(el.tagName)) {
  171. translateRelativeTimeEl(el);
  172. return;
  173. }
  174.  
  175. if (child.nodeType === Node.TEXT_NODE) {
  176. translateElement(child);
  177. }
  178. else if(child.nodeType === Node.ELEMENT_NODE) {
  179. if (child.tagName === "INPUT") {
  180. translateElement(child);
  181. } else {
  182. traverseElement(child);
  183. }
  184. } else {
  185. // pass
  186. }
  187. }
  188. }
  189.  
  190. function watchUpdate() {
  191. const m = window.MutationObserver || window.WebKitMutationObserver;
  192. const observer = new m(function (mutations, observer) {
  193. var reTrans = false;
  194. for(let mutationRecord of mutations) {
  195. if (mutationRecord.addedNodes || mutationRecord.type === 'attributes') {
  196. reTrans = true;
  197. // traverseElement(mutationRecord.target);
  198. }
  199. }
  200. if(reTrans) {
  201. traverseElement(document.body);
  202. }
  203. });
  204.  
  205. observer.observe(document.body, {
  206. subtree: true,
  207. characterData: true,
  208. childList: true,
  209. attributeFilter: ['value', 'placeholder', 'aria-label', 'data', 'data-confirm'], // 仅观察特定属性变化(试验测试阶段,有问题再恢复)
  210. });
  211. }
  212.  
  213. // translate "about"
  214. function translateDesc(el) {
  215. $(el).append("<br/>");
  216. $(el).append("<a id='translate-me' href='#' style='color:rgb(27, 149, 224);font-size: small'>翻译</a>");
  217. $("#translate-me").click(function() {
  218. // get description text
  219. const desc = $(el)
  220. .clone()
  221. .children()
  222. .remove()
  223. .end()
  224. .text()
  225. .trim();
  226.  
  227. if(!desc) {
  228. return;
  229. }
  230.  
  231. GM_xmlhttpRequest({
  232. method: "GET",
  233. url: `https://www.githubs.cn/translate?q=`+ encodeURIComponent(desc),
  234. onload: function(res) {
  235. if (res.status === 200) {
  236. $("#translate-me").hide();
  237. // render result
  238. const text = res.responseText;
  239. $(el).append("<span style='font-size: small'>由 <a target='_blank' style='color:rgb(27, 149, 224);' href='https://www.githubs.cn'>GitHub中文社区</a> 翻译👇</span>");
  240. $(el).append("<br/>");
  241. $(el).append(text);
  242. } else {
  243. alert("翻译失败");
  244. }
  245. }
  246. });
  247. });
  248. }
  249.  
  250. function translateByCssSelector() {
  251. if(locales.css) {
  252. for(var css of locales.css) {
  253. if($(css.selector).length > 0) {
  254. if(css.key === '!html') {
  255. $(css.selector).html(css.replacement);
  256. } else {
  257. $(css.selector).attr(css.key, css.replacement);
  258. }
  259. }
  260. }
  261. }
  262. }
  263. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址