// ==UserScript== // @name VNDB优先原文和中文化 // @namespace http://tampermonkey.net/ // @version 3.0.0 // @description 优先显示原文(title->value),以及中文化(mainMap[value]->value) // @author aotmd // @match https://vndb.org/* // @noframes // @license MIT // @run-at document-body // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @downloadURL none // ==/UserScript== /*通用主map,作用在全局*/ let mainMap = { /*https://vndb.org/用户ID/ulist?vnlist=1*/ /*显示选项*/ "display options": "显示选项", "Order by": "排序方式", "Results": "显示数量", "Update": "更新", "Visible": "显示的", "columns": "列", /*排序标记*/ "Title": "标题", "Vote date": "评分时间", "Vote": "我的评分", "Rating": "评分", "Labels": "标识", "Added": "添加时间", "Modified": "修改时间", "Start date": "开始日期", "Finish date": "完成日期", "Release date": "发布日期", /*带排序标记*/ "Title ▴": "标题 ▴", "Vote date ▴": "评分时间 ▴", "Vote ▴": "我的评分 ▴", "Rating ▴": "评分 ▴", "Labels ▴": "标识 ▴", "Added ▴": "添加时间 ▴", "Modified ▴": "修改时间 ▴", "Start date ▴": "开始日期 ▴", "Finish date ▴": "完成日期 ▴", "Release date ▴": "发布日期 ▴", /*Opt*/ 'Notes': '笔记', 'Remove VN': '删除 VN', '-- add release --':'--添加版本--', /*状态*/ "Playing": "在玩", "Finished": "玩过", "Stalled": "搁置", "Dropped": "抛弃", "Wishlist": "愿望单", "Blacklist": "黑名单", /*VNDB封面插件翻译*/ "Always Show the VN Info": "始终显示 VN 信息", "Show NSFW Covers": "显示 NSFW 封面", "Disable tooltip": "禁用工具提示", "Skip Additional Info": "跳过附加信息", "Async Cover": "异步封面", "Query Mode": "查询方式", "Legacy View": "旧版视图", /*左侧栏*/ /*菜单*/ "Support VNDB": "赞助 VNDB", "Patreon": "Patreon", "SubscribeStar": "SubscribeStar", "Menu": "菜单", "Home": "首页", "Visual novels": "视觉小说", "Tags": "标签", "Releases": "版本", "Producers": "制作人", "Staff": "工作人员", "Characters": "人物", "Traits": "特征", "Users": "用户", "Recent changes": "最近更改", "Discussion board": "讨论区", "FAQ":"常见问题", "Random visual novel": "随机视觉小说", "Dumps": "转储", "API":"API", "Query": "查询", "Search": "搜索", "search": "搜索", /*我的*/ "My Profile": "我的个人资料", "My Visual Novel List": "我的视觉小说列表", "My Votes": "我的评分", "My Wishlist": "我的愿望单", "My Notifications": "我的通知", "My Recent Changes": "我的最近更改", "My Tags": "我的标签", "Image Flagging": "图片标记", "Add Visual Novel": "添加视觉小说", "Add Producer": "添加制作人", "Add Staff": "添加工作人员", "Logout": "退出登录", /*数据库统计*/ "Database Statistics": "数据库统计", "Visual Novels": "视觉小说", /*用户页顶栏*/ "the visual novel database": "视觉小说数据库", "edit": "编辑", "list": "列表", "votes": "评分", "wishlist": "愿望单", "reviews": "评论", "posts": "帖子", "history": "历史", /*个人资料页面*/ "Username": "用户名", "Registered": "注册日期", "Edits": "编辑", "Votes": "评分", "Browse votes »": "浏览评分 »", "Play times": "游戏时间", "List stats": "列表统计", "Browse list »": "浏览列表 »", "Reviews": "评论", "Images": "图片", "Browse image votes »": "浏览图片投票 »", "Forum stats": "论坛统计", "Vote statistics": "评分统计", "Vote stats": "评分统计", "Recent votes": "最近评分", "show all": "显示全部", "about us": "关于我们", /*评分说明*/ "masterpiece":"杰作|超神作", "excellent":"极好|神作", "so-so":"一般般|不过不失", "very good":"很好|力荐", "good":"好|推荐", "decent":"不错|还行", "weak":"不太行|较差", "bad":"糟糕|差", "awful":"很坏|很差", "worst ever":"最差|不忍直视" }; /*特殊全局map,用以替换变动的文本节点[正则],value出现的%%var%%为需要继续翻译的值*/ let specialMap={ "^(\\d+) vote[s]? total, average ([\\d.]+) \\(([a-zA-Z -]+)\\)$":"总共$1票, 平均分$2 (%%$3%%)", "^(\\d+) \\(([a-zA-Z -]+)\\)$":"$1 (%%$2%%)", }; /*额外map,作用在指定页面*/ let otherPageRules = [ { /*作用页说明*/ name: '个人页', /*正则表达式*/ regular: /u\d+/i, /*mainMap k->v*/ map: { "My Account": "我的账号", "Account settings": "账号设置", "change": "修改", "E-Mail": "邮箱", "Change password": "更改密码", "Preferences": "偏好", "NSFW": "NSFW", "Hide sexually suggestive or explicit images": "隐藏性暗示或露骨的图像", "Hide all images": "隐藏所有图片", "Hide only sexually explicit images": "只隐藏色情图片", "Don't hide suggestive or explicit images": "不隐藏性暗示或露骨图片", "Hide violent or brutal images": "隐藏暴力或残暴图像", "Hide only brutal images": "只隐藏残暴的图像", "Don't hide violent or brutal images": "不隐藏暴力或残暴的图像", "Show sexual traits by default on character pages": "默认情况下在人物页面上显示性特征", "Title language": "标题语言", "Add language": "添加语言", "Original language": "原始语言", "romanized": "罗马化", "Alternative title": "副标题", "The alternative title is displayed below the main title and as tooltip for links.": "副标题显示在主标题下方,并作为链接的提示", "Arabic": "阿拉伯语", "Bulgarian": "保加利亚语", "Catalan": "加泰罗尼亚语", "Chinese": "中文", "Chinese (simplified)": "中文(简体)", "Chinese (traditional)": "中文(繁体)", "Croatian": "克罗地亚语", "Czech": "捷克语", "Danish": "丹麦语", "Dutch": "荷兰语", "English": "英语", "Esperanto": "世界语", "Finnish": "芬兰语", "French": "法语", "German": "德语", "Greek": "希腊语", "Hebrew": "希伯来语", "Hindi": "印地语", "Hungarian": "匈牙利语", "Indonesian": "印尼语", "Irish": "爱尔兰语", "Italian": "意大利语", "Japanese": "日语", "Korean": "韩语", "Latin": "拉丁语", "Latvian": "拉脱维亚语", "Lithuanian": "立陶宛语", "Macedonian": "马其顿语", "Malay": "马来语", "Norwegian": "挪威语", "Persian": "波斯语", "Polish": "波兰语", "Portuguese (Brazil)": "葡萄牙语(巴西)", "Portuguese (Portugal)": "葡萄牙语(葡萄牙)", "Romanian": "罗马尼亚语", "Russian": "俄语", "Scottish Gaelic": "苏格兰盖尔语", "Slovak": "斯洛伐克语", "Slovene": "斯洛文尼亚语", "Spanish": "西班牙语", "Swedish": "瑞典语", "Tagalog": "塔加洛语", "Thai": "泰语", "Turkish": "土耳其语", "Ukrainian": "乌克兰语", "Urdu": "乌尔都语", "Vietnamese": "越南语", "remove": "移除", "Show all tags by default on visual novel pages (don't summarize)": "在视觉小说页面上默认显示所有标签(不汇总)", "Default tag categories on visual novel pages:": "视觉小说页面上默认显示的标签类别:", "Content": "内容", "Sexual content": "色情内容", "Technical": "技术相关", "Spoiler level": "剧透级别", "Hide spoilers": "隐藏剧透", "Show only minor spoilers": "仅显示次要剧透", "Show all spoilers": "显示所有剧透", "Skin": "皮肤", "AIR (sky blue)": "AIR(天蓝)", "Angelic Serenade (dark blue)": "エンジェリックセレナーデ 天使小夜曲(深蓝色)", "EIeL (peach-orange)": "電脳妖精エルファン (桃橙色)", "Eien no Aselia (falu red)": "永遠のアセリア 永远的艾塞莉娅 (法鲁红)", "Ever17 (bondi blue)": "ever17 (邦迪蓝)", "Fate/stay night (pale carmine)": "fate/stay night (淡胭脂红)", "Fate/stay night (seal brown)": "fate/stay night (海豹棕)", "Gekkou no Carnevale (black)": "月光のカルネヴァーレ 月光嘉年华(黑色)", "Higanbana no Saku Yoru ni (maroon)": "彼岸花の咲く夜に 彼岸花盛开之夜 (栗色)", "Higurashi no Naku Koro ni (orange)": "higurashi no naku koro ni (橙色)", "Little Busters! (lemon chiffon)": "little busters! (柠檬雪纺)", "Little Busters! (pink)": "little busters! (粉色)", "Neon (black)": "neon (黑色)", "Primitive Link (pale chestnut)": "primitive link (淡栗子)", "Saya no Uta (dark scarlet)": "saya no uta (深红)", "Seinarukana (white)": "seinarukana (白色)", "Sora no Iro, Mizu no Iro (turquoise)": "sora no iro, mizu no iro (绿松石)", "Teal (teal)": "teal (青色)", "Touhou (grey)": "touhou (灰色)", "Tsukihime (black)": "tsukihime (黑色)", "Tsukihime (midnight blue)": "tsukihime (午夜蓝)", "Custom CSS": "自定义CSS", "Public profile": "公开资料", "You can add": "您可以添加", "character traits": "性格特征", "to your account. These will be displayed on your public profile.": "到您的帐户。这些资料会公开显示。", "No results": "无结果", "Add trait...": "添加特征...", "Submit": "提交", /*在选择后出现的新文本:*/ "Only if original title": "仅当是原始标题时", "Only if official title": "仅当是官方标题时", "Include non-official titles": "也包括非官方标题", "New username": "新用户名", "You may only change your username once a day. Your old username(s) will be displayed on your profile for a month after the change.": "您每天只能更改一次用户名。更改用户名后,旧用户名会在个人资料中显示一个月。", "Old password": "旧密码", "New password": "新密码", "Repeat": "重复新密码", }, specialMap:{ "^(.+)'s profile$": "$1 的个人资料", "^discussions \\((\\d+)\\)$": "讨论 ($1)", "^(\\d+) votes, ([\\d.]+) average.$": "$1个评分, 平均$2分.", "^(\\d+) votes total, average ([\\d.]+)$": "$1个评分, 平均$2分", "^(\\d+)h$": "$1小时", "^(\\d+)m$": "$1分钟", "^from (\\d+) submitted play times.$": ",来自$1个游戏.", "^(\\d+) release of (\\d+) visual novels.$": "$1个版本,$2部视觉小说.", "^(\\d+) images flagged.$": "标记了$1个图片.", }, }, {} ]; /** ---------------------------map处理---------------------------*/ let pathname=window.location.pathname; otherPageRules.forEach((item)=>{ //当regular是正则才执行 if (item.regular!==undefined&&item.regular instanceof RegExp){ if (item.regular.test(pathname)){ //添加到主map,若存在重复项则覆盖主map Object.assign(mainMap,item.map); //添加特殊map,正则 Object.assign(specialMap,item.specialMap); console.log(item.name+',规则匹配:'+pathname+'->'+item.regular); } } }); /*object转Map*/ (function(){ let tempMap=new Map(); let k=Object.getOwnPropertyNames(specialMap); for (let i=0,len=k.length;i{ console.time('原文化:时间消耗'); 递归(document.body, 原文化); console.timeEnd('原文化:时间消耗'); console.time('字典翻译:时间消耗'); 递归(document.body, 字典翻译); console.timeEnd('字典翻译:时间消耗'); }); function 原文化(node, value, attribute = 'Text') { if (value == null || value.trim().length === 0) return; value = value.trim(); if (attribute==='Text'){ let title = node.parentNode.getAttribute("title"); if (内容判定(title,value)) { node.parentNode.setAttribute("title", value); node.nodeValue = title; // console.log(value+'->'+title) } }else { let title = node.getAttribute("title"); if (内容判定(title,value)) { //若为通常节点则正常设置属性 node.setAttribute('title', value); node.setAttribute(attribute, title); // console.log(value+'->'+title) } } /** * 显示的部分不为中文或日文,并且交换的部分为中文或日文 * @param title * @param value * @returns {boolean} */ function 内容判定(title,value) { return title != null && !/[\u4E00-\u9FA5ぁ-んァ-ヶ]+/.test(value) && /[\u4E00-\u9FA5ぁ-んァ-ヶ]+/.test(title); } } function 字典翻译(node, value, attribute = 'Text') { if (value == null || value.trim().length === 0) return; value = value.trim(); if (mainMap[value] !== undefined) { if (attribute === 'Text') { //若为文本节点则追加父节点title属性 let title = node.parentNode.getAttribute('title'); if (title != null&&title.trim()!==value) { node.parentNode.setAttribute('title', title + '\n' + value); } else { node.parentNode.setAttribute('title', value); } node.nodeValue = mainMap[value]; } else { //若为通常节点则正常设置属性 node.setAttribute('title', value); node.setAttribute(attribute, mainMap[value]) } }else { //遍历specialMap,正则替换 for (let key of specialMap.keys()){ /*正则匹配*/ if (key.test(value)){ /*正则替换*/ let newValue=value.replace(key, specialMap.get(key)); /*若有循环替换符,则进行替换*/ let nvs=newValue.split('%%'); if (nvs.length !== 1 && nvs.length % 2 === 1) { for (let i = 1; i < nvs.length; i += 2) { if (mainMap[nvs[i]] !== undefined) { nvs[i]=mainMap[nvs[i]]; } } newValue=nvs.join('') } if (attribute === 'Text') { //若为文本节点则追加父节点title属性 let title = node.parentNode.getAttribute('title'); if (title != null&&title.trim()!==value) { node.parentNode.setAttribute('title', title + ' ' + value); } else { node.parentNode.setAttribute('title', value); } node.nodeValue = newValue; } else { //若为通常节点则正常设置属性 node.setAttribute('title', value); node.setAttribute(attribute, newValue) } // console.log(value+'->'+newValue); /*替换后结束遍历*/ break; } } } } /** * dom修改事件,包括属性,内容,节点修改 * @param document 侦听对象 * @param func 执行函数 */ function dom修改事件(document,func) { const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;//浏览器兼容 const config = {attributes: true, childList: true, characterData: true, subtree: true};//配置对象 const observer = new MutationObserver(function () { //进入后停止侦听 observer.disconnect(); try { func(); } catch (e) {console.error('执行错误')} //结束后继续侦听 observer.observe(document, config); }); observer.observe(document, config); } })(); /** 开启后通过控制台调用函数即可*/ let 开发者模式 = true; if (开发者模式) { /*exportMap已弃用*/ /** * 导出新的已被翻译的内容到控制台显示 *
即手动在网页上改文本,注意: *
先在要翻译的文本中间写入翻译后的内容 *
然后用del和backspace删除前后内容 *
开启编辑模式: *
document.body.contentEditable='true'; *
document.designMode='on'; */ exportMap = function () { let addMap = {}; 递归(document.body, 数据处理); /*导出到控制台处理*/ console.log(JSON.stringify(addMap)); function 数据处理(node, value, attribute = 'Text') { if (value == null || value.trim().length === 0) return; value = value.trim(); //没有在map中找到翻译 if (mainMap[value] === undefined) { //是中文、不是日文 if (/[\u4E00-\u9FA5]+/.test(value) && !/[ぁ-んァ-ヶ]+/.test(value)) { if (attribute === 'Text') { node = node.parentNode; } let title = node.getAttribute('title'); //如果title没有翻译,则记录 if (title !== null && mainMap[title] === undefined) { addMap[title] = value; } } } } }; /*** 记录所有满足条件的未翻译内容
缺点为找不到上下文*/ noMap = {}; /*** 用以复制value到title
若出现新元素,请手动通过控制台重新调用 */ copyToTitle = () => { //清空 noMap = {}; 递归(document.body, 数据处理); console.log(JSON.stringify(noMap)); function 数据处理(node, value, attribute = 'Text') { if (value == null || value.trim().length === 0) return; value = value.trim(); //没有在map中找到翻译 if (mainMap[value] === undefined) { //1<长度<300,不为中文、日文,不是纯数字 if (1title if (title != null &&title.trim()!==value) { node.setAttribute('title', title + ' ' + value); } else { node.setAttribute('title', value); } //设置没有翻译的map标记 noMap[value] = value.toLowerCase(); } } } }; /*立即执行*/ copyToTitle(); /** *统计不应该匹配,但可以匹配的k->v与正则,用以将局部map升级到主map * @type {{Object}} */ otherLog=GM_getValue('otherLog')||{}; console.log(otherLog); delotherLog=()=>{GM_deleteValue('otherLog');}; (() => { /*object转Map,将其他没有生效的map合起来*/ let otherMap = {}; let otherSpecialMap = new Map(); otherPageRules.forEach((item) => { let pathname = window.location.pathname; if (item.regular !== undefined && item.regular instanceof RegExp && !item.regular.test(pathname)) { let k = Object.getOwnPropertyNames(item.specialMap); for (let i = 0, len = k.length; i < len; i++) { try { otherSpecialMap.set(new RegExp(k[i]), item.specialMap[k[i]]); } catch (e) { console.log('"' + k[i] + '"不是一个合法正则表达式'); } } Object.assign(otherMap, item.map); } }); 递归(document.body, 子字典匹配测试); /*当body发生变化时执行*/ dom修改事件(document.body, () => { console.time('子字典调用,调试:时间消耗'); 递归(document.body, 子字典匹配测试); console.timeEnd('子字典调用,调试:时间消耗'); /*若不相等则更新并输出*/ if (JSON.stringify(otherLog)!==JSON.stringify(GM_getValue('otherLog')||{})){ GM_setValue('otherLog',otherLog); console.log(otherLog); } }); /** * 统计不应该匹配,但可以匹配的k->v与正则,用以将局部map升级到主map * @param key * @param value */ function otherLogAdd(key, value) { if (otherLog[key] === undefined) { otherLog[key] = [1,value[0],value[1]]; } else { let item = otherLog[key]; item[0]++; /*去重*/ let a1=item[1].split('$$'); a1.push(value[0]); let mySet = new Set(a1); a1=[...mySet]; item[1]=a1.join('$$'); a1=item[2].split('$$'); a1.push(value[1]); mySet = new Set(a1); a1=[...mySet]; item[2]=a1.join('$$'); } } function 子字典匹配测试(node, value, attribute = 'Text') { if (value == null || value.trim().length === 0) return; value = value.trim(); if (mainMap[value] === undefined) { for (let key of specialMap.keys()) { if (key.test(value)) { return; } } /*不被mainMap和specialMap匹配*/ if (otherMap[value] !== undefined) { if (attribute === 'Text') { //若为文本节点则追加父节点title属性 let title = node.parentNode.getAttribute('title'); if (title != null && title.trim() !== value) { node.parentNode.setAttribute('title', title + ' ' + value); } else { node.parentNode.setAttribute('title', value); } node.nodeValue = otherMap[value]; otherLogAdd(value, ['otherMap匹配,Text', otherMap[value]]); } else { //若为通常节点则正常设置属性 node.setAttribute('title', value); node.setAttribute(attribute, otherMap[value]); otherLogAdd(value, ['otherMap匹配,节点', otherMap[value]]); } } else { //遍历specialMap,正则替换 for (let key of otherSpecialMap.keys()) { /*正则匹配*/ if (key.test(value)) { let info = 'otherSpecialMap匹配,正则:' + key + ','; /*正则替换*/ let newValue = value.replace(key, otherSpecialMap.get(key)); /*若有循环替换符,则进行替换*/ let nvs = newValue.split('%%'); if (nvs.length !== 1 && nvs.length % 2 === 1) { for (let i = 1; i < nvs.length; i += 2) { if (otherMap[nvs[i]] !== undefined) { nvs[i] = otherMap[nvs[i]]; info += '在otherMap找到%%%%(额外匹配),'; } if (mainMap[nvs[i]] !== undefined) { nvs[i] = mainMap[nvs[i]]; info += '在mainMap找到%%%%(额外匹配),'; } } newValue = nvs.join('') } if (attribute === 'Text') { //若为文本节点则追加父节点title属性 let title = node.parentNode.getAttribute('title'); if (title != null && title.trim() !== value) { node.parentNode.setAttribute('title', title + ' ' + value); } else { node.parentNode.setAttribute('title', value); } node.nodeValue = newValue; info += 'Text'; } else { //若为通常节点则正常设置属性 node.setAttribute('title', value); node.setAttribute(attribute, newValue); info += '节点'; } otherLogAdd(value, [info, newValue]); // console.log(value + '->' + newValue); /*替换后结束遍历*/ break; } } } } } /** * dom修改事件,包括属性,内容,节点修改 * @param document 侦听对象 * @param func 执行函数 */ function dom修改事件(document, func) { const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;//浏览器兼容 const config = {attributes: true, childList: true, characterData: true, subtree: true};//配置对象 const observer = new MutationObserver(function () { //进入后停止侦听 observer.disconnect(); try { func(); } catch (e) { console.error('执行错误') } //结束后继续侦听 observer.observe(document, config); }); observer.observe(document, config); } })(); }