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

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