您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
中文化 18xx.games 界面的部分菜单及内容。
当前为
// ==UserScript== // @name 18𝕏𝕏.games 中文化插件 // @namespace https://github.com/klingeling/18xx-i18n-plugin/ // @description 中文化 18xx.games 界面的部分菜单及内容。 // @copyright 2023, klingeling // @icon https://18xx.games/images/logo_polygon_yellow.svg // @version 1.0.7 // @author klingeling // @license GPL-3.0 // @match https://18xx.games/* // @require https://greasyfork.org/scripts/472263-18xx-games-%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/18xxgames%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?version=1233185 // @run-at document-end // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @supportURL https://github.com/klingeling/18xx-i18n-plugin/issues // @license MIT // ==/UserScript== (function (window, document, undefined) { 'use strict'; const lang = 'zh'; // 设置默认语言 let page; //let enable_RegExp = GM_getValue("enable_RegExp", 1); let enable_RegExp = 1; /** * watchUpdate 函数:监视页面变化,根据变化的节点进行翻译 */ function watchUpdate() { // 检测浏览器是否支持 MutationObserver const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; // 获取当前页面的 URL const getCurrentURL = () => document.URL; getCurrentURL.previousURL = getCurrentURL(); // 创建 MutationObserver 实例,监听 DOM 变化 const observer = new MutationObserver((mutations, observer) => { const currentURL = getCurrentURL(); // 如果页面的 URL 发生变化 if (currentURL !== getCurrentURL.previousURL) { getCurrentURL.previousURL = currentURL; page = getPage(); // 当页面地址发生变化时,更新全局变量 page console.log(`链接变化 page= ${page}`); transTitle(); // 翻译页面标题 transBySelector(); if (page) { setTimeout(() => { // 使用 CSS 选择器找到页面上的元素,并将其文本内容替换为预定义的翻译 transTitle(); // 翻译页面标题 transBySelector(); //if (page === "repository") { //仓库简介翻译 // transDesc(".f4.my-3"); //} else if (page === "gist") { // Gist 简介翻译 // transDesc(".gist-content [itemprop='about']"); //} }, 500); } } if (page) { // 使用 filter 方法对 mutations 数组进行筛选, // 返回 `节点增加 或 属性更改的 mutation` 组成的新数组 filteredMutations。 const filteredMutations = mutations.filter(mutation => mutation.addedNodes.length > 0 || mutation.type === 'attributes'); // 处理每个变化 filteredMutations.forEach(mutation => traverseNode(mutation.target)); } }); // 配置 MutationObserver const config = { subtree: true, childList: true, attributeFilter: ['value', 'placeholder', 'aria-label', 'data-confirm'], // 仅观察特定属性变化 }; // 开始观察 document.body 的变化 observer.observe(document.body, config); } /** * traverseNode 函数:遍历指定的节点,并对节点进行翻译。 * @param {Node} node - 需要遍历的节点。 */ function traverseNode(node) { // 跳过忽略 if (I18N.conf.reIgnoreId.includes(node.id) || I18N.conf.reIgnoreClass.test(node.className) || I18N.conf.reIgnoreTag.includes(node.tagName) || (node.getAttribute && I18N.conf.reIgnoreItemprop.includes(node.getAttribute("itemprop"))) || (node.getAttribute && I18N.conf.reIgnorehrefprop.includes(node.getAttribute("href"))) ) { return; } if (node.nodeType === Node.ELEMENT_NODE) { // 元素节点处理 // 翻译时间元素 if ( ["RELATIVE-TIME", "TIME-AGO", "TIME", "LOCAL-TIME"].includes(node.tagName) ) { if (node.shadowRoot) { transTimeElement(node.shadowRoot); watchTimeElement(node.shadowRoot); } else { transTimeElement(node); } return; } // 元素节点属性翻译 if (["INPUT", "TEXTAREA"].includes(node.tagName)) { // 输入框 按钮 文本域 if (node.hasAttribute('title')) { transElement(node, 'title', true); // 翻译 浏览器 提示对话框 } if (["button", "submit", "reset"].includes(node.type)) { if (node.hasAttribute('data-confirm')) { // 翻译 浏览器 提示对话框 transElement(node, 'data-confirm', true); } transElement(node, 'value'); } else { transElement(node, 'placeholder'); } } else if (node.tagName === 'BUTTON') { if (node.hasAttribute('aria-label') && ["tooltipped"].includes(node.className)) { transElement(node, 'aria-label', true); // 翻译 浏览器 提示对话框 } if (node.hasAttribute('title')) { transElement(node, 'title', true); // 翻译 浏览器 提示对话框 } if (node.hasAttribute('data-confirm')) { transElement(node, 'data-confirm', true); // 翻译 浏览器 提示对话框 ok } if (node.hasAttribute('data-confirm-text')) { transElement(node, 'data-confirm-text', true); // 翻译 浏览器 提示对话框 ok } if (node.hasAttribute('data-confirm-cancel-text')) { transElement(node, 'data-confirm-cancel-text', true); // 取消按钮 提醒 } if (node.hasAttribute('cancel-confirm-text')) { transElement(node, 'cancel-confirm-text', true); // 取消按钮 提醒 } if (node.hasAttribute('data-disable-with')) { // 按钮等待提示 transElement(node.dataset, 'disableWith'); } } else if (node.tagName === 'OPTGROUP') { // 翻译 <optgroup> 的 label 属性 transElement(node, 'label'); } else if (["tooltipped"].includes(node.className)) { // 仅当 元素存在'tooltipped'样式 aria-label 才起效果 transElement(node, 'aria-label', true); // 带提示的元素,类似 tooltip 效果的 } else if (node.tagName === 'LABEL') { // 翻译 <LABEL> 的 label 属性 if (node.hasAttribute('title')) { transElement(node, 'title', true); // 翻译 浏览器 提示对话框 } } else if (node.tagName === 'A') { if (node.hasAttribute('title')) { transElement(node, 'title', true); // 翻译 浏览器 提示对话框 } if (node.hasAttribute('data-hovercard-type')) { return; // 不翻译 } } else if (node.tagName === 'DIV') { if (node.hasAttribute('title')) { transElement(node, 'title', true); // 翻译 浏览器 提示对话框 } } let childNodes = node.childNodes; childNodes.forEach(traverseNode); // 遍历子节点 } else if (node.nodeType === Node.TEXT_NODE) { // 文本节点翻译 if (node.length <= 1000) { // 修复 许可证编辑框初始化载入内容被翻译 transElement(node, 'data'); } } } /** * getPage 函数:获取当前页面的类型。 * @returns {string|boolean} 当前页面的类型,如果无法确定类型,那么返回 false。 */ function getPage() { // 站点,如 gist, developer, help 等,默认主站是 github const site = location.hostname === "18xx.games" ? "18xx" : "18xx"; // 站点 const pathname = location.pathname; // 当前路径 // 是否登录 // const isLogin = document.body.classList.contains("logged-in"); // 用于确定 个人首页,组织首页,仓库页 然后做判断 // const analyticsLocation = (document.getElementsByName('analytics-location')[0] || {}).content || ''; // 组织页 // const isOrganization = /\/<org-login>/.test(analyticsLocation)||/^\/(?:orgs|organizations)/.test(pathname); // 仓库页 // const isRepository = /\/<user-name>\/<repo-name>/.test(analyticsLocation); // 优先匹配 body 的 class let page, t = document.body.className.match(I18N.conf.rePageClass); // if (t) { // if (t[1] === 'page-profile') { // let matchResult = location.search.match(/tab=(\w+)/); // if (matchResult) { // page = 'page-profile/' + matchResult[1]; // } else { // page = pathname.match(/\/(stars)/) ? 'page-profile/stars' : 'page-profile'; // } // } else { // page = t[1]; // } // } else if (site === 'gist') { // Gist 站点 // page = 'gist'; // } else if (pathname === '/' && site === '18xx') { // 18xx.games 首页 page = 'homepage'; // } else if (isRepository) { // 仓库页 // t = pathname.match(I18N.conf.rePagePathRepo); // page = t ? 'repository/'+ t[1] : 'repository'; // } else if (isOrganization) { // 组织页 // t = pathname.match(I18N.conf.rePagePathOrg); // page = t ? 'orgs/'+ t[1] : 'orgs'; } else { t = pathname.match(I18N.conf.rePagePath); page = t ? t[1] : false; // 取页面 key } if (!page || !I18N[lang][page]) { console.log(`请注意对应 page: ${page} 词库节点不存在`); console.log(`当前路径: ${pathname}`) page = false; } return page; } /** * transTitle 函数:翻译页面标题 */ function transTitle() { let key = document.title; // 标题文本内容 let str = I18N[lang]['title']['static'][key] || ''; if (!str) { let res = I18N[lang]['title'].regexp || []; for (let [a, b] of res) { str = key.replace(a, b); if (str !== key) { break; } } } document.title = str; } /** * transTimeElement 函数:翻译时间元素文本内容。 * @param {Element} el - 需要翻译的元素。 */ function transTimeElement(el) { let key = el.childNodes.length > 0 ? el.lastChild.textContent : el.textContent; let res = I18N[lang]['pubilc']['time-regexp']; // 时间正则规则 for (let [a, b] of res) { let str = key.replace(a, b); if (str !== key) { el.textContent = str; break; } } } /** * watchTimeElement 函数:监视时间元素变化, 触发和调用时间元素翻译 * @param {Element} el - 需要监视的元素。 */ function watchTimeElement(el) { const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; new MutationObserver(mutations => { transTimeElement(mutations[0].addedNodes[0]); }).observe(el, { childList: true }); } /** * transElement 函数:翻译指定元素的文本内容或属性。 * @param {Element} el - 需要翻译的元素。 * @param {string} field - 需要翻译的文本内容或属性的名称。 * @param {boolean} isAttr - 是否需要翻译属性。 */ function transElement(el, field, isAttr = false) { let text = isAttr ? el.getAttribute(field) : el[field]; // 需要翻译的文本 let str = translateText(text); // 翻译后的文本 // 替换翻译后的内容 if (str) { if (!isAttr) { el[field] = str; } else { el.setAttribute(field, str); } } } /** * translateText 函数:翻译文本内容。 * @param {string} text - 需要翻译的文本内容。 * @returns {string|boolean} 翻译后的文本内容,如果没有找到对应的翻译,那么返回 false。 */ function translateText(text) { // 翻译 // 内容为空, 空白字符和或数字, 不存在英文字母和符号,. 跳过 if (!isNaN(text) || !/[a-zA-Z,.]+/.test(text)) { return false; } let _key = text.trim(); // 去除首尾空格的 key let _key_neat = _key.replace(/\xa0|[\s]+/g, ' ') // 去除多余空白字符( 空格 换行符) let str = fetchTranslatedText(_key_neat); // 翻译已知页面 (局部优先) if (str && str !== _key_neat) { // 已知页面翻译完成 return text.replace(_key, str); // 替换原字符,保留首尾空白部分 } return false; } /** * fetchTranslatedText 函数:从特定页面的词库中获得翻译文本内容。 * @param {string} key - 需要翻译的文本内容。 * @returns {string|boolean} 翻译后的文本内容,如果没有找到对应的翻译,那么返回 false。 */ function fetchTranslatedText(key) { // 静态翻译 let str = I18N[lang][page]['static'][key] || I18N[lang]['pubilc']['static'][key]; // 默认翻译 公共部分 if (typeof str === 'string') { return str; } // 正则翻译 if (enable_RegExp) { let res = (I18N[lang][page].regexp || []).concat(I18N[lang]['pubilc'].regexp || []); // 正则数组 for (let [a, b] of res) { str = key.replace(a, b); if (str !== key) { return str; } } } return false; // 没有翻译条目 } /** * transDesc 函数:为指定的元素添加一个翻译按钮,并为该按钮添加点击事件。 * @param {string} el - CSS选择器,用于选择需要添加翻译按钮的元素。 */ function transDesc(el) { // 使用 CSS 选择器选择元素 let element = document.querySelector(el); // 如果元素不存在,那么直接返回 if (!element) { return false; } // 在元素后面插入一个翻译按钮 let button = document.createElement('div'); button.id = 'translate-me'; button.style.cssText = 'color: rgb(27, 149, 224); font-size: small; cursor: pointer'; button.textContent = '翻译'; element.insertAdjacentElement('afterend', button); // 为翻译按钮添加点击事件 button.addEventListener('click', () => { // 获取元素的文本内容 const desc = element.textContent.trim(); // 如果文本内容为空,那么直接返回 if (!desc) { return false; } // 调用 translateDescText 函数进行翻译 translateDescText(desc, text => { // 翻译完成后,隐藏翻译按钮,并在元素后面插入翻译结果 button.style.display = "none"; 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}`; element.insertAdjacentHTML('afterend', translationHTML); }); }); } /** * translateDescText 函数:将指定的文本发送到讯飞的翻译服务进行翻译。 * @param {string} text - 需要翻译的文本。 * @param {function} callback - 翻译完成后的回调函数,该函数接受一个参数,即翻译后的文本。 */ function translateDescText(text, callback) { // 使用 GM_xmlhttpRequest 函数发送 HTTP 请求 GM_xmlhttpRequest({ method: "POST", // 请求方法为 POST url: "https://www.iflyrec.com/TranslationService/v1/textTranslation", // 请求的 URL headers: { // 请求头 'Content-Type': 'application/json', 'Origin': 'https://www.iflyrec.com', }, data: JSON.stringify({ "from": "2", "to": "1", "contents": [{ "text": text, "frontBlankLine": 0 }] }), // 请求的数据 responseType: "json", // 响应的数据类型为 JSON onload: (res) => { try { const { status, response } = res; const translatedText = (status === 200) ? response.biz[0].translateResult : "翻译失败"; callback(translatedText); } catch (error) { callback("翻译失败"); } } }); } /** * transBySelector 函数:通过 CSS 选择器找到页面上的元素,并将其文本内容替换为预定义的翻译。 */ function transBySelector() { // 获取当前页面的翻译规则,如果没有找到,那么使用公共的翻译规则 let res = (I18N[lang][page]?.selector || []).concat(I18N[lang]['pubilc'].selector || []); // 数组 // 如果找到了翻译规则 if (res.length > 0) { // 遍历每个翻译规则 for (let [selector, translation] of res) { // 使用 CSS 选择器找到对应的元素 let element = document.querySelector(selector); // 如果找到了元素,那么将其文本内容替换为翻译后的文本 if (element) { element.textContent = translation; } } } } // GM_registerMenuCommand("正则切换", () => { // enable_RegExp = enable_RegExp ? 0 : 1; // GM_setValue("enable_RegExp", enable_RegExp); // GM_notification(`已${enable_RegExp ? '开启' : '关闭'}正则功能`); // if (enable_RegExp) { // location.reload(); // } // }); /** * init 函数:初始化翻译功能。 */ function init() { // 获取当前页面的翻译规则 page = getPage(); console.log(`开始page = ${page}`); // 翻译页面标题 transTitle(); if (page) { // 立即翻译页面 traverseNode(document.body); setTimeout(() => { // 使用 CSS 选择器找到页面上的元素,并将其文本内容替换为预定义的翻译 // 翻译页面标题 transTitle(); transBySelector(); if (page === "repository") { //仓库简介翻译 transDesc(".f4.my-3"); } else if (page === "gist") { // Gist 简介翻译 transDesc(".gist-content [itemprop='about']"); } }, 500); } // 监视页面变化 watchUpdate(); } // 执行初始化 init(); })(window, document);