Translate GitHub.com
// ==UserScript== // @name GitHub Internationalization // @name:zh-CN GitHub汉化插件 // @name:ja GitHub日本語 // @namespace https://github.com/k1995/github-i18n-plugin/ // @version 0.30 // @description Translate GitHub.com // @description:zh GitHub汉化插件,包含人机翻译 // @description:zh-CN GitHub汉化插件,包含人机翻译 // @description:ja GitHub日本語プラグイン // @author k1995 // @match https://github.com/* // @match https://gist.github.com/* // @grant GM_xmlhttpRequest // @grant GM_getResourceText // @resource zh-CN https://www.github-zh.com/raw-githubusercontent/k1995/github-i18n-plugin/master/locales/zh-CN.json?v=20240617 // @resource ja https://www.github-zh.com/raw-githubusercontent/k1995/github-i18n-plugin/master/locales/ja.json // @require https://cdn.staticfile.org/timeago.js/4.0.2/timeago.min.js // @require https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js // @license MIT // ==/UserScript== (function() { 'use strict'; const SUPPORT_LANG = ["zh-CN", "ja"]; const lang = (navigator.language || navigator.userLanguage); const locales = getLocales(lang) translateByCssSelector(); translateTime(); traverseElement(document.body); watchUpdate(); // 翻译描述 if(window.location.pathname.split('/').length == 3) { translateDesc(".repository-content .f4"); //仓库简介翻译 // translateDesc(".gist-content [itemprop='about']"); // Gist 简介翻译 } function getLocales(lang) { if(lang.startsWith("zh")) { // zh zh-TW --> zh-CN lang = "zh-CN"; } if(SUPPORT_LANG.includes(lang)) { return JSON.parse(GM_getResourceText(lang)); } return { css: [], dict: {} }; } function translateRelativeTimeEl(el) { const datetime = $(el).attr('datetime'); let humanTime = timeago.format(datetime, lang.replace('-', '_')); if(el.shadowRoot) { el.shadowRoot.textContent = humanTime; } else { el.textContent = humanTime; } } function translateElement(el) { // Get the text field name let k; if(el.tagName === "INPUT") { if (el.type === 'button' || el.type === 'submit') { k = 'value'; } else { k = 'placeholder'; } } else { k = 'data'; } if (isNaN(el[k])){ const txtSrc = el[k].trim(); const key = txtSrc.toLowerCase() .replace(/\xa0/g, ' ') // replace ' ' .replace(/\s{2,}/g, ' '); if (locales.dict[key]) { el[k] = el[k].replace(txtSrc, locales.dict[key]) } } translateElementAriaLabel(el) } function translateElementAriaLabel(el) { if (el.ariaLabel) { const k = 'ariaLabel' const txtSrc = el[k].trim(); const key = txtSrc.toLowerCase() .replace(/\xa0/g, ' ') // replace ' ' .replace(/\s{2,}/g, ' '); if (locales.dict[key]) { el[k] = el[k].replace(txtSrc, locales.dict[key]) } } } function shouldTranslateEl(el) { const blockIds = [ "readme", "file-name-editor-breadcrumb", "StickyHeader" // fix repo详情页文件路径breadcrumb ]; const blockClass = [ "CodeMirror", "js-navigation-container", // 过滤文件目录 "blob-code", "topic-tag", // 过滤标签, // "text-normal", // 过滤repo name, 复现:https://github.com/search?q=explore "repo-list",//过滤搜索结果项目,解决"text-normal"导致的有些文字不翻译的问题,搜索结果以后可以考虑单独翻译 "js-path-segment","final-path", "react-tree-show-tree-items", //过滤目录,文件位置栏 "markdown-body", // 过滤wiki页面, "search-input-container", //搜索框 "search-match", //fix搜索结果页,repo name被翻译 "cm-editor", "react-code-lines", //代码编辑框 "PRIVATE_TreeView-item", // 文件树 "repo", // 项目名称 ]; const blockTags = ["CODE", "SCRIPT", "LINK", "IMG", "svg", "TABLE", "PRE"]; const blockItemprops = ["name"]; if (blockTags.includes(el.tagName)) { return false; } if (el.id && blockIds.includes(el.id)) { return false; } if (el.classList) { for (let clazz of blockClass) { if (el.classList.contains(clazz)) { return false; } } } if (el.getAttribute) { let itemprops = el.getAttribute("itemprop"); if (itemprops) { itemprops = itemprops.split(" "); for (let itemprop of itemprops) { if (blockItemprops.includes(itemprop)) { return false; } } } } return true; } function traverseElement(el) { translateElementAriaLabel(el) if (!shouldTranslateEl(el)) { return } if (el.childNodes.length === 0) { if (el.nodeType === Node.TEXT_NODE) { translateElement(el); return; } else if(el.nodeType === Node.ELEMENT_NODE) { if (el.tagName === "INPUT") { translateElement(el); return; } } } for (const child of el.childNodes) { if (child.nodeType === Node.TEXT_NODE) { translateElement(child); } else if(child.nodeType === Node.ELEMENT_NODE) { if (child.tagName === "INPUT") { translateElement(child); } else { traverseElement(child); } } else { // pass } } } function watchUpdate() { const m = window.MutationObserver || window.WebKitMutationObserver; const observer = new m(function (mutations, observer) { var reTrans = false; for(let mutationRecord of mutations) { if (mutationRecord.addedNodes || mutationRecord.type === 'attributes') { reTrans = true; // traverseElement(mutationRecord.target); } } if(reTrans) { traverseElement(document.body); translateTime(); } }); observer.observe(document.body, { subtree: true, characterData: true, childList: true, attributeFilter: ['value', 'placeholder', 'aria-label', 'data', 'data-confirm'], // 仅观察特定属性变化(试验测试阶段,有问题再恢复) }); } // translate "about" function translateDesc(el) { $(el).append("<br/>"); $(el).append("<a id='translate-me' href='#' style='color:rgb(27, 149, 224);font-size: small'>翻译</a>"); $("#translate-me").click(function() { // get description text const desc = $(el) .clone() .children() .remove() .end() .text() .trim(); if(!desc) { return; } let lang = (navigator.userLanguage || navigator.language).toLowerCase(); let data_json = { header: { fn: "auto_translation" }, type: "plain", source: { text_list: [ desc ] }, target: { lang: lang == "zh-cn" ? "zh" : lang } } const repoId = $("input[name=repository_id]").val(); GM_xmlhttpRequest({ method: "GET", url: `https://www.github-zh.com/translate?i=${repoId}&q=`+ encodeURIComponent(desc), onload: function(rsp) { if (rsp.status === 200) { $("#translate-me").hide(); // render result const text = rsp.responseText; $(".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>"); $(".repository-content .f4").append("<br/>"); $(".repository-content .f4").append(text); } else { console.error("仓库描述翻译失败:", rsp) alert("翻译失败"); } } }); }); } function translateByCssSelector() { if(locales.css) { for(var css of locales.css) { if($(css.selector).length > 0) { if(css.key === '!html') { $(css.selector).html(css.replacement); } else { $(css.selector).attr(css.key, css.replacement); } } } } } function translateTime() { $("relative-time").each(function() { translateRelativeTimeEl(this); }) } })();