GitHub Internationalization

Translate GitHub.com

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

QingJ © 2025

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