// ==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);
})
}
})();