// ==UserScript== // @name LogCN ——esologs中文全站翻译补丁 // @namespace http://tampermonkey.net/ // @version 1.0 // @license MIT // @icon https://images.uesp.net/1/15/ON-icon-Elsweyr.png // @description 【ESOCN】为esologs全站提供中文翻译补丁 1.全站自动翻译装备名称 2.修复Unknown Item错误 3.翻译试炼、地下城、竞技场列表 4.翻译试炼BOSS列表 5.修复部分中文翻译错误 // @author 苏@RodMajors // @match https://www.esologs.com/* // @match https://cn.esologs.com/* // @grant GM_xmlhttpRequest // @connect cnb.cool // @downloadURL none // ==/UserScript== (function() { 'use strict'; const EQUIPMENT_DATA_URL = 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/equipmentWithPart.json'; const TRIALS_DATA_URL = 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/trials.json'; const DUNGEONS_DATA_URL = 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/dungeons.json'; let idToNameMap = {}; let enNameToNameMap = {}; let enZoneToCnMap = {}; let enDungeonToCnMap = {}; let enBossToCnMap = { "shapers of flesh": "血肉塑形者", "hall of fleshcraft": "血肉塑形者", "jynorah and skorkhif": "吉诺拉和斯科尔基弗", "count ryelaz and zilyesset": "雷拉兹伯爵和齐利塞特", "archwizard twelvane and chimera": "首席巫师特尔乌万和奇美拉", "lylanar and turlassil": "莱拉纳尔和图拉塞尔", "the hunter killers": "猎杀者博西特洛克斯", "the refabrication committee": "监管委员会", "the twins": "双子", "the yokedas": "尤可达·凯", } ; let enarenaToCnMap = { "Dragonstar Arena": "龙星竞技场", "Blackrose Prison": "黑玫瑰监狱", "Maelstrom Arena": "漩涡竞技场", "Vale of the Surreal": "超现实之谷", "Seht\'s Balcony": "赛特的露台", "Drome of Toxic Shock": "中毒休克场地", "Seht\'s Flywheel": "赛特的飞轮", "Rink of Frozen Blood": "冻血溜冰场", "Spiral Shadows": "螺旋暗影", "Vault of Umbrage": "树荫拱顶", "Igneous Cistern": "火岩池", "Theater of Despair": "绝望剧场", "Arenas (Group)": "组队竞技场" } let isEquipmentDataReady = false; let isTrialsDataReady = false; let isDungeonsDataReady = false; let isTranslating = false; function main() { translateTrialButton(); observeZoneMenu(); const url = new URL(window.location.href); if (url.pathname.includes('/reports/')) { if (!isEquipmentDataReady) fetchEquipmentData(); if (!observer.isObserving) { observer.observe(document.body, { childList: true, subtree: true }); observer.isObserving = true; } } else if (url.pathname.includes('/rankings/')) { if (!isEquipmentDataReady) fetchEquipmentData(); processRankingsPage(); if (!observer.isObserving) { observer.observe(document.body, { childList: true, subtree: true }); observer.isObserving = true; } } else { if (observer.isObserving) { observer.disconnect(); observer.isObserving = false; } } } function observeZoneMenu() { // 确保所有数据都已加载,然后开始观察菜单 if (!isTrialsDataReady || !isDungeonsDataReady) { return; } const menuObserver = new MutationObserver((mutations, observer) => { const menu = document.querySelector('div.header__menu-wrapper--content'); if (menu) { const menuText = menu.innerText; if (menuText.includes('Iron Atronach') || menuText.includes('Halls of Fabrication')) { translateTrialObserver(menu); } else if (menuText.includes('Bal Sunnar') || menuText.includes('Fungal Grotto I')) { translateDungeonObserver(menu); } else if (menuText.includes('Maelstrom Arena')) { translateArenaObserver(menu) } } }); menuObserver.observe(document.body, { childList: true, subtree: true }); } function translateTrialObserver(menuContainer) { translateTrialMenu(menuContainer); const arenaObserver = new MutationObserver((mutations, observer) => { let shouldTranslate = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) for (const node of mutation.addedNodes) if (node.querySelector && (node.querySelector('.header-section-header__content-title a') || node.querySelector('.header-section-item__content-title'))) { shouldTranslate = true; break; } if (shouldTranslate) break; } if (shouldTranslate) { translateTrialMenu(menuContainer); } }); arenaObserver.observe(menuContainer, { childList: true, subtree: true, characterData: true }); } function translateTrialMenu(menuContainer) { const links = menuContainer.querySelectorAll('a'); const bosses = menuContainer.querySelectorAll('.header-section-item__content-title') bosses.forEach(boss => { const enName = boss.innerText.toLowerCase().replace(/’/g, '\''); let translatedName = ''; if (enBossToCnMap[enName]) { translatedName = enBossToCnMap[enName]; } if (translatedName) { boss.innerText = translatedName;; } }) links.forEach(link => { const enName = link.innerText.trim(); let translatedName = ''; // 特殊处理 'The Halls of Fabrication' if (enName === 'The Halls of Fabrication') { translatedName = enZoneToCnMap['Halls of Fabrication']; } else if (enName === 'Iron Atronach') { translatedName = "钢铁侍灵-打桩"; } else if (enZoneToCnMap[enName]) { translatedName = enZoneToCnMap[enName]; } if (translatedName) { link.innerText = translatedName; } }); } function translateDungeonObserver(menuContainer) { translateDungeonMenu(menuContainer); const arenaObserver = new MutationObserver((mutations, observer) => { let shouldTranslate = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) for (const node of mutation.addedNodes) if (node.querySelector && (node.querySelector('.header-section-header__content-title a') || node.querySelector('.header-section-item__content-title'))) { shouldTranslate = true; break; } if (shouldTranslate) break; } if (shouldTranslate) { translateDungeonMenu(menuContainer); } }); arenaObserver.observe(menuContainer, { childList: true, subtree: true, characterData: true }); } function translateDungeonMenu(menuContainer) { const dungeonTitleLink = menuContainer.querySelector('.header-section-header__content-title a') const dungeonLinks = menuContainer.querySelectorAll('.header-section-item__content-title'); const enName = dungeonTitleLink.innerText.trim(); if (enName === 'Dungeons') dungeonTitleLink.innerText = "地下城" dungeonLinks.forEach(link => { const enName = link.innerText.trim(); const translatedName = enDungeonToCnMap[enName]; if (translatedName) { link.innerText = translatedName; } }); } function translateArenaObserver(menuContainer) { translateArenaonMenu(menuContainer); const arenaObserver = new MutationObserver((mutations, observer) => { let shouldTranslate = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) for (const node of mutation.addedNodes) if (node.querySelector && (node.querySelector('.header-section-header__content-title a') || node.querySelector('.header-section-item__content-title'))) { shouldTranslate = true; break; } if (shouldTranslate) break; } if (shouldTranslate) { translateArenaonMenu(menuContainer); } }); arenaObserver.observe(menuContainer, { childList: true, subtree: true, characterData: true }); } function translateArenaonMenu(menuContainer) { const ArenaObserver = new MutationObserver((mutations, observer) => { const arenaTitleLink = menuContainer.querySelectorAll('.header-section-header__content-title a') const arenaLinks = menuContainer.querySelectorAll('.header-section-item__content-title'); arenaTitleLink.forEach(link => { const enName = link.innerText.trim(); const translatedName = enarenaToCnMap[enName]; if (translatedName) { link.innerText = translatedName; } }); arenaLinks.forEach(link => { const enName = link.innerText.trim(); const translatedName = enarenaToCnMap[enName]; if (translatedName) { link.innerText = translatedName; } }); }) ArenaObserver.observe(document.body, { childList: true, subtree: true }); } function translateTrialButton() { const trialButton = document.querySelector("#header-container > header > div.header__desktop > div.header-bottom-bar > div.header-bottom-bar__left > button.header-bottom-bar__item.header-bottom-bar__item--raid-content.eso"); const dungeonButton = document.querySelector("#header-container > header > div.header__desktop > div.header-bottom-bar > div.header-bottom-bar__left > button.header-bottom-bar__item.header-bottom-bar__item--dungeon-content.eso"); if (trialButton) { for (const node of trialButton.childNodes) { if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '尝试') { node.textContent = '试炼'; break; } } } if (dungeonButton) { for (const node of dungeonButton.childNodes) { if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === 'Dungeons') { node.textContent = '地下城'; break; } } } const buttonObserver = new MutationObserver((mutations, observer) => { const button = document.querySelector("#header-container > header > div.header__desktop > div.header-bottom-bar > div.header-bottom-bar__left > button.header-bottom-bar__item.header-bottom-bar__item--raid-content.eso"); const dungeonButton = document.querySelector("#header-container > header > div.header__desktop > div.header-bottom-bar > div.header-bottom-bar__left > button.header-bottom-bar__item.header-bottom-bar__item--dungeon-content.eso"); if (button) { for (const node of button.childNodes) { if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '尝试') { node.textContent = '试炼'; break; } } } if (dungeonButton) { for (const node of dungeonButton.childNodes) { if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === 'Dungeons') { node.textContent = '地下城'; break; } } } if (button && dungeonButton) { observer.disconnect(); } }); buttonObserver.observe(document.body, { childList: true, subtree: true }); } const observer = new MutationObserver(() => { if (!isEquipmentDataReady || isTranslating) return; isTranslating = true; const url = window.location.href; if (url.includes('/reports/')) { processReportsPage(); processSummaryRoles(); } else if (url.includes('/rankings/')) { processRankingsPage(); } isTranslating = false; }); observer.isObserving = false; function listenForUrlChange() { let lastUrl = location.href; const bodyObserver = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; main(); } }); bodyObserver.observe(document.body, { childList: true, subtree: true }); window.addEventListener('popstate', () => main()); const originalPushState = history.pushState; history.pushState = function() { originalPushState.apply(this, arguments); main(); }; } fetchData(); main(); listenForUrlChange(); function fetchData() { fetchEquipmentData(); fetchTrialsData(); fetchDungeonsData(); } function fetchEquipmentData() { if (isEquipmentDataReady) return; GM_xmlhttpRequest({ method: 'GET', url: EQUIPMENT_DATA_URL, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36', 'Referer': 'https://cnb.cool/', }, onload: function(response) { try { const equipmentData = JSON.parse(response.responseText); const equipmentList = Array.isArray(equipmentData) ? equipmentData : equipmentData.equipment; idToNameMap = {}; enNameToNameMap = {}; equipmentList.forEach(item => { if (item.partIDs && Array.isArray(item.partIDs)) { item.partIDs.forEach(id => { idToNameMap[id.toString()] = item.name; }); } if (item.partNames && Array.isArray(item.partNames)) { item.partNames.forEach(enName => { enNameToNameMap[enName] = item.name; }); } }); isEquipmentDataReady = true; main(); } catch (error) { console.error(error); } }, onerror: function(error) { console.error(error); } }); } function fetchTrialsData() { if (isTrialsDataReady) return; GM_xmlhttpRequest({ method: 'GET', url: TRIALS_DATA_URL, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36', 'Referer': 'https://cnb.cool/', }, onload: function(response) { try { const trialsData = JSON.parse(response.responseText); enZoneToCnMap = {}; trialsData.forEach(item => { enZoneToCnMap[item.enName] = item.name; if (item.BOSS) { item.BOSS.forEach(boss => { enBossToCnMap[boss.enName.trim().replace(/\u200e/g, '').toLowerCase()] = boss.name }) } }); isTrialsDataReady = true; main(); } catch (error) { console.error(error); } }, onerror: function(error) { console.error(error); } }); } function fetchDungeonsData() { if (isDungeonsDataReady) return; GM_xmlhttpRequest({ method: 'GET', url: DUNGEONS_DATA_URL, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36', 'Referer': 'https://cnb.cool/', }, onload: function(response) { try { console.log(response.responseText) const dungeonsData = JSON.parse(response.responseText); enDungeonToCnMap = {}; dungeonsData.forEach(item => { enDungeonToCnMap[item.enName] = item.name; }); isDungeonsDataReady = true; main(); } catch (error) { console.error('ESO Logs Helper: 获取或解析地下城数据失败。', error); } }, onerror: function(error) { console.error('ESO Logs Helper: GM_xmlhttpRequest 请求地下城数据失败。', error); } }); } function processReportsPage() { if (!isEquipmentDataReady) return; const gearDivs = document.querySelectorAll('div.filter-bar.miniature'); let gearTable; for (const div of gearDivs) { const divText = div.innerText.trim(); if (divText.includes('Gear') || divText.includes('装备')) { gearTable = div.nextElementSibling; if (gearTable && gearTable.classList.contains('summary-table')) { break; } } } if (!gearTable) return; const rows = gearTable.querySelectorAll('tbody tr'); rows.forEach(row => { const nameCell = row.querySelector('td:nth-child(4)'); if (!nameCell || nameCell.dataset.translated) return; const anchor = nameCell.querySelector('a'); const nameSpan = nameCell.querySelector('span'); const fifthCell = row.querySelector('td:nth-child(5)'); if (anchor && nameSpan && fifthCell) { const href = anchor.getAttribute('href'); let translatedName; const idMatch = href.match(/^(\d+)/); if (idMatch && idToNameMap[idMatch[1]]) { translatedName = idToNameMap[idMatch[1]]; } else { const englishName = nameSpan.innerText.trim(); translatedName = enNameToNameMap[englishName]; } if (translatedName) { nameSpan.innerText = translatedName; fifthCell.innerText = translatedName; nameCell.dataset.translated = 'true'; } } }); } function processSummaryRoles() { if (!isEquipmentDataReady) return; const containers = document.querySelectorAll('div.summary-role-container'); containers.forEach(container => { const secondCells = container.querySelectorAll('td:nth-child(2)'); secondCells.forEach(cell => { const links = cell.querySelectorAll('a:not([data-translated="true"])'); links.forEach(link => { const href = link.getAttribute('href'); let translatedName; const itemIdMatch = href.match(/(\d+)/); if (itemIdMatch && idToNameMap[itemIdMatch[1]]) { translatedName = idToNameMap[itemIdMatch[1]]; } else { const englishName = link.title.trim(); translatedName = enNameToNameMap[englishName]; } if (translatedName) { link.title = translatedName; const span = link.querySelector('span'); if (span) { span.innerText = translatedName; } link.dataset.translated = 'true'; } }); }); }); } function processRankingsPage() { if (!isEquipmentDataReady) { return; } const playerRows = document.querySelectorAll('table.summary-table tbody tr.odd, table.summary-table tbody tr.even'); playerRows.forEach((row, rowIndex) => { const disclosureSpan = row.querySelector('span.disclosure'); if (disclosureSpan && !disclosureSpan.dataset.listenerAttached) { disclosureSpan.addEventListener('click', (event) => { const clickedRow = event.currentTarget.closest('tr'); const parentBody = clickedRow.parentNode; const tempObserver = new MutationObserver((mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.tagName === 'TR' && node.querySelector('div.talents-and-gear')) { const gearRow = node; if (gearRow.dataset.translated) { observer.disconnect(); return; } const scripts = clickedRow.querySelectorAll('script'); let gearScript = null; for (const script of scripts) { if (script.innerText.includes('talentsAndGear') && script.innerText.includes('gear.push')) { gearScript = script; break; } } if (!gearScript) { observer.disconnect(); return; } const scriptContent = gearScript.innerText; const idRegex = /id:\s*(\d+)/g; let match; const ids = []; while ((match = idRegex.exec(scriptContent)) !== null) { ids.push(match[1]); } const gearItems = gearRow.querySelectorAll('td.rankings-gear-row'); if (gearItems.length > 0) { ids.forEach((id, index) => { if (gearItems[index]) { const translatedName = idToNameMap[id]; if (translatedName) { const img = gearItems[index].querySelector('img'); gearItems[index].innerHTML = `${translatedName}${translatedName}`; } } }); gearRow.dataset.translated = 'true'; } observer.disconnect(); } }); } } }); tempObserver.observe(parentBody, { childList: true }); }); disclosureSpan.dataset.listenerAttached = 'true'; } }); } })();