Greasy Fork is available in English.

Greasyfork – Auto-Translator (v19)

Translates ANY foreign language on Greasyfork to your chosen language (20+ languages supported)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Greasyfork – Auto-Translator (v19)
// @namespace    http://tampermonkey.net/
// @version      19
// @description  Translates ANY foreign language on Greasyfork to your chosen language (20+ languages supported)
// @author       Solomon
// @match        https://greasyfork.org/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      translate.googleapis.com
// ==/UserScript==

/*
 * ═══════════════════════════════════════════════════════════════════════════
 * 📋 CHANGELOG
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * Previous Features (Preserved):
 * ✅ Auto-translate foreign content on page load
 * ✅ 20+ supported languages
 * ✅ Smart language detection
 * ✅ Draggable control panel
 * ✅ Restore original text
 * ✅ Translation badges
 * ✅ Language preference saved
 *
 * 🆕 NEW in v18:
 * 🐛 Fixed language selector text color (now black, was invisible white)
 *
 * 🆕 NEW in v19:
 * 🐛 Fixed: Restore now STOPS auto-translate (won't re-translate after restore)
 * 🐛 Fixed: Better Korean text detection (handles mixed brackets/special chars)
 * ✨ Added: Citrus GFork theme compatibility (thanks decembre!)
 * ✨ Added: Toggle to enable/disable auto-translate from panel
 *
 * ═══════════════════════════════════════════════════════════════════════════
 */

(function() {
    'use strict';

    // ═══════════════════════════════════════════════════════════════════════════
    // 🔧 CONFIGURATION
    // ═══════════════════════════════════════════════════════════════════════════

    const CONFIG = {
        debug: false,                 // Console logging
        minTextLength: 5,             // Minimum text length to translate (reduced for short titles)
        translationDelay: 250,        // Delay between translations (ms)
        maxConcurrent: 3,             // Max concurrent translation requests
        retryAttempts: 2,             // Retry failed translations
        retryDelay: 1000              // Delay before retry (ms)
    };

    // 🌍 Supported target languages
    const LANGUAGES = {
        en: { name: 'English', flag: '🇺🇸' },
        es: { name: 'Spanish', flag: '🇪🇸' },
        fr: { name: 'French', flag: '🇫🇷' },
        de: { name: 'German', flag: '🇩🇪' },
        it: { name: 'Italian', flag: '🇮🇹' },
        pt: { name: 'Portuguese', flag: '🇵🇹' },
        ru: { name: 'Russian', flag: '🇷🇺' },
        zh: { name: 'Chinese', flag: '🇨🇳' },
        ja: { name: 'Japanese', flag: '🇯🇵' },
        ko: { name: 'Korean', flag: '🇰🇷' },
        ar: { name: 'Arabic', flag: '🇸🇦' },
        hi: { name: 'Hindi', flag: '🇮🇳' },
        tr: { name: 'Turkish', flag: '🇹🇷' },
        pl: { name: 'Polish', flag: '🇵🇱' },
        nl: { name: 'Dutch', flag: '🇳🇱' },
        vi: { name: 'Vietnamese', flag: '🇻🇳' },
        th: { name: 'Thai', flag: '🇹🇭' },
        id: { name: 'Indonesian', flag: '🇮🇩' },
        he: { name: 'Hebrew', flag: '🇮🇱' },
        uk: { name: 'Ukrainian', flag: '🇺🇦' }
    };

    // ═══════════════════════════════════════════════════════════════════════════
    // 📊 STATE MANAGEMENT
    // ═══════════════════════════════════════════════════════════════════════════

    const state = {
        processedElements: new WeakSet(),
        translationCount: 0,
        targetLanguage: GM_getValue('targetLanguage', 'en'),
        autoTranslate: GM_getValue('autoTranslate', true),  // 🆕 v19: Saveable auto-translate
        isTranslating: false,
        wasRestored: false,  // 🆕 v19: Track if restore was clicked
        translationQueue: [],
        activeRequests: 0
    };

    // ═══════════════════════════════════════════════════════════════════════════
    // 🎨 STYLES (with Citrus GFork compatibility)
    // ═══════════════════════════════════════════════════════════════════════════

    GM_addStyle(`
        /* Translation Badge - Citrus compatible */
        .gf-translation-badge {
            display: inline-block;
            background: linear-gradient(135deg, #4caf50, #45a049);
            color: white !important;
            padding: 2px 6px;
            border-radius: 3px;
            font-size: 9px;
            margin-left: 6px;
            font-weight: bold;
            vertical-align: middle;
            box-shadow: 0 1px 3px rgba(0,0,0,0.2);
        }

        /* Formatted Text - Citrus compatible */
        .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;
        }

        /* Control Panel - Citrus compatible with !important overrides */
        #gf-translator-panel {
            position: fixed;
            background: linear-gradient(180deg, #ffffff 0%, #f8f9fa 100%) !important;
            border: 2px solid #4caf50 !important;
            border-radius: 12px !important;
            padding: 12px !important;
            box-shadow: 0 8px 25px rgba(0,0,0,0.15) !important;
            z-index: 999999 !important;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif !important;
            min-width: 220px;
            max-width: 240px;
            cursor: move;
            user-select: none;
            transition: box-shadow 0.3s ease;
            color: #333333 !important;
        }
        #gf-translator-panel.dragging { cursor: grabbing !important; }
        #gf-translator-panel:hover { box-shadow: 0 12px 35px rgba(0,0,0,0.2) !important; }
        #gf-translator-panel.minimized { min-width: 140px; padding: 8px !important; }

        /* Panel Header - Citrus compatible */
        .gf-panel-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
            padding-bottom: 8px;
            border-bottom: 2px solid #4caf50 !important;
        }
        .gf-panel-title {
            font-weight: bold;
            color: #2e7d32 !important;
            font-size: 14px;
            flex: 1;
            display: flex;
            align-items: center;
            gap: 6px;
        }
        .gf-panel-controls { display: flex; gap: 4px; }
        .gf-panel-btn {
            width: 22px;
            height: 22px;
            border: none !important;
            border-radius: 4px !important;
            cursor: pointer;
            font-size: 12px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s ease;
            background: #f0f0f0 !important;
            color: #333 !important;
        }
        .gf-panel-btn:hover { transform: scale(1.1); }
        .gf-minimize-btn { background: #FFC107 !important; color: white !important; }
        .gf-minimize-btn:hover { background: #FFB300 !important; }
        .gf-close-btn { background: #f44336 !important; color: white !important; }
        .gf-close-btn:hover { background: #e53935 !important; }

        /* Panel Content */
        .gf-panel-content { display: block; }
        .gf-panel-content.hidden { display: none; }

        /* Language Selector - v18 fix + Citrus compatible */
        .gf-lang-selector {
            width: 100%;
            padding: 8px 10px;
            border: 2px solid #e0e0e0 !important;
            border-radius: 6px !important;
            font-size: 12px;
            margin-bottom: 10px;
            background: #ffffff !important;
            color: #333333 !important;
            cursor: pointer;
            transition: border-color 0.2s;
            font-family: inherit;
        }
        .gf-lang-selector option {
            color: #333333 !important;
            background: #ffffff !important;
        }
        .gf-lang-selector:hover { border-color: #4caf50 !important; }
        .gf-lang-selector:focus {
            outline: none;
            border-color: #4caf50 !important;
            box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
        }

        /* Stats Box - Citrus compatible */
        .gf-stat-box {
            margin-bottom: 10px;
            font-size: 12px;
            color: #555 !important;
            background: linear-gradient(135deg, #e8f5e9 0%, #f1f8f4 100%) !important;
            padding: 10px;
            border-radius: 8px !important;
            text-align: center;
            border: 1px solid #c8e6c9 !important;
        }
        .gf-stat-count {
            font-size: 24px;
            font-weight: bold;
            color: #2e7d32 !important;
            display: block;
            line-height: 1;
            margin-bottom: 4px;
        }
        .gf-stat-label {
            font-size: 10px;
            color: #666 !important;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }

        /* 🆕 v19: Auto-translate toggle row */
        .gf-toggle-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 8px 10px;
            margin-bottom: 10px;
            background: #f5f5f5 !important;
            border-radius: 6px !important;
            font-size: 12px;
            color: #333 !important;
        }
        .gf-toggle-label {
            font-weight: 500;
        }
        .gf-toggle-switch {
            position: relative;
            width: 40px;
            height: 22px;
            background: #ccc !important;
            border-radius: 11px !important;
            cursor: pointer;
            transition: background 0.2s ease;
        }
        .gf-toggle-switch.on {
            background: #4caf50 !important;
        }
        .gf-toggle-switch::after {
            content: "";
            position: absolute;
            top: 2px;
            left: 2px;
            width: 18px;
            height: 18px;
            background: white !important;
            border-radius: 50% !important;
            transition: left 0.2s ease;
            box-shadow: 0 1px 3px rgba(0,0,0,0.2);
        }
        .gf-toggle-switch.on::after {
            left: 20px;
        }

        /* Buttons - Citrus compatible */
        .gf-btn {
            width: 100%;
            padding: 10px;
            border: none !important;
            border-radius: 6px !important;
            cursor: pointer;
            font-weight: 600;
            font-size: 12px;
            margin-bottom: 6px;
            transition: all 0.2s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 6px;
        }
        .gf-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        }
        .gf-btn:active { transform: translateY(0); }
        .gf-btn:disabled {
            opacity: 0.6;
            cursor: not-allowed;
            transform: none !important;
        }
        .gf-btn-primary {
            background: linear-gradient(135deg, #4caf50 0%, #43a047 100%) !important;
            color: white !important;
        }
        .gf-btn-primary:hover { background: linear-gradient(135deg, #43a047 0%, #388e3c 100%) !important; }
        .gf-btn-secondary {
            background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%) !important;
            color: white !important;
        }
        .gf-btn-secondary:hover { background: linear-gradient(135deg, #1976D2 0%, #1565C0 100%) !important; }
        .gf-btn-tertiary {
            background: #f5f5f5 !important;
            color: #333 !important;
            border: 1px solid #ddd !important;
        }
        .gf-btn-tertiary:hover { background: #eeeeee !important; }

        /* Status Bar - Citrus compatible */
        #gf-status-bar {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: linear-gradient(135deg, #4caf50 0%, #43a047 100%) !important;
            color: white !important;
            padding: 12px 20px;
            border-radius: 8px !important;
            box-shadow: 0 4px 15px rgba(0,0,0,0.2) !important;
            z-index: 999998 !important;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif !important;
            font-size: 13px;
            font-weight: 600;
            display: flex;
            align-items: center;
            gap: 8px;
            animation: slideIn 0.3s ease;
        }
        @keyframes slideIn {
            from { transform: translateX(100px); opacity: 0; }
            to { transform: translateX(0); opacity: 1; }
        }

        /* Detected language badge */
        .gf-detected-lang {
            font-size: 9px;
            background: rgba(255,255,255,0.2) !important;
            padding: 1px 4px;
            border-radius: 2px !important;
            margin-left: 4px;
            color: white !important;
        }
    `);

    // ═══════════════════════════════════════════════════════════════════════════
    // 🔧 UTILITY FUNCTIONS
    // ═══════════════════════════════════════════════════════════════════════════

    function debugLog(...args) {
        if (CONFIG.debug) console.log('🌐 [v19]', ...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.innerHTML = message;
        statusBar.style.display = 'flex';
        if (duration > 0) {
            setTimeout(() => {
                statusBar.style.display = 'none';
            }, duration);
        }
    }

    function hideStatus() {
        const statusBar = document.getElementById('gf-status-bar');
        if (statusBar) statusBar.style.display = 'none';
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 🔍 LANGUAGE DETECTION (v19: Improved Korean detection)
    // ═══════════════════════════════════════════════════════════════════════════

    function isTargetLanguageText(text, targetLang) {
        // Language-specific character patterns
        const langPatterns = {
            en: /^[a-zA-Z\s\d\.,;:!?()\-"'@#$%&*+=\[\]{}|\\/<>~`]+$/,
            es: /^[a-zA-ZáéíóúüñÁÉÍÓÚÜÑ¿¡\s\d\.,;:!?()\-"']+$/,
            fr: /^[a-zA-ZàâäéèêëïîôùûüÿçœæÀÂÄÉÈÊËÏÎÔÙÛÜŸÇŒÆ\s\d\.,;:!?()\-"']+$/,
            de: /^[a-zA-ZäöüßÄÖÜ\s\d\.,;:!?()\-"']+$/,
            it: /^[a-zA-ZàèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ\s\d\.,;:!?()\-"']+$/,
            pt: /^[a-zA-ZáàâãéêíóôõúüçÁÀÂÃÉÊÍÓÔÕÚÜÇ\s\d\.,;:!?()\-"']+$/,
            ru: /[\u0400-\u04FF]/,
            zh: /[\u4e00-\u9fff\u3400-\u4dbf]/,
            ja: /[\u3040-\u30ff\u4e00-\u9fff]/,
            ko: /[\uac00-\ud7af\u1100-\u11ff\u3130-\u318f]/,  // 🆕 v19: Added more Korean ranges
            ar: /[\u0600-\u06ff\u0750-\u077f]/,
            he: /[\u0590-\u05ff]/,
            th: /[\u0e00-\u0e7f]/,
            vi: /[àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ]/i,
            tr: /[çğıöşüÇĞİÖŞÜ]/,
            pl: /[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/,
            nl: /^[a-zA-Z\s\d\.,;:!?()\-"']+$/,
            uk: /[\u0400-\u04FF]/,
            hi: /[\u0900-\u097F]/,
            id: /^[a-zA-Z\s\d\.,;:!?()\-"']+$/
        };

        const pattern = langPatterns[targetLang];
        if (!pattern) return false;

        // For script-based languages, check if text CONTAINS those characters
        if (['ru', 'zh', 'ja', 'ko', 'ar', 'he', 'th', 'uk', 'hi'].includes(targetLang)) {
            return pattern.test(text);
        }

        return pattern.test(text);
    }

    /**
     * 🆕 v19: Improved detection for foreign text
     * Now handles Korean and other scripts mixed with brackets/special chars
     */
    function containsForeignScript(text) {
        // Check for various non-Latin scripts
        const foreignPatterns = [
            /[\u0400-\u04FF]/,           // Cyrillic (Russian, Ukrainian)
            /[\u4e00-\u9fff]/,           // CJK (Chinese, Japanese Kanji)
            /[\u3040-\u30ff]/,           // Japanese Hiragana/Katakana
            /[\uac00-\ud7af]/,           // Korean Hangul syllables
            /[\u1100-\u11ff]/,           // Korean Jamo
            /[\u3130-\u318f]/,           // Korean compatibility Jamo
            /[\u0600-\u06ff]/,           // Arabic
            /[\u0590-\u05ff]/,           // Hebrew
            /[\u0e00-\u0e7f]/,           // Thai
            /[\u0900-\u097F]/,           // Hindi/Devanagari
        ];

        return foreignPatterns.some(pattern => pattern.test(text));
    }

    function shouldTranslate(text) {
        if (!text || text.length < CONFIG.minTextLength) return false;

        const cleanText = text.trim();
        const targetLang = state.targetLanguage;

        // 🆕 v19: Check if contains foreign script (more reliable than checking if NOT target lang)
        if (containsForeignScript(cleanText)) {
            // If target is one of these scripts, check if it matches
            if (['ru', 'zh', 'ja', 'ko', 'ar', 'he', 'th', 'uk', 'hi'].includes(targetLang)) {
                if (isTargetLanguageText(cleanText, targetLang)) {
                    debugLog('⏭️ Skipping - already in target script:', targetLang);
                    return false;
                }
            }
            debugLog('✅ Will translate (foreign script detected):', cleanText.substring(0, 50) + '...');
            return true;
        }

        // For Latin-based target languages, check if already in that language
        if (isTargetLanguageText(cleanText, targetLang)) {
            debugLog('⏭️ Skipping - already in target language:', targetLang);
            return false;
        }

        // Check if text contains meaningful content
        const hasLetters = /[a-zA-Z\u0080-\uffff]/.test(cleanText);
        if (!hasLetters) {
            debugLog('⏭️ Skipping - no translatable content');
            return false;
        }

        debugLog('✅ Will translate:', cleanText.substring(0, 50) + '...');
        return true;
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 🌐 TRANSLATION API
    // ═══════════════════════════════════════════════════════════════════════════

    async function translateText(text, attempt = 1) {
        const targetLang = state.targetLanguage;

        try {
            const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang}&dt=t&dt=ld&q=${encodeURIComponent(text.trim())}`;

            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    headers: {
                        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
                    },
                    timeout: 15000,
                    onload: (response) => {
                        try {
                            if (response.status === 200) {
                                const data = JSON.parse(response.responseText);
                                if (data && data[0]) {
                                    let result = '';
                                    let detectedLang = data[2] || 'unknown';

                                    data[0].forEach(item => {
                                        if (item && item[0]) result += item[0];
                                    });

                                    if (result && result !== text) {
                                        debugLog(`✅ Translated from ${detectedLang}:`, result.substring(0, 50));
                                        resolve({ text: result, detectedLang });
                                    } else {
                                        resolve({ text, detectedLang: null });
                                    }
                                } else {
                                    resolve({ text, detectedLang: null });
                                }
                            } else if (attempt < CONFIG.retryAttempts) {
                                debugLog(`⚠️ Retry ${attempt}/${CONFIG.retryAttempts}...`);
                                setTimeout(() => {
                                    translateText(text, attempt + 1).then(resolve);
                                }, CONFIG.retryDelay);
                            } else {
                                resolve({ text, detectedLang: null });
                            }
                        } catch (e) {
                            debugLog('❌ Parse error:', e);
                            resolve({ text, detectedLang: null });
                        }
                    },
                    onerror: (error) => {
                        debugLog('❌ Request error:', error);
                        if (attempt < CONFIG.retryAttempts) {
                            setTimeout(() => {
                                translateText(text, attempt + 1).then(resolve);
                            }, CONFIG.retryDelay);
                        } else {
                            resolve({ text, detectedLang: null });
                        }
                    },
                    ontimeout: () => {
                        debugLog('❌ Timeout');
                        resolve({ text, detectedLang: null });
                    }
                });
            });
        } catch (error) {
            debugLog('❌ Exception:', error);
            return { text, detectedLang: null };
        }
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 📝 TEXT FORMATTING & REPLACEMENT
    // ═══════════════════════════════════════════════════════════════════════════

    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) {
                    html += `<span class="gf-item"><strong>${match[1]}</strong>${match[2].trim()}</span>`;
                }
            } else {
                html += `<span class="gf-item">${part}</span>`;
            }
        });

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

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

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

        state.translationCount++;
        updateCounter();
    }

    function replaceElementText(element, translatedText, detectedLang = null, 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.innerHTML = '🌐';
            if (detectedLang) {
                badge.innerHTML += `<span class="gf-detected-lang">${detectedLang}</span>`;
            }
            element.appendChild(badge);
        }

        state.translationCount++;
        updateCounter();
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 🔄 TRANSLATION PROCESSORS
    // ═══════════════════════════════════════════════════════════════════════════

    async function processScriptTitles() {
        const scriptLinks = document.querySelectorAll('h2 a.script-link, .script-list article h2 a');
        let count = 0;

        for (const link of scriptLinks) {
            if (state.processedElements.has(link)) continue;

            const titleText = link.textContent.trim();
            if (shouldTranslate(titleText)) {
                state.processedElements.add(link);

                const result = await translateText(titleText);
                if (result.text !== titleText) {
                    replaceElementText(link, result.text, result.detectedLang, true);
                    count++;
                }

                await sleep(CONFIG.translationDelay);
            }
        }

        return count;
    }

    async function processDescriptionSpans() {
        const descriptionSpans = document.querySelectorAll('span.script-description, span.description, .script-list article .script-description');
        let count = 0;

        for (const span of descriptionSpans) {
            if (state.processedElements.has(span)) continue;

            const spanText = span.textContent.trim();
            if (spanText.length > 20 && shouldTranslate(spanText)) {
                state.processedElements.add(span);

                showStatus(`🌐 Translating ${count + 1}...`, 0);

                const result = await translateText(spanText);
                if (result.text !== spanText) {
                    replaceWithNaturalFormat(span, result.text, result.detectedLang);
                    count++;
                }

                await sleep(CONFIG.translationDelay);
            }
        }

        return count;
    }

    async function processDetailPageHeaders() {
        const headers = document.querySelectorAll('header h2, #script-info h2, .script-show-header h2');
        let count = 0;

        for (const header of headers) {
            if (state.processedElements.has(header)) continue;

            const headerText = header.textContent.trim();
            if (shouldTranslate(headerText)) {
                state.processedElements.add(header);

                const result = await translateText(headerText);
                if (result.text !== headerText) {
                    replaceElementText(header, result.text, result.detectedLang, true);
                    count++;
                }

                await sleep(CONFIG.translationDelay);
            }
        }

        return count;
    }

    async function processDetailPageDescriptions() {
        const descriptions = document.querySelectorAll('#script-description, p.script-description, .script-description');
        let count = 0;

        for (const desc of descriptions) {
            if (state.processedElements.has(desc)) continue;

            const descText = desc.textContent.trim();
            if (descText.length > 20 && shouldTranslate(descText)) {
                state.processedElements.add(desc);

                const result = await translateText(descText);
                if (result.text !== descText) {
                    replaceWithNaturalFormat(desc, result.text, result.detectedLang);
                    count++;
                }

                await sleep(CONFIG.translationDelay);
            }
        }

        return count;
    }

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

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

        for (const element of elements) {
            if (state.processedElements.has(element)) continue;

            const text = element.textContent.trim();
            if (text.length < 20) continue;

            if (shouldTranslate(text)) {
                state.processedElements.add(element);

                showStatus(`🌐 Translating additional info...`, 0);

                const result = await translateText(text);
                if (result.text !== text) {
                    replaceWithNaturalFormat(element, result.text, result.detectedLang);
                    count++;
                }

                await sleep(CONFIG.translationDelay);
            }
        }

        return count;
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 🎛️ CONTROL PANEL
    // ═══════════════════════════════════════════════════════════════════════════

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

    function createLanguageSelector() {
        let options = '';
        for (const [code, info] of Object.entries(LANGUAGES)) {
            const selected = code === state.targetLanguage ? 'selected' : '';
            options += `<option value="${code}" ${selected}>${info.flag} ${info.name}</option>`;
        }
        return options;
    }

    /**
     * 🆕 v19: Restore function now sets wasRestored flag to prevent auto-translate
     */
    function restoreOriginalText() {
        // Set flag to prevent auto-translate
        state.wasRestored = true;

        // Restore elements with data-original-text
        document.querySelectorAll('[data-original-text]').forEach(el => {
            el.textContent = el.getAttribute('data-original-text');
            el.removeAttribute('data-original-text');
        });

        // Restore elements with data-original-html
        document.querySelectorAll('[data-original-html]').forEach(el => {
            el.innerHTML = el.getAttribute('data-original-html');
            el.removeAttribute('data-original-html');
        });

        // Remove badges
        document.querySelectorAll('.gf-translation-badge').forEach(badge => badge.remove());

        // Reset state
        state.processedElements = new WeakSet();
        state.translationCount = 0;
        updateCounter();

        showStatus('🔄 Restored! Click "Translate" to translate again.', 3000);
    }

    function makeDraggable(element) {
        let isDragging = false, offsetX = 0, offsetY = 0;

        const savedX = GM_getValue('panelX', null);
        const savedY = GM_getValue('panelY', null);

        if (savedX !== null && savedY !== null) {
            element.style.left = savedX + 'px';
            element.style.top = savedY + 'px';
            element.style.right = 'auto';
        } else {
            element.style.right = '15px';
            element.style.top = '15px';
        }

        element.addEventListener('mousedown', (e) => {
            if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT' || e.target.closest('button') || e.target.closest('select') || e.target.closest('.gf-toggle-switch')) 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;
            let 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 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">
                    🌐 <span>v19</span>
                </div>
                <div class="gf-panel-controls">
                    <button class="gf-panel-btn gf-minimize-btn" id="gf-minimize-btn" title="Minimize">−</button>
                    <button class="gf-panel-btn gf-close-btn" id="gf-close-btn" title="Close">✕</button>
                </div>
            </div>
            <div class="gf-panel-content" id="gf-panel-content">
                <select class="gf-lang-selector" id="gf-lang-selector" title="Select target language">
                    ${createLanguageSelector()}
                </select>

                <!-- 🆕 v19: Auto-translate toggle -->
                <div class="gf-toggle-row">
                    <span class="gf-toggle-label">Auto-Translate</span>
                    <div class="gf-toggle-switch ${state.autoTranslate ? 'on' : ''}" id="gf-auto-toggle" title="Toggle auto-translate on page load"></div>
                </div>

                <div class="gf-stat-box">
                    <span class="gf-stat-count" id="gf-translation-count">0</span>
                    <span class="gf-stat-label">Translated</span>
                </div>
                <button id="gf-translate-btn" class="gf-btn gf-btn-primary">
                    🌐 Translate
                </button>
                <button id="gf-restore-btn" class="gf-btn gf-btn-secondary">
                    🔄 Restore
                </button>
                <button id="gf-close-btn-bottom" class="gf-btn gf-btn-tertiary">
                    ✕ Close Panel
                </button>
            </div>
        `;

        document.body.appendChild(panel);
        makeDraggable(panel);

        // Event Listeners
        document.getElementById('gf-lang-selector').addEventListener('change', (e) => {
            state.targetLanguage = e.target.value;
            GM_setValue('targetLanguage', state.targetLanguage);
            showStatus(`🌐 Language set to ${LANGUAGES[state.targetLanguage].flag} ${LANGUAGES[state.targetLanguage].name}`, 2000);
        });

        // 🆕 v19: Auto-translate toggle
        document.getElementById('gf-auto-toggle').addEventListener('click', (e) => {
            e.stopPropagation();
            state.autoTranslate = !state.autoTranslate;
            GM_setValue('autoTranslate', state.autoTranslate);
            e.target.classList.toggle('on', state.autoTranslate);
            showStatus(`Auto-translate ${state.autoTranslate ? 'enabled ✅' : 'disabled ❌'}`, 2000);
        });

        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('gf-translate-btn').addEventListener('click', async () => {
            if (state.isTranslating) return;
            state.wasRestored = false;  // 🆕 v19: Reset flag when manually translating
            await runTranslation();
        });

        document.getElementById('gf-restore-btn').addEventListener('click', restoreOriginalText);

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

        document.getElementById('gf-close-btn-bottom').addEventListener('click', () => {
            panel.style.display = 'none';
        });

        updateCounter();
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 🚀 MAIN TRANSLATION RUNNER
    // ═══════════════════════════════════════════════════════════════════════════

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function runTranslation() {
        if (state.isTranslating) return;
        state.isTranslating = true;

        const btn = document.getElementById('gf-translate-btn');
        if (btn) {
            btn.disabled = true;
            btn.innerHTML = '⏳ Translating...';
        }

        showStatus('🌐 Starting translation...', 0);

        const isDetailPage = document.querySelector('#script-info, .script-show-container');
        let total = 0;

        try {
            if (isDetailPage) {
                total += await processDetailPageHeaders();
                total += await processDetailPageDescriptions();
                total += await processAdditionalInfo();
            } else {
                total += await processScriptTitles();
                total += await processDescriptionSpans();
            }

            const langInfo = LANGUAGES[state.targetLanguage];
            showStatus(`✅ Done! ${total} items translated to ${langInfo.flag} ${langInfo.name}`, 4000);
        } catch (error) {
            debugLog('❌ Translation error:', error);
            showStatus('❌ Translation error occurred', 3000);
        }

        if (btn) {
            btn.disabled = false;
            btn.innerHTML = '🌐 Translate';
        }

        state.isTranslating = false;
        return total;
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 🎬 INITIALIZATION
    // ═══════════════════════════════════════════════════════════════════════════

    async function init() {
        const langInfo = LANGUAGES[state.targetLanguage];
        console.log(`🌐 Greasyfork Auto-Translator v19 loaded! Target: ${langInfo.flag} ${langInfo.name}`);

        // Wait for page to settle
        await sleep(1000);

        // Add control panel
        addControlPanel();

        // 🆕 v19: Only auto-translate if enabled AND not restored
        if (state.autoTranslate && !state.wasRestored) {
            await sleep(500);
            await runTranslation();
        }
    }

    // Start when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();