// ==UserScript== // @name GameBanana 中文化 // @namespace gamebanana-chinese // @version 1.4 // @description GameBanana 模组网站中文化,支持自定义词库。 // @author 夏布为荔 // @match https://gamebanana.com/* // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ==================== 自定义词库 ==================== const dictionary = { // 导航与标签 'Games': '游戏', 'Main': '主页', 'Get Started': '入门', 'Sections': '分区', 'Featured': '精选', 'Discussions': '讨论', 'Rules': '规则', 'Collections': '合集', 'Exchange': '交流', 'Admin': '管理', '/\\bAdd\\b/g': '添加', '/\\bSubscribe\\b/g': '订阅', '/\\bcat\\b/g': '个分类', 'cats': '个分类', 'Character Skins': '角色皮肤', 'Other/Misc': '其他/杂项', 'Bangboo Skins': '邦布皮肤', 'Blogs': '博客', 'Concepts': '概念设计', 'Contests': '比赛', 'Threads': '讨论贴', 'Polls': '投票', 'Projects': '项目', 'Questions': '问答', 'Requests': '悬赏', 'Scripts': '脚本', 'Sounds': '音效', 'Sprays': '喷漆', 'Tools': '工具', 'Tutorials': '指南', 'Wares': '商品', 'WiPs': '开发中', 'Community Spotlight': '社区精选', 'Filters': '筛选', 'Most Liked': '最受喜爱', 'Most Downloaded': '最多下载', 'Most Viewed': '最多浏览', 'Studio Releases': '团队发布', '/\\bCategories\\b/g': '类别', 'Overview': '概览', 'Advanced Filters': '高级筛选', 'Order': '排序', 'Show Empty': '展示空白', 'Exactly Equal': '精确匹配', 'Contains': '包含', 'Starts With': '开头匹配', 'Ends With': '结尾匹配', 'Name': '名称', 'Release Type': '发布类型', '/\\bAny\\b/g': '不限', 'Studio': '工作室', '/\\bIndie\\b/g': '独立', 'Redistribution': '转载', 'Content Ratings': '内容分级', 'Unrated': '未评级', 'Crude or Profane': '粗俗语言', 'Sexual Themes': '性暗示', 'Sexual Content': '性内容', 'Blood and Gore': '血腥', 'Alcohol Use': '饮酒', 'Tobacco Use': '吸烟', 'Drug Use': '药物使用', 'Flashing Lights & Patterns': '闪光警示', 'Skimpy Attire': '暴露穿着', 'Lewd Angles & Poses': '性暗示姿势', 'Partial Nudity': '部分裸露', 'Full Nudity': '全裸', 'Intense Violence': '激烈暴力', 'Fetishistic': '恋物癖', 'Rating Pending': '待定级', '/\\bApply\\b/g': '应用', '/\\bReset\\b/g': '重置', 'Includes WiP': '含制作中内容', 'Part of Project': '属于项目作品', 'Report': '反馈', 'Icon': '图标', 'Date Added': '添加日期', 'Submitter': '添加者', 'Shown': '展示', 'NSFW': '工作时不宜', '/\\bInstallers\\b/g': '安装工具', 'Likes': '喜欢', 'Views': '浏览数', 'Posts': '评论', 'CPU/Processor': 'CPU / 处理器', 'RAM/Memory': '内存', 'Motherboard': '主板', 'Graphics Card': '显卡', 'Status': '状态', 'Online': '在线', 'Outline': '离线', 'Time Online': '在线时长', 'Newest': '最新', 'Popular': '最受欢迎', 'Studios': '工作室', 'Clubs': '俱乐部', 'Subscriptions': '订阅', 'Stats': '统计', 'Lists': '列表', 'Liked Submissions': '点赞投稿', 'Thanked Submissions': '获赞投稿', 'Voted Polls': '参与的投票', 'Rated Ideas': '评过分的创意', 'Contributed Bounties': '贡献的悬赏', 'Awarded Bounties': '获得的悬赏', 'Participated Contests': '参与的比赛', 'Solved Questions': '解答的问题', 'Members': '用户', 'Index ': '注册数', 'Login': '登录', 'Signup': '注册', 'Do Not Sell My Personal Information': 'Do Not Sell My Personal Information', 'Site Rules': '站点规则', 'Legal': '法律条款', // 标题与描述 'Mods and Modding Resources by the Zenless Zone Zero Modding Community | ZZZ Hub.': '模组及其制作资源由 绝区零模组社区|ZZZ Hub 提供。', 'Welcome to the Zenless Zone Zero Gamebanana Page!': '欢迎来到《绝区零》Gamebanana 页面!', 'Looking to start modding?': '想开始上手模组?', '- Install the XXMI Launcher': '- 安装 XXMI Launcher,', '- Start following guides to make your own mods, which can be found': '- 查看入门教程并开始制作自己的模组,', 'The modding discord server': 'Mod 制作 Discord 服务器', 'can be found': '请', 'here!': '点这里!', 'here': '点这里!', 'Best of this year': '年度最佳', 'Best of this month': '本月最佳', 'Best of this week': '本周最佳', 'Best of today': '今日最佳', 'Mods for': '模组适用于', 'Category Info': '版块信息', 'Install and manage your mods easily with community-made 1-Click mod downloaders.': '借助社区制作的一键 Mod 下载器,轻松安装与管理你的 Mod。', 'Notifications': '通知', '/\\bPersonal\\b/g': '个人中心', 'Control Center': '控制中心', 'No subcategories to display.': '没有可显示的类别。', 'Profile': '个人资料', 'Join Date': '注册时间', 'Buddies': '好友', 'No buddies to display.': '没有可显示的好友。', 'Medals': '勋章', 'This user has not been awarded any medals.': '该用户还没有获得过勋章。', '/\\bBio\\b/g': '简介', 'No information set.': '未填写任何信息。', 'Contact Info': '联系方式', 'PC Specs': '电脑配置', 'Software Kit': '软件工具集', 'Communities': '社区', 'Gaming Devices': '游戏设备', 'Let other Bananites know what powers your gaming rig. Got other gaming devices? Set them': '让其他 Bananites 了解你游戏设备的配置。还有其他游戏设备?', 'Save': '保存', 'Custom Entry': '自定义项', 'Core Stats': '核心统计', 'Submissions': '投稿', 'No submissions found.': '暂无投稿。', 'Comments': '评论', 'Add Comment': '添加评论', 'No comments yet!': '暂无评论!', 'Points': "点数", 'Subscribers': '订阅者', 'Featurings': '精选', 'Thanked': '获赞', 'Account age': '账户年龄', //'Unlocks': '解锁内容', //'Personalization': '个性化装扮', //'Asset Online! Customize your Online title that appears in avatar tooltips and buddy lists.': '上线资产!自定义你的在线头衔,该头衔会显示在头像提示框和好友列表中。', // 游戏名 '/\\bZenless Zone Zero\\b/g': '绝区零', '/\\bZZZ\\b/g': '绝区零', //角色名 'Alexandrina Sebastiane': '丽娜', 'Alice Thymefield': '爱丽丝', 'Anby Demara': '安比', 'Anton Ivanov': '安东', '/\\bAria\\b/g': '爱芮', 'Asaba Harumasa': '浅羽悠真', 'Astra Yao': '耀嘉音', 'Banyue': '般岳', 'Belle': '铃', 'Ben Bigger': '本·比格', 'Billy Kid': '比利·奇德', 'Burnice White': '柏妮思', 'Caesar King': '凯撒', 'Cissia': '希希芙', 'Corin Wickes': '可琳', 'Dialyn': '琉音', 'Ellen Joe': '艾莲·乔', 'Evelyn Chevalier': '伊芙琳', 'Grace Howard': '格莉丝', 'Hoshimi Miyabi': '星见雅', 'Hugo Vlad': '雨果•维拉德', 'Jane Doe': '简·杜', 'Ju Fufu': '橘福福', 'Koleda Belobog': '珂蕾妲', 'Komano Manato': '狛野真斗', 'Lighter': '莱特', 'Lucia Elowen': '卢西娅', 'Luciana de Montefio': '露西', 'Nangong Yu': '南宫羽', 'Nekomiya Mana': '猫又', 'Nicole Demara': '妮可', 'Norma Hollowell': '诺姆', 'Orphie Magnusson': '奥菲丝 &「鬼火」', 'Pan Yinhu': '潘引壶', 'Piper Wheel': '派派', 'Promeia': '普罗米娅', 'Pulchra Fellini': '波可娜', 'Pyrois': '佩洛伊斯', 'Qingyi': '青衣', 'Remielle Dan': '蕾米埃尔·丹', 'Seed': '「席德」', 'Seth Lowell': '赛斯', 'Sigrid de L\'Azur': '希格莉德', 'Soldier 0 Anby': '零号·安比', 'Soldier 11': '「11号」', 'Soukaku': '苍角', 'Starlight Billy': '星徽·比利', 'Sunna': '千夏', 'Trigger': '「扳机」', 'Velina Airgid': '维琳娜', 'Vivian Banshee': '薇薇安•班希', 'Von Lycaon': '冯•莱卡恩', 'Wise': '哲', 'Yanagi Tsukishiro': '月城柳', 'Ye Shunguang': '叶瞬光', 'Yidhari Murphy': '伊德海莉', 'Yixuan': '仪玄', 'Yuzuha Ukinami': '浮波柚叶', 'Zhao': '照', 'Zhu Yuan': '朱鸢', 'Roxy Ifrita Pryce': '洛克茜', 'Claret Flint': '克拉蕾', 'Xiadeye': '夏布为荔', 'Xiayuexuxian': '夏月虚闲', 'Eous': '伊埃斯', 'Sharkboo': '鲨鱼布', 'Sumoboo': '扑击布', 'Amillion': '艾米莉安', 'Avocaboo': '果核布', 'Bagboo': '纸袋布', 'Bangvolver': '左轮布', 'Boollseye': '飞靶布', 'Booressure': '气压布', 'Safety': '阿全', 'Luckyboo': '招财布', 'Exploreboo': '寻宝布', 'Butler': '巴特勒', 'Cryboo': '泪眼布', 'Devilboo': '恶魔布', 'Electroboo': '电击布', 'Paperboo': '纸壳布', 'Magnetiboo': '磁力布', 'Penguinboo': '企鹅布', 'Plugboo': '插头布', 'Resonaboo': '共鸣布', 'Rocketboo': '飙速布', }; // ==================== 配置 ==================== const CONFIG = { excludeSelectors: [ 'script', 'style', 'code', 'pre', 'textarea', 'input', 'noscript', 'svg', 'math', '[class*="code"]', '[class*="hljs"]', '[class*="ace_editor"]' ], attributes: ['title', 'alt', 'placeholder', 'aria-label'], translateTitle: false, debounceMs: 80, maxObserveTime: 60000, // 延迟兜底扫描时间(毫秒),用于捕获延迟加载内容 delayedScanIntervals: [500, 1500, 3000] }; // ==================== 核心引擎 ==================== let isProcessing = false; let debounceTimer = null; let observer = null; const observeStartTime = Date.now(); // 按原文长度降序构建替换规则 const rules = Object.entries(dictionary).map(([key, val]) => { const isRegex = key.startsWith('/') && key.lastIndexOf('/') > 0; if (isRegex) { const lastSlash = key.lastIndexOf('/'); const pattern = key.slice(1, lastSlash); const flags = key.slice(lastSlash + 1) || 'gi'; return { regex: new RegExp(pattern, flags), replacement: val }; } return { regex: new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), replacement: val }; }).sort((a, b) => b.regex.source.length - a.regex.source.length); function isExcluded(el) { if (!el || el.nodeType !== Node.ELEMENT_NODE) return false; for (const sel of CONFIG.excludeSelectors) { if (el.matches(sel) || el.closest(sel)) return true; } return false; } // 在原始文本上收集所有匹配,一次性替换 function translateText(text) { if (!text || typeof text !== 'string') return null; const matches = []; for (const rule of rules) { rule.regex.lastIndex = 0; let match; while ((match = rule.regex.exec(text)) !== null) { matches.push({ start: match.index, end: match.index + match[0].length, replacement: rule.replacement }); if (!rule.regex.global) break; if (match.index === rule.regex.lastIndex) { rule.regex.lastIndex++; } } rule.regex.lastIndex = 0; } if (matches.length === 0) return null; matches.sort((a, b) => { if (a.start !== b.start) return a.start - b.start; return b.end - a.end; }); const filtered = []; let lastEnd = -1; for (const m of matches) { if (m.start >= lastEnd) { filtered.push(m); lastEnd = m.end; } } let result = text; for (let i = filtered.length - 1; i >= 0; i--) { const m = filtered[i]; result = result.substring(0, m.start) + m.replacement + result.substring(m.end); } return result; } function translateTextNode(node) { const parent = node.parentElement; if (!parent || isExcluded(parent)) return; const original = node.textContent; const newText = translateText(original); // 只有当文本确实发生变化时才修改,避免无意义 mutation if (newText !== null && newText !== original) { node.textContent = newText; } } function translateAttributes(el) { if (isExcluded(el)) return; for (const attr of CONFIG.attributes) { const val = el.getAttribute(attr); if (val) { const newVal = translateText(val); if (newVal !== null && newVal !== val) { el.setAttribute(attr, newVal); } } } } function walk(root) { if (isProcessing) return; isProcessing = true; observer?.disconnect(); const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, null, false ); const textNodes = []; let node; while ((node = walker.nextNode())) { textNodes.push(node); } for (const n of textNodes) { translateTextNode(n); } if (root.nodeType === Node.ELEMENT_NODE) { const elements = root.querySelectorAll('*'); for (const el of elements) translateAttributes(el); translateAttributes(root); } isProcessing = false; if (Date.now() - observeStartTime <= CONFIG.maxObserveTime) { observer?.observe(document.body, { childList: true, subtree: true, characterData: true }); } } function translateTitle() { if (!CONFIG.translateTitle) return; const newTitle = translateText(document.title); if (newTitle !== null && newTitle !== document.title) { document.title = newTitle; } } function onMutations(mutations) { if (Date.now() - observeStartTime > CONFIG.maxObserveTime) { observer?.disconnect(); return; } clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { const targets = new Set(); for (const m of mutations) { if (m.type === 'childList') { for (const n of m.addedNodes) { if (n.nodeType === Node.ELEMENT_NODE) targets.add(n); else if (n.nodeType === Node.TEXT_NODE) targets.add(n); } } else if (m.type === 'characterData') { const n = m.target; if (n.nodeType === Node.TEXT_NODE) { targets.add(n); } } } for (const n of targets) { if (n.nodeType === Node.ELEMENT_NODE) walk(n); else translateTextNode(n); } translateTitle(); }, CONFIG.debounceMs); } function init() { if (!document.body) return; translateTitle(); walk(document.body); observer = new MutationObserver(onMutations); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); // 延迟兜底扫描:GameBanana 很多内容是异步加载的 for (const delay of CONFIG.delayedScanIntervals) { setTimeout(() => { if (Date.now() - observeStartTime <= CONFIG.maxObserveTime) { walk(document.body); } }, delay); } setTimeout(() => observer?.disconnect(), CONFIG.maxObserveTime); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();