Greasy Fork is available in English.
【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
// @connect cnb.cool
// ==/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 = `<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';
}
});
}
})();