PokeRogue-Pokedex-Translator

PokeRogue Pokedex 항목을 한국어로 번역합니다.

// ==UserScript==
// @name         PokeRogue-Pokedex-Translator
// @namespace    https://github.com/manhattanhouse/poke_kor
// @version      7.0
// @description  PokeRogue Pokedex 항목을 한국어로 번역합니다.
// @author       manhattanhouse
// @match        https://ydarissep.github.io/PokeRogue-Pokedex/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=github.io
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(async function () {
    'use strict';

    const json_url = "https://raw.githubusercontent.com/manhattanhouse/poke_kor/main/pokedex_trans.json";
    const translations = await fetchJsonData(json_url);

    // JSON 데이터 fetch
    async function fetchJsonData(url) {
        const response = await fetch(url);
        return response.json();
    }

    /*
    // 첫 글자를 제외하고 소문자로 변경
    function lowerText(text) {
        return text.charAt(0) + text.slice(1).toLowerCase();
    }

    // 메뉴 번역
    function addButtonsEventListener() {
        const buttons = document.getElementById('tableButton').getElementsByTagName('button');
        const buttonTexts = ["특성", "포켓몬", "기술", "바이옴", "트레이너", "아이템"];
        Array.from(buttons).forEach((button, index) => {
            button.innerText = buttonTexts[index];
        });
    }
    */
    // 셀 번역
    function translateCell(cell, translationDict, classPrefix = '') {
        const originalText = cell.textContent.trim();
        const translatedText = translationDict[originalText] /*|| translationDict[lowerText(cell.className.split(' ')[0].replace(classPrefix, ''))]*/;
        if (translatedText) {
            cell.textContent = translatedText;
        }
    } 
    /*
    // 행 번역
    function translateRow(row, translations) {
        translateCell(row.querySelector('.nameContainer .species'), translations.names);
        row.querySelectorAll('.types .background').forEach(cell => translateCell(cell, translations.types, 'TYPE_'));
        row.querySelectorAll('.abilities div').forEach(cell => translateCell(cell, translations.abilities));
        row.querySelectorAll('.italic').forEach(cell => translateCell(cell, translations.headers));
    }
    
    // 기술 행 번역
    function translateMovesRow(row, translations) {
        const moveCell = row.querySelector('.move');
        const originalText = moveCell.textContent.trim();
        const matchedText = originalText.match(/\((.)\)/);
        const originalMoveText = moveCell.textContent.replace(/\(.\)/, '').trim();
        const moveData = translations.moves[originalMoveText];

        if (moveData) {
            if (matchedText) {
                moveCell.textContent = `${moveData.name_kr} (${matchedText[1]})`;
            } else {
                moveCell.textContent = moveData.name_kr;
            }

            const effectCell = row.querySelector('.effect');
            if (effectCell) {
                const effectTranslation = translations.move_effect.find(effect => effect[0] === effectCell.textContent.trim());
                if (effectTranslation) effectCell.textContent = effectTranslation[1];
            }

            const descriptionCell = row.querySelector('.description div');
            if (descriptionCell) {
                if (effectCell.textContent.length > 0 && moveData.Effect_kr.length < 44) {
                    descriptionCell.textContent = moveData.Effect_kr;
                } else {
                    descriptionCell.outerHTML = moveData.Effect_kr.includes(". ") ? moveData.Effect_kr.replace(". ", ". \n").split("\n").map(part => `<div>${part}</div>`).join("") : splitMiddle(moveData.Effect_kr, 35).map(part => `<div>${part}</div>`).join("");
                }
            }

            translateCell(row.querySelector('td.type div:first-child'), translations.types, 'TYPE_');
        }
    }

    // 헤더 번역
    function translateHeader(headers, translations) {
        headers.forEach(cell => translateCell(cell, translations));
    }
    */
    // 필터 목록 번역
    function translateSpeciesFilterList(tableFilters) {
        tableFilters.forEach(item => {
            const spans = item.querySelectorAll('span');
            if (spans.length >= 2) {
                const firstSpan = spans[0];
                const secondSpan = spans[1];
                if (!secondSpan.hasAttribute('data-original-text')) {
                    const originalText = secondSpan.textContent.trim();
                    secondSpan.setAttribute('data-original-text', originalText);
                    const className = firstSpan.className.trim();

                    const translationOptions = {
                        "Form": {
                            "text": "폼: ",
                            "data": translations.form
                        },
                        "Type": {
                            "text": "타입: ",
                            "data": translations.types
                        },
                        "Ability": {
                            "text": "특성: ",
                            "data": translations.abilities
                        },
                        "Biome": {
                            "text": "바이옴: ",
                            "data": translations.biomes
                        },
                        "Split": {
                            "text": "분류: "
                        },
                        "Flag": {
                            "text": "플래그: "
                        },
                        "Target": {
                            "text": "타겟: "
                        }
                    }

                    const option = translationOptions[className]
                    let translatedText = '';

                    if (option && option['data']) {
                        translatedText = option.data[originalText];
                        firstSpan.innerText = option.text;
                    } else if (option) {
                        for (var filter of translations.move_filters) {
                            if (filter[0] == originalText) {
                                translatedText = filter[1];
                                break;
                            }
                        }
                        firstSpan.innerText = option.text;
                    } else if (className == 'Move') {
                        Object.values(translations.moves).forEach(({ name, name_kr }) => {
                            if (name.includes(originalText)) {
                                translatedText = name_kr;
                            }
                        });
                        firstSpan.innerText = '기술: ';
                    }

                    if (translatedText) {
                        secondSpan.textContent = translatedText;
                    }
                }
            }
        });
    }

    // 팝업 번역
    function translatePopup(translations) {
        const popup = document.getElementById('popup');
        if (popup) {
            /*
            const title = popup.querySelector('h2');
            const originalText = title.textContent.trim();
            const matchedText = originalText.match(/\((.)\)/);
            const originalMoveText = title.textContent.replace(/\(.\)/, '').trim();
            const moveData = translations.moves[originalMoveText];
            if (moveData) {
                if (matchedText) {
                    title.textContent = `${moveData.name_kr} (${matchedText[1]})`;
                } else {
                    title.textContent = moveData.name_kr;
                }
                const description = popup.querySelector('.popupTrainerMoveDescription');
                if (description) description.textContent = moveData.Effect_kr;

                popup.querySelectorAll('.popupTrainerMoveStat').forEach(stat => {
                    Object.keys(translations.moves_head).forEach(key => {
                        if (stat.innerText.includes(key)) {
                            stat.innerText = stat.innerText.replace(key, translations.moves_head[key]);
                        }
                    });
                });

                const effectElement = popup.querySelector('.popupTrainerMoveEffect');
                if (effectElement) {
                    const effectTranslation = translations.move_effect.find(effect => effect[0] === effectElement.textContent.trim());
                    if (effectTranslation) effectElement.textContent = effectTranslation[1];
                }
            }
            */
            popup.querySelectorAll('.hyperlink').forEach(link => {
                const filterTranslation = translations.move_filters.find(filter => filter[0] == link.innerText);
                if (filterTranslation) link.innerText = filterTranslation[1];
            });
            translateCell(popup.querySelector('.popupTrainerMoveType'), translations.types);
            popup.querySelector('.popupFilterButton').textContent = '필터';
        }
    }

    /*
    // 요소 관찰
    function observeElement(selector, callback) {
        const observer = new MutationObserver((mutationsList, observer) => {
            for (let mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    const element = document.querySelector(selector);
                    if (element) {
                        callback();
                        break;
                    }
                }
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    // 노드 번역
    function translateNodes(nodes, jsonData) {
        nodes.forEach(node => {
            const translatedText = jsonData[node.innerText];
            if (translatedText) node.innerText = translatedText;
        });
    }

    // 선택자로 번역
    function transElement(selector, key, text = null) {
        const element = document.querySelector(selector);
        if (element) {
            if (text !== null) {
                element.innerText = text;
            } else {
                let name_key = element.innerText;
                if (key === 'types') name_key = lowerText(name_key);
                if (translations[key][name_key]) {
                    element.innerText = translations[key][name_key];
                }
            }
        }
    }
    */

    let previousUrl = window.location.href;
    //let previousHeight = 0;
    //let previousHeightAb = 0;

    function checkUrlChange() {
        const currentUrl = window.location.href;
        //const currentHeight = document.querySelector("#locationsTable").offsetHeight;
        //const currentHeightAb = document.querySelector("#abilitiesTable").offsetHeight;

        if (currentUrl !== previousUrl /*|| previousHeight !== currentHeight*/) {
            previousUrl = currentUrl;
            //previousHeight = currentHeight
            mainScript();
        } /*else if (previousHeightAb !== currentHeightAb) {
            transAbilities();
        }*/
    }

    const observer = new MutationObserver(checkUrlChange);
    observer.observe(document, { childList: true, subtree: true });

    /*

    observeElement('#speciesTable tbody', () => {
        document.querySelectorAll('#speciesTable tbody tr').forEach(row => translateRow(row, translations));
    });

    observeElement('#movesTableTbody', () => {
        document.querySelectorAll('#movesTableTbody tr').forEach(row => translateMovesRow(row, translations));
    });

    */

    const selectors = ['#speciesInput', '#movesInput', '#locationsInput', '#abilitiesInput'];
    const filterSelectors = ['#speciesFilterList .tableFilter', '#movesFilterList .tableFilter', '#locationsFilterList .tableFilter', false];

    setTimeout(() => {
        mainScript();

        /*
        const locationsBody = document.querySelector('#locationsTableTbody');
        observeElement('#locationsTableTbody', () => {
            const currentHeight = locationsBody.offsetHeight;
            if (previousHeight != currentHeight) {
                mainScript();
            }
        });
        */

        const searchInputs = selectors.map(selector => document.querySelector(selector));

        // 검색창 한국어 -> 영어 변환
        searchInputs.forEach((input, index) => {
            input.addEventListener('keyup', (event) => {
                const filterSelector = filterSelectors[index];
                /*
                if (event.key === 'Enter') {
                    const query = input.value.trim();
                    let value = '';
                    if (filterSelector) {
                        value = translations.reverse[query] || 
                            Object.keys(translations.biomes).find(key => translations.biomes[key] === query) ||
                                Object.keys(translations.rarityHead).find(key => translations.rarityHead[key] === query) || 
                                    reverseTranslatorMove(query);
                    } else {
                        value = Object.keys(translations.abilities).find(key => translations.abilities[key] === query);
                    }
                    if (value) {
                        setTimeout(() => {
                            input.value = value;
                        }, 0);
                    }
                    checkUrlChange();
                }
                */
                if (!filterSelector) return;
                setTimeout(() => { toggleFilter(input, filterSelector); }, 100);        
            });
        });

        const dropmenu_input = document.querySelector('input#speciesPanelInputSpecies');
        dropmenu_input.addEventListener('input', () => {
            const query = dropmenu_input.value.trim();
            if (query) {
                let value = translations.reverse[query];
                if (value) {
                    setTimeout(() => {
                        dropmenu_input.blur();
                        dropmenu_input.focus();
                        dropmenu_input.value = value;
                        const species = `SPECIES_${value.replaceAll(" ", "_").toUpperCase()}`
                        createSpeciesPanel(species)
                        speciesPanelInputSpecies.blur()
                        speciesPanelInputSpecies.value = ""
                    }, 0);
                }
            }
        });

        // IME 중복 입력 방지

        dropmenu_input.addEventListener('blur', function(event) {
            const value = this.value
            this.value = '';
            this.value = value;
        });

        const popupObserver = new MutationObserver(() => translatePopup(translations));
        popupObserver.observe(document.getElementById('popup'), { childList: true, subtree: true });

    }, 1000);

    // 일치하는 태그 토글
    function toggleFilter(input, selector) {
        const query = input.value.trim();
        const hasKr = /([\uAC00-\uD7A3]){2,}/.test(query);
        const filterListItems = document.querySelectorAll(selector);
        filterListItems.forEach(item => {
            if (query.length < 3 && !hasKr) {
                item.style.removeProperty('display');
                return
            }
            const span = Array.from(item.querySelectorAll('span')).find(span => 
                (span.getAttribute('data-original-text') && span.getAttribute('data-original-text').includes(query)) || 
                (span.innerText && span.innerText.includes(query))
            );
            if (span) {
                item.style = 'display: inline-block !important;';
            } else {
                item.style.removeProperty('display');
            }
        });
    }

    // 메인 스크립트
    function mainScript() {
        const currentUrl = window.location.href;

        //addButtonsEventListener();

        if (currentUrl.includes('?table=speciesTable')) {
            translateSpeciesFilterList(document.querySelectorAll('#speciesFilterList .tableFilter'));
            toggleFilter(document.querySelector(selectors[0]), filterSelectors[0]);
            /*
            const variantNode = document.querySelector('#onlyShowVariantPokemon');
            if (variantNode) {
                variantNode.innerText = '변종';
            }
            translateHeader(document.querySelectorAll('#speciesTableThead th'), translations.headers);
            document.querySelectorAll('#speciesTable tbody tr').forEach(row => translateRow(row, translations));
            */

        } else if (currentUrl.includes('?table=movesTable')) {
            translateSpeciesFilterList(document.querySelectorAll('#movesFilterList .tableFilter'));
            toggleFilter(document.querySelector(selectors[1]), filterSelectors[1]);
            /*
            translateHeader(document.querySelectorAll('#movesTableThead th'), translations.moves_head);
            document.querySelectorAll('#movesTableTbody tr').forEach(row => translateMovesRow(row, translations));
            */

        } else if (currentUrl.includes('?species')) {
            speciesPage(translations);
            //translateNodes(document.querySelectorAll('#speciesPanelBiomesContainer .speciesPanelText'), { "바이옴:": "바이옴:" });
            //translateNodes(document.querySelectorAll('#speciesPanelBiomesContainer .hyperlink'), translations.biomes);

        } else if (currentUrl.includes('?table=locationsTable')) {
            translateSpeciesFilterList(document.querySelectorAll('#locationsFilterList .tableFilter'));
            toggleFilter(document.querySelector(selectors[2]), filterSelectors[2]);
            /*
            const variantNode = document.querySelector('#onlyShowVariantPokemonLocations');
            if (variantNode) {
                variantNode.innerText = '변종';
            }

            const timeData = { "Dawn": "아침", "Day": "낮", "Dusk": "저녁", "Night": "밤" };
            const nodesToTranslate = {
                '.locationSpeciesName': translations.names,
                '.rarityTableThead': translations.rarityHead,
                '.locationName': translations.biomes,
                '.timeOfDay': timeData
            };

            Object.keys(nodesToTranslate).forEach(selector => {
                const jsonData = nodesToTranslate[selector];
                translateNodes(document.querySelectorAll(selector), jsonData);
            });

            document.querySelectorAll('.previousLinkInfo, .nextLinkInfo').forEach(info => {
                const textNode = info.childNodes[0];
                if (translations.biomes[textNode.textContent]) {
                    textNode.textContent = translations.biomes[textNode.textContent];
                }
            });
            */

        } else if (currentUrl.includes('?table=abilitiesTable')) {
            /*
            document.querySelectorAll('#abilitiesTableThead th').forEach(th => {
                if (th.className == 'ability') th.textContent = '이름';
                else if (th.className == 'description') th.textContent = '설명';
            });
            transAbilities();
            */
        }
    }
    /*
    // 특성 페이지 번역 함수
    function transAbilities() {
        const cells = document.querySelectorAll('#abilitiesTableTbody td.ability');
        cells.forEach(cell => {
            const childNodes = cell.childNodes;
            if (childNodes) {
                const textNode = childNodes[0];
                const parts = textNode.textContent.split(' (');
                const key = parts[0];
                if (translations.abilities[key]) {
                    const des_key = translations.abilities[key];
                    if (parts[1]) {
                        textNode.textContent = `${des_key} (${parts[1]}`;
                    } else {
                        textNode.textContent = des_key;
                    }
                    const des = cell.nextElementSibling;
                    if (des && translations.abilities_kr[des_key]) {
                        des.textContent = translations.abilities_kr[des_key];
                    }
                }
            }
        })
    }

    // 기술 페이지 역번역 함수
    function reverseTranslatorMove(query) {
        if (translations.move_reverse[query]) {
            return translations.move_reverse[query];
        }
        const filterTranslation = translations.move_filters.find(filter => filter[1] === query);
        if (filterTranslation) return filterTranslation[0];

        const move = Object.keys(translations.moves).find(key => translations.moves[key].name_kr === query);
        if (move) return move;

        const type = Object.keys(translations.types).find(key => translations.types[key] === query);
        if (type) return type;

        return false;
    }
    */

    // key to value replace 함수
    function translateText(text, dictionary, key) {
        if (dictionary[key]) {
            if (dictionary[key]["name_kr"]) {
                return text.replace(key, dictionary[key].name_kr);
            }
            return text.replace(key, dictionary[key]);
        }
        return text;
    }

    // 포켓몬 상세 페이지 번역
    function speciesPage(translations) {
        const dropmenu = document.querySelectorAll('datalist#speciesPanelInputSpeciesDataList option');
        dropmenu.forEach(option => {
            if (translations.names[option.textContent]) {
                option.textContent = translations.names[option.textContent];
            }
        })

        /*
        const translationMappings = [
            { selector: '#speciesName', key: 'names' },
            { selector: '#speciesType1', key: 'types' },
            { selector: '#speciesType2', key: 'types' },
            { selector: '#speciesType3', key: 'types' }
        ];
        
        translationMappings.forEach(mapping => {
            transElement(mapping.selector, mapping.key, mapping.text);
        });
        */

        const panelText = {
            //"Types:": "타입:",
            //"Starter Cost:": "코스트:",
            //"Abilities:": "특성:",
            //"Biomes:": "바이옴:",
            "Evolution:": "진화:",
            "Formes:": "폼:"
        }

        const panels = document.querySelectorAll('.speciesPanelText');
        panels.forEach(panel => {
            if (panelText[panel.innerText]) {
                panel.innerText = panelText[panel.innerText];
            }
        })

        /*
        const abilities = document.querySelector('#speciesAbilities');
        if (abilities) {
            abilities.querySelectorAll('span.hyperlink').forEach(link => {
                if (translations.abilities[link.innerText]) {
                    link.innerText = translations.abilities[link.innerText];
                    const des = link.nextElementSibling;
                    des.textContent = translations.abilities_kr[link.innerText.split(' ')[0]];
                }
            });
        }

        const stats = document.querySelector('#statsSection');
        if (stats) {
            let replaceNames = { "Atk": "공격", "Def": "방어", "SpA": "특공", "SpD": "특방", "Spe": "속도", "BST": "합계" };
            Object.entries(replaceNames).forEach(([key, value]) => {
                stats.innerHTML = stats.innerHTML.replace(new RegExp(key, 'g'), value);
            });
        }
        */

        const evoDiv = document.querySelector('#speciesEvoTable');
        const regex = /Item In Biome (.+) \((.+)\)/;
        const regex_move = /Level Move ([^\(]+) \((\d+)\)/;
        const regex_item = /\(([^)]+)\)/;
        function evoTrans(evoMethod) {
            Object.entries(translations.evol_long).forEach(([key, value]) => {
                evoMethod.innerText = evoMethod.innerText.replace(key, value);
            });
            let match = evoMethod.innerText.match(regex);
            if (match) {
                evoMethod.innerText = `[${match[1]}] \n바이옴에서 아이템 사용 (${match[2]})`;
                evoMethod.innerText = translateText(evoMethod.innerText, translations.items, match[2]);
                let entries = Object.entries(translations.biomes);
                entries.sort((a, b) => b[0].length - a[0].length);
                entries.forEach(([key, value]) => {
                    evoMethod.innerText = evoMethod.innerText.replace(key, value+', ');
                });
                evoMethod.innerText = evoMethod.innerText.replace(', ]', ']');
            } else {
                match = evoMethod.innerText.match(regex_move);
                if (match) {
                    evoMethod.innerText = `[${match[1].trim()}] 기술을 익히고 레벨 (${match[2]})`;
                    evoMethod.innerText = translateText(evoMethod.innerText, translations.moves, match[1]);
                } else {
                    match = evoMethod.innerText.match(regex_item);
                    if (match) {
                        evoMethod.innerText = translateText(evoMethod.innerText, translations.items, match[1]);
                    }
                }
            }
            Object.entries(translations.evol).forEach(([key, value]) => {
                evoMethod.innerText = evoMethod.innerText.replace(key, value);
            });
        }
        if (evoDiv) {
            const evoMethods = evoDiv.querySelectorAll('.evoMethod');
            evoMethods.forEach(evoMethod => {
                evoTrans(evoMethod);
            });
        }
        /*
        const formDiv = document.querySelector('#speciesFormes');
        if (formDiv) {
            formDiv.querySelectorAll('.underline').forEach(form => {
                if (translations.names[form.innerText]) {
                    form.innerText = translations.names[form.innerText];
                }
            });
        }
        */

        const defensive = document.querySelector('#speciesDefensiveTypeChartContainer');
        if (defensive) {
            defensive.querySelector('.bold').innerText = "공격받을 때";
            /*
            defensive.querySelectorAll('.backgroundSmall').forEach(type => {
                const key = lowerText(type.className.split(' ')[1].replace('TYPE_', ''));
                if (translations.types[key]) {
                    type.innerText = translations.types[key];
                }
            });
            */
        }

        const offensive = document.querySelector('#speciesOffensiveTypeChartContainer');
        if (offensive) {
            offensive.querySelector('.bold').innerText = "공격할 때";
            /*
            offensive.querySelectorAll('.backgroundSmall').forEach(type => {
                const key = lowerText(type.className.split(' ')[1].replace('TYPE_', ''));
                if (translations.types[key]) {
                    type.innerText = translations.types[key];
                }
            });
            */
        }

        const moveTables = document.querySelectorAll('.speciesPanelLearnsetsTableMargin');
        const transTexts = { "Egg Moves": "유전 기술", "Level-Up": "레벨업", "TM/HM": "기술머신/비전머신" };

        moveTables.forEach(table => {
            /*
            let shift = 0;
            const ths = table.querySelectorAll('thead th');
            ths.forEach(th => {
                if (translations.moves_head[th.innerText]) {
                    th.innerText = translations.moves_head[th.innerText];
                } else if (th.innerText === 'Level' || th.innerText === '레벨') {
                    th.innerText = '레벨';
                    shift = 1;
                }
            });
            */

            const caption = table.querySelector('caption.bold');
            const parts = caption.childNodes[0].textContent.split('\n');
            if (parts && transTexts[parts[0]]) {
                caption.childNodes[0].textContent = transTexts[parts[0]] + '\n' + parts[1];
            }
            /*
            table.querySelectorAll('tr').forEach(row => {
                const cells = row.querySelectorAll('td');
                const nameCell = cells[0 + shift];
                if (nameCell) {
                    const effectCell = cells[6 + shift];
                    const move = translations.moves[Object.keys(translations.moves).find(key => translations.moves[key].name === nameCell.innerText)];
                    if (move) {
                        nameCell.innerText = move.name_kr;
                        if (move.Effect_kr) {
                            effectCell.innerHTML = move.Effect_kr.includes(". ") ? move.Effect_kr.replace(". ", ". \n").split("\n").map(part => `<div>${part}</div>`).join("") : splitMiddle(move.Effect_kr, 35).map(part => `<div>${part}</div>`).join("");
                        }
                    }

                    const typeDiv = cells[1 + shift].querySelector('div');
                    if (typeDiv) {
                        const key = lowerText(typeDiv.className.split(' ')[0].replace('TYPE_', ''));
                        if (translations.types[key]) {
                            typeDiv.innerText = translations.types[key];
                        }
                    }
                }
            });
            */
        });
    }
    /*
    function splitMiddle(text, maxLength) {
        if (text.length <= maxLength) return [text];
        const middle = Math.floor(text.length / 2);
        let left = middle, right = middle;
        while (left > 0 && text[left] !== ' ') left--;
        while (right < text.length && text[right] !== ' ') right++;
        const splitIndex = (middle - left <= right - middle) ? left : right;
        return [text.substring(0, splitIndex), text.substring(splitIndex + 1)];
    }
    */
})();