Greasyfork – Auto-Translator (v16)

Translates non-Latin languages (Chinese, Japanese, Korean, etc) but SKIPS Spanish and other Latin languages

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Greasyfork – Auto-Translator (v16)
// @namespace    http://tampermonkey.net/
// @version      16
// @description  Translates non-Latin languages (Chinese, Japanese, Korean, etc) but SKIPS Spanish and other Latin languages
// @author       Your Name
// @match        https://greasyfork.org/*/scripts*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      translate.googleapis.com
// ==/UserScript==

(function() {
    'use strict';

    console.log('🌐 Greasyfork Auto-Translator v16 loaded!');

    const CONFIG = {
        autoTranslate: true,
        debug: true
    };

    const processedElements = new WeakSet();
    let translationCount = 0;

    GM_addStyle(`
        .gf-translation-badge {
            display: inline-block;
            background: #4caf50;
            color: white;
            padding: 2px 6px;
            border-radius: 3px;
            font-size: 9px;
            margin-left: 6px;
            font-weight: bold;
            vertical-align: middle;
        }

        .gf-formatted-text {
            line-height: 1.6 !important;
            font-size: 14px !important;
        }

        .gf-formatted-text .gf-item {
            display: block !important;
            margin: 8px 0 !important;
            line-height: 1.6 !important;
        }

        .gf-formatted-text .gf-item strong {
            color: #2e7d32 !important;
            font-weight: 600 !important;
        }

        #gf-translator-panel {
            position: fixed;
            background: white;
            border: 2px solid #4caf50;
            border-radius: 8px;
            padding: 10px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
            z-index: 999999;
            font-family: Arial, sans-serif;
            min-width: 200px;
            max-width: 220px;
            cursor: move;
            user-select: none;
        }
        #gf-translator-panel.dragging { cursor: grabbing !important; }
        #gf-translator-panel:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.25); }
        #gf-translator-panel.minimized { min-width: 160px; padding: 8px; }

        .gf-panel-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 8px;
            padding-bottom: 6px;
            border-bottom: 2px solid #4caf50;
        }
        .gf-panel-title { font-weight: bold; color: #2e7d32; font-size: 14px; flex: 1; }
        .gf-panel-controls { display: flex; gap: 4px; }
        .gf-panel-btn {
            width: 20px; height: 20px; border: none; border-radius: 3px; cursor: pointer;
            font-size: 12px; display: flex; align-items: center; justify-content: center;
            transition: all 0.2s; background: #f0f0f0;
        }
        .gf-panel-btn:hover { transform: scale(1.1); }
        .gf-minimize-btn { background: #FFC107; color: white; }
        .gf-minimize-btn:hover { background: #FFB300; }
        .gf-close-btn { background: #f44336; color: white; }
        .gf-close-btn:hover { background: #e53935; }
        .gf-panel-content { display: block; }
        .gf-panel-content.hidden { display: none; }
        .gf-stat-box {
            margin-bottom: 8px; font-size: 11px; color: #555;
            background: #f1f8f4; padding: 6px; border-radius: 4px; text-align: center;
        }
        .gf-btn {
            width: 100%; padding: 7px; border: none; border-radius: 4px;
            cursor: pointer; font-weight: bold; font-size: 11px; margin-bottom: 5px;
            transition: all 0.2s;
        }
        .gf-btn:hover { transform: translateY(-1px); box-shadow: 0 3px 6px rgba(0,0,0,0.2); }
        .gf-btn-primary { background-color: #4caf50; color: white; }
        .gf-btn-primary:hover { background-color: #45a049; }
        .gf-btn-tertiary { background-color: #2196F3; color: white; }
        .gf-btn-tertiary:hover { background-color: #1976D2; }
        .gf-btn-danger { background-color: #f44336; color: white; font-size: 10px; padding: 6px; }
        .gf-btn-danger:hover { background-color: #e53935; }

        #gf-status-bar {
            position: fixed; bottom: 10px; right: 10px; background: #4caf50;
            color: white; padding: 10px 15px; border-radius: 5px;
            box-shadow: 0 3px 10px rgba(0,0,0,0.3); z-index: 999998;
            font-family: Arial, sans-serif; font-size: 12px; font-weight: bold;
        }
    `);

    function debugLog(...args) {
        if (CONFIG.debug) console.log('🌐 [v16]', ...args);
    }

    function showStatus(message, duration = 3000) {
        let statusBar = document.getElementById('gf-status-bar');
        if (!statusBar) {
            statusBar = document.createElement('div');
            statusBar.id = 'gf-status-bar';
            document.body.appendChild(statusBar);
        }
        statusBar.textContent = message;
        statusBar.style.display = 'block';
        setTimeout(() => statusBar.style.display = 'none', duration);
    }

    function hasNonLatinCharacters(text) {
        // Only detect NON-LATIN scripts: Chinese, Japanese, Korean, Cyrillic, Arabic, Thai, etc.
        // This EXCLUDES Spanish, French, Portuguese, Italian, German, etc.
        const nonLatinPattern = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f\u0400-\u04ff\u0600-\u06ff\u0e00-\u0e7f]/;
        return nonLatinPattern.test(text);
    }

    function isSpanishOrLatinLanguage(text) {
        // Check if text is primarily Spanish/Portuguese/French/Italian (uses Latin alphabet)
        // These languages don't have special characters outside basic Latin + accents
        const latinOnly = /^[a-zA-ZÀ-ÿ\s\d\.,;:!?¿¡()\-"']+$/;
        return latinOnly.test(text);
    }

    function shouldTranslate(text) {
        // Skip if it's English
        const isEnglish = /^[a-zA-Z\s\d\.,;:!?()\-"']+$/.test(text);
        if (isEnglish) {
            debugLog('⏭️ Skipping English text');
            return false;
        }

        // Skip if it's Spanish or other Latin languages (has accents but no non-Latin characters)
        if (isSpanishOrLatinLanguage(text)) {
            debugLog('⏭️ Skipping Spanish/Latin language text');
            return false;
        }

        // Only translate if it has non-Latin characters (Chinese, Japanese, Korean, etc.)
        if (hasNonLatinCharacters(text)) {
            debugLog('✅ Will translate non-Latin text');
            return true;
        }

        return false;
    }

    async function translateText(text) {
        try {
            const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=t&q=${encodeURIComponent(text.trim())}`;
            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    headers: { 'User-Agent': 'Mozilla/5.0' },
                    onload: (response) => {
                        try {
                            if (response.status === 200) {
                                const data = JSON.parse(response.responseText);
                                if (data && data[0]) {
                                    let result = '';
                                    data[0].forEach(item => { if (item[0]) result += item[0]; });
                                    resolve(result || text);
                                } else resolve(text);
                            } else resolve(text);
                        } catch (e) { resolve(text); }
                    },
                    onerror: () => resolve(text),
                    timeout: 15000
                });
            });
        } catch (error) {
            return text;
        }
    }

    function createNaturalFormat(translatedText) {
        const parts = translatedText.split(/(?=\d+\.\s)/);

        let html = '<div class="gf-formatted-text">';

        parts.forEach(part => {
            part = part.trim();
            if (!part) return;

            if (/^\d+\.\s/.test(part)) {
                const match = part.match(/^(\d+\.\s)(.+)/s);
                if (match) {
                    const num = match[1];
                    const content = match[2].trim();
                    html += `<span class="gf-item"><strong>${num}</strong>${content} </span>`;
                }
            } else {
                html += `<span class="gf-item">${part} </span>`;
            }
        });

        html += '</div>';
        return html;
    }

    function replaceWithNaturalFormat(element, translatedText) {
        if (!element.hasAttribute('data-original-html')) {
            element.setAttribute('data-original-html', element.innerHTML);
        }

        const formattedHTML = createNaturalFormat(translatedText);
        element.innerHTML = formattedHTML;

        translationCount++;
    }

    function replaceElementText(element, translatedText, showBadge = true) {
        if (!element.hasAttribute('data-original-text')) {
            element.setAttribute('data-original-text', element.textContent);
        }
        element.textContent = translatedText;
        if (showBadge) {
            const badge = document.createElement('span');
            badge.className = 'gf-translation-badge';
            badge.textContent = '🌐';
            element.appendChild(badge);
        }
        translationCount++;
    }

    async function processScriptTitles() {
        const scriptLinks = document.querySelectorAll('h2 a.script-link');
        let count = 0;
        for (const link of scriptLinks) {
            if (processedElements.has(link)) continue;
            const titleText = link.textContent.trim();

            if (shouldTranslate(titleText)) {
                processedElements.add(link);
                count++;
                const translated = await translateText(titleText);
                if (translated && translated !== titleText) {
                    replaceElementText(link, translated, true);
                }
                await new Promise(resolve => setTimeout(resolve, 300));
            }
        }
        return count;
    }

    async function processDescriptionSpans() {
        const descriptionSpans = document.querySelectorAll('span.script-description, span.description');
        let count = 0;
        for (const span of descriptionSpans) {
            if (processedElements.has(span)) continue;
            const spanText = span.textContent.trim();

            if (spanText.length > 20 && shouldTranslate(spanText)) {
                processedElements.add(span);
                count++;
                showStatus(`🌐 ${count}...`, 800);
                const translated = await translateText(spanText);
                if (translated && translated !== spanText) {
                    replaceWithNaturalFormat(span, translated);
                }
                await new Promise(resolve => setTimeout(resolve, 400));
            }
        }
        return count;
    }

    async function processDetailPageHeaders() {
        const headers = document.querySelectorAll('header h2, #script-info h2');
        let count = 0;
        for (const header of headers) {
            if (processedElements.has(header)) continue;
            const headerText = header.textContent.trim();

            if (shouldTranslate(headerText)) {
                processedElements.add(header);
                count++;
                const translated = await translateText(headerText);
                if (translated && translated !== headerText) {
                    replaceElementText(header, translated, true);
                }
                await new Promise(resolve => setTimeout(resolve, 400));
            }
        }
        return count;
    }

    async function processDetailPageDescriptions() {
        const descriptions = document.querySelectorAll('#script-description, p.script-description');
        let count = 0;
        for (const desc of descriptions) {
            if (processedElements.has(desc)) continue;
            const descText = desc.textContent.trim();

            if (descText.length > 20 && shouldTranslate(descText)) {
                processedElements.add(desc);
                count++;
                const translated = await translateText(descText);
                if (translated && translated !== descText) {
                    replaceWithNaturalFormat(desc, translated);
                }
                await new Promise(resolve => setTimeout(resolve, 500));
            }
        }
        return count;
    }

    async function processAdditionalInfo() {
        const additionalInfo = document.querySelector('#additional-info');
        if (!additionalInfo) return 0;

        let count = 0;
        const elements = additionalInfo.querySelectorAll('p');

        for (const element of elements) {
            if (processedElements.has(element)) continue;
            const text = element.textContent.trim();
            if (text.length < 20) continue;

            // Check for non-Latin characters (not just ratio)
            if (shouldTranslate(text)) {
                processedElements.add(element);
                count++;
                showStatus(`🌐 ${count}...`, 800);
                const translated = await translateText(text);
                if (translated && translated !== text) {
                    replaceWithNaturalFormat(element, translated);
                }
                await new Promise(resolve => setTimeout(resolve, 500));
            }
        }
        return count;
    }

    function restoreOriginalText() {
        location.reload();
    }

    function makeDraggable(element) {
        let isDragging = false, offsetX = 0, offsetY = 0;
        const savedX = GM_getValue('panelX', null), savedY = GM_getValue('panelY', null);
        if (savedX !== null && savedY !== null) {
            element.style.left = savedX + 'px';
            element.style.top = savedY + 'px';
        } else {
            element.style.right = '10px';
            element.style.top = '10px';
        }

        element.addEventListener('mousedown', (e) => {
            if (e.target.tagName === 'BUTTON' || e.target.closest('button')) return;
            isDragging = true;
            element.classList.add('dragging');
            const rect = element.getBoundingClientRect();
            offsetX = e.clientX - rect.left;
            offsetY = e.clientY - rect.top;
            e.preventDefault();
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            e.preventDefault();
            let newX = e.clientX - offsetX, newY = e.clientY - offsetY;
            const rect = element.getBoundingClientRect();
            newX = Math.max(0, Math.min(newX, window.innerWidth - rect.width));
            newY = Math.max(0, Math.min(newY, window.innerHeight - rect.height));
            element.style.left = newX + 'px';
            element.style.top = newY + 'px';
            element.style.right = 'auto';
        });

        document.addEventListener('mouseup', () => {
            if (!isDragging) return;
            isDragging = false;
            element.classList.remove('dragging');
            const rect = element.getBoundingClientRect();
            GM_setValue('panelX', rect.left);
            GM_setValue('panelY', rect.top);
        });
    }

    function updateCounter() {
        const counter = document.getElementById('translation-count');
        if (counter) counter.textContent = translationCount;
    }

    function addControlPanel() {
        if (document.getElementById('gf-translator-panel')) return;
        const panel = document.createElement('div');
        panel.id = 'gf-translator-panel';
        panel.innerHTML = `
            <div class="gf-panel-header">
                <div class="gf-panel-title">🌐 v16</div>
                <div class="gf-panel-controls">
                    <button class="gf-panel-btn gf-minimize-btn" id="gf-minimize-btn">−</button>
                    <button class="gf-panel-btn gf-close-btn" id="gf-close-btn-x">✕</button>
                </div>
            </div>
            <div class="gf-panel-content" id="gf-panel-content">
                <div class="gf-stat-box">
                    ✅ <strong id="translation-count">0</strong>
                    <div style="font-size: 9px; margin-top: 2px;">No Spanish</div>
                </div>
                <button id="translateNowBtn" class="gf-btn gf-btn-primary">🌐 Translate</button>
                <button id="restoreBtn" class="gf-btn gf-btn-tertiary">🔄 Restore</button>
                <button id="closeBtn" class="gf-btn gf-btn-danger">✕ Close</button>
            </div>
        `;
        document.body.appendChild(panel);
        makeDraggable(panel);

        document.getElementById('gf-minimize-btn').addEventListener('click', (e) => {
            e.stopPropagation();
            const content = document.getElementById('gf-panel-content');
            const btn = e.target;
            if (content.classList.contains('hidden')) {
                content.classList.remove('hidden');
                panel.classList.remove('minimized');
                btn.textContent = '−';
            } else {
                content.classList.add('hidden');
                panel.classList.add('minimized');
                btn.textContent = '□';
            }
        });

        document.getElementById('translateNowBtn').addEventListener('click', async () => {
            const btn = document.getElementById('translateNowBtn');
            btn.disabled = true;
            btn.textContent = '⏳';
            const isDetailPage = document.querySelector('#script-info');
            let total = 0;
            if (isDetailPage) {
                total += await processDetailPageHeaders();
                total += await processDetailPageDescriptions();
                total += await processAdditionalInfo();
            } else {
                total += await processScriptTitles();
                total += await processDescriptionSpans();
            }
            updateCounter();
            btn.disabled = false;
            btn.textContent = '🌐 Translate';
            showStatus(`✅ ${total}!`, 3000);
        });

        document.getElementById('restoreBtn').addEventListener('click', restoreOriginalText);
        document.getElementById('gf-close-btn-x').addEventListener('click', (e) => {
            e.stopPropagation();
            panel.style.display = 'none';
        });
        document.getElementById('closeBtn').addEventListener('click', () => panel.style.display = 'none');
        updateCounter();
    }

    async function init() {
        debugLog('🚀 v16 - Skips Spanish');
        await new Promise(resolve => setTimeout(resolve, 1500));
        addControlPanel();

        if (CONFIG.autoTranslate) {
            showStatus('🌐 Translating...', 2000);
            const isDetailPage = document.querySelector('#script-info');
            let total = 0;
            if (isDetailPage) {
                total += await processDetailPageHeaders();
                total += await processDetailPageDescriptions();
                total += await processAdditionalInfo();
            } else {
                total += await processScriptTitles();
                total += await processDescriptionSpans();
            }
            if (total > 0) showStatus(`✅ Done! ${total}`, 4000);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();