Greasy Fork

Greasy Fork is available in English.

LogCN ——esologs中文全站翻译补丁

【ESOCN】为esologs全站提供中文翻译补丁 1.全站自动翻译装备名称 2.修复Unknown Item错误 3.翻译试炼、地下城、竞技场列表 4.翻译试炼BOSS列表 5.修复部分中文翻译错误

当前为 2025-08-13 提交的版本,查看 最新版本

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