// ==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();
});
})();