您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
【ESOCN】为esologs全站提供中文翻译补丁 1.全站自动翻译装备名称 2.修复Unknown Item错误 3.翻译试炼、地下城、竞技场列表 4.翻译试炼BOSS列表 5.修复部分中文翻译错误
// ==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 // @grant GM_getValue // @grant GM_setValue // @connect cnb.cool // ==/UserScript== (function() { 'use strict'; const DATA_URLS = { idToNameMap: 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/LogCN/idToNameMap.json', enNameToNameMap: 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/LogCN/enNameToNameMap.json', enZoneToCnMap: 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/LogCN/enZoneToCnMap.json', enDungeonToCnMap: 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/LogCN/enDungeonToCnMap.json', enBossToCnMap: 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/LogCN/enBossToCnMap.json', enarenaToCnMap: 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/LogCN/enarenaToCnMap.json', enEnchantmentToCnMap: 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/LogCN/enEnchantmentToCnMap.json', cnEnchantmentToCnMap: 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/LogCN/cnEnchantmentToCnMap.json', enTraitToCnMap: 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/LogCN/enTraitToCnMap.json', cnTraitToCnMap: 'https://cnb.cool/ESOCN/ESOCN/-/git/raw/main/src/Data/LogCN/cnTraitToCnMap.json' }; let idToNameMap = {}; let enNameToNameMap = {}; let enZoneToCnMap = {}; let enDungeonToCnMap = {}; let enBossToCnMap = {}; let enarenaToCnMap = {}; let enEnchantmentToCnMap = {}; let cnEnchantmentToCnMap = {}; let enTraitToCnMap = {}; let cnTraitToCnMap = {}; let isEquipmentDataReady = false; let isTrialsDataReady = false; let isDungeonsDataReady = false; let isArenaDataReady = false; let isEnchantmentReady = false; let isTraitReady = false; let isTranslating = false; async function fetchAndCacheData(name, url) { const cacheKey = `${name}Cache`; const cachedData = GM_getValue(cacheKey); if (cachedData) { return cachedData; } const headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36', 'Referer': 'https://cnb.cool/', }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, headers: headers, onload: (response) => { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText.replace(/\r|\n/g, '')); GM_setValue(cacheKey, data); resolve(data); } catch (error) { console.error(`Failed to parse data for ${name}:`, error); reject(error); } } else { reject(new Error(`HTTP Error: ${response.status} for ${name}`)); } }, onerror: (error) => { console.error(`Network error for ${name}:`, error); reject(error); } }); }); } async function main() { if (!isEquipmentDataReady) await fetchEquipmentData(); if (!isTrialsDataReady) await fetchTrialsData(); if (!isDungeonsDataReady) await fetchDungeonsData(); if (!isArenaDataReady) await fetchArenaData(); if (!isEnchantmentReady) await fetchEnchantmentData(); if (!isTraitReady) await fetchTraitData(); translateTrialButton(); observeZoneMenu(); const url = new URL(window.location.href); if (url.pathname.includes('/reports/')) { if (!observer.isObserving) { observer.observe(document.body, { childList: true, subtree: true }); observer.isObserving = true; } } else if (url.pathname.includes('/rankings/')) { processRankingsPage(); addExpandCollapseAllButton(); 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 || !isArenaDataReady) { return; } const menuObserver = new MutationObserver((mutations) => { 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')) { translateTrialMenu(menu); } else if (menuText.includes('Bal Sunnar') || menuText.includes('Fungal Grotto I')) { translateDungeonMenu(menu); } else if (menuText.includes('Maelstrom Arena') || menuText.includes('Vale of the Surreal')) { translateArenaonMenu(menu); } } }); menuObserver.observe(document.body, { childList: true, subtree: 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 = ''; 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 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 translateArenaonMenu(menuContainer) { 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; } }); } 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 || ! isTraitReady || !isEnchantmentReady || 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(); }; } async function fetchData() { try { await Promise.all([ fetchEquipmentData(), fetchTrialsData(), fetchDungeonsData(), fetchArenaData(), fetchEnchantmentData(), fetchTraitData() ]); } catch (error) { console.error('An error occurred during data fetching:', error); } } async function fetchEquipmentData() { if (isEquipmentDataReady) return; try { [idToNameMap, enNameToNameMap] = await Promise.all([ fetchAndCacheData('idToNameMap', DATA_URLS.idToNameMap), fetchAndCacheData('enNameToNameMap', DATA_URLS.enNameToNameMap) ]); isEquipmentDataReady = true; } catch (error) { console.error('Failed to fetch equipment data:', error); } } async function fetchTrialsData() { if (isTrialsDataReady) return; try { [enZoneToCnMap, enBossToCnMap] = await Promise.all([ fetchAndCacheData('enZoneToCnMap', DATA_URLS.enZoneToCnMap), fetchAndCacheData('enBossToCnMap', DATA_URLS.enBossToCnMap) ]); isTrialsDataReady = true; } catch (error) { console.error('Failed to fetch trials data:', error); } } async function fetchDungeonsData() { if (isDungeonsDataReady) return; try { enDungeonToCnMap = await fetchAndCacheData('enDungeonToCnMap', DATA_URLS.enDungeonToCnMap); isDungeonsDataReady = true; } catch (error) { console.error('Failed to fetch dungeons data:', error); } } async function fetchArenaData() { if (isArenaDataReady) return; try { enarenaToCnMap = await fetchAndCacheData('enarenaToCnMap', DATA_URLS.enarenaToCnMap); isArenaDataReady = true; } catch (error) { console.error('Failed to fetch arena data:', error); } } async function fetchEnchantmentData() { if (isEnchantmentReady) return; try { [enEnchantmentToCnMap, cnEnchantmentToCnMap] = await Promise.all([ fetchAndCacheData('enEnchantmentToCnMap', DATA_URLS.enEnchantmentToCnMap), fetchAndCacheData('cnEnchantmentToCnMap', DATA_URLS.cnEnchantmentToCnMap) ]); isEnchantmentReady = true; } catch (error) { console.error('Failed to fetch enchantment data:', error); } } async function fetchTraitData() { if (isTraitReady) return; try { [enTraitToCnMap, cnTraitToCnMap] = await Promise.all([ fetchAndCacheData('enTraitToCnMap', DATA_URLS.enTraitToCnMap), fetchAndCacheData('cnTraitToCnMap', DATA_URLS.cnTraitToCnMap) ]); isTraitReady = true; } catch (error) { console.error('Failed to fetch trait data:', error); } } function processReportsPage() { if (!isEquipmentDataReady || ! isTraitReady || !isEnchantmentReady) 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 setCell = row.querySelector('td:nth-child(5)'); const traitCell = row.querySelector('td:nth-child(6)') const enchantmentCell = row.querySelector('td:nth-child(7)') if (anchor && nameSpan && setCell) { 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; setCell.innerText = translatedName; nameCell.dataset.translated = 'true'; } } if (enTraitToCnMap[traitCell.innerText]) traitCell.innerText = enTraitToCnMap[traitCell.innerText] else traitCell.innerText = cnTraitToCnMap[traitCell.innerText] if (enEnchantmentToCnMap[enchantmentCell.innerText]) enchantmentCell.innerText = enEnchantmentToCnMap[enchantmentCell.innerText] else enchantmentCell.innerText = cnEnchantmentToCnMap[enchantmentCell.innerText] }); } 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.previousElementSibling !== clickedRow) { return; } 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 = `<img class="rankings-gear-image" src="${img ? img.src : ''}" alt="${translatedName}" loading="lazy">${translatedName}`; } } }); gearRow.dataset.translated = 'true'; } observer.disconnect(); } }); } } }); tempObserver.observe(parentBody, { childList: true }); }); disclosureSpan.dataset.listenerAttached = 'true'; } }); } function addExpandCollapseAllButton() { if (document.getElementById('display-all-or-none')) { return; } const menubar = document.getElementById('rankings-menubar'); if (!menubar) return; const li = document.createElement('li'); li.id = 'display-all-or-none'; const button = document.createElement('a'); button.href = '#'; button.innerText = '展开/折叠全部装备'; button.className = 'filter-item has-submenu'; button.style='padding-right: 12px; color: rgb(241, 195, 60)!important' li.appendChild(button); menubar.appendChild(li); button.addEventListener('click', (event) => { event.preventDefault(); const disclosureSpans = document.querySelectorAll('span.disclosure.zmdi.zmdi-caret-down'); if (disclosureSpans.length === 0) return; const expandedCount = document.querySelectorAll('span.disclosure[data-expanded="true"]').length; const shouldCollapse = expandedCount > 0; disclosureSpans.forEach(span => { const isExpanded = span.getAttribute('data-expanded') === 'true'; if (shouldCollapse) { if (isExpanded) { span.click(); } } else { if (!isExpanded) { span.click(); } } }); }); } fetchData().then(() => { main(); listenForUrlChange(); }); })();