K-Translate

If the page is already in the target language, the interface and floating button remain completely hidden. Based on Armored Engine V21.

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!)

Advertisement:

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!)

Advertisement:

// ==UserScript==
// @name           K-Translate
// @name:en        K-Translate
// @name:tr        K-Translate
// @namespace      https://greasyfork.org/en/users/1500762-kerimdemirkaynak
// @version        22.0.0
// @description    If the page is already in the target language, the interface and floating button remain completely hidden. Based on Armored Engine V21.
// @description:en If the page is already in the target language, the interface and floating button remain completely hidden. Based on Armored Engine V21.
// @description:tr Sayfa zaten hedef dildeyse arayüz ve yüzen buton tamamen gizli kalır. Motor Zırhlı V21 temellidir.
// @author         Kerim Demirkaynak
// @license        MIT License
// @icon           https://addons.mozilla.org/user-media/addon_icons/3026/3026609-64.png?modified=e1e054ef
// @match          *://*/*
// @run-at         document-end
// @grant          GM_xmlhttpRequest
// @grant          GM_registerMenuCommand
// @grant          GM_setValue
// @grant          GM_getValue
// @connect        translate.googleapis.com
// ==/UserScript==

(function () {
    'use strict';

    // ==========================================
    // 0. EVRENSEL (UNIVERSAL) API DESTEĞİ
    // ==========================================
    const isGM = typeof GM_getValue !== 'undefined' && typeof GM_xmlhttpRequest !== 'undefined';

    const Store = {
        get: (key, def) => {
            try {
                if (isGM) return GM_getValue(key, def);
                const val = localStorage.getItem('k_' + key);
                return val !== null ? JSON.parse(val) : def;
            } catch { return def; }
        },
        set: (key, val) => {
            try {
                if (isGM) GM_setValue(key, val);
                else localStorage.setItem('k_' + key, JSON.stringify(val));
            } catch {}
        }
    };

    const FetchAPI = async (url, data) => {
        if (isGM) {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "POST", url: url, timeout: 12000,
                    headers: { "Content-Type": "application/x-www-form-urlencoded" },
                    data: data,
                    onload: (res) => resolve(res.responseText),
                    onerror: reject, ontimeout: reject
                });
            });
        } else {
            const res = await fetch(url, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: data
            });
            if (!res.ok) throw new Error('API Error');
            return await res.text();
        }
    };

    // ==========================================
    // 1. YAPILANDIRMA VE SABİTLER
    // ==========================================
    const CONFIG = {
        maxConcurrent: 3, batchMaxChars: 3500, batchMaxBlocks: 40, 
        batchSeparator: '\n\n|||\n\n', 
        inlineTags: new Set(['A','B','I','STRONG','EM','SPAN','MARK','SMALL','DEL','INS','SUB','SUP','CODE','Q','ABBR','BR','FONT'])
    };

    const LANG_MAP = {
        'auto': 'Detect Language', 'tr': 'Turkish', 'en': 'English', 'de': 'German', 'ja': 'Japanese', 
        'es': 'Spanish', 'fr': 'French', 'ru': 'Russian', 'ko': 'Korean', 'zh-CN': 'Chinese (Simplified)', 
        'ar': 'Arabic', 'pt': 'Portuguese', 'it': 'Italian', 'nl': 'Dutch'
    };

    const TRANSLATION_CACHE = new Map();
    const ORIGINAL_HTML_MAP = new Map();

    const STATE = {
        sourceLang: 'auto', detectedLang: null,
        targetLang: Store.get('targetLang', 'en'), // Default changed to English or keep 'tr' depending on your pref, leaving as user set or fallback.
        disabledLangs: Store.get('disabledLangs', []),
        isActive: false, totalBlocks: 0, processedBlocks: 0, blockCounter: 0, observer: null
    };

    function getLangName(code) { return LANG_MAP[code] || code.toUpperCase(); }

    // ==========================================
    // 2. TAG MASKELEME VE ÇEVİRİ MOTORU
    // ==========================================
    class HTMLMasker {
        static mask(htmlString) {
            const tagMap = {}; let counter = 0;
            const maskedHtml = htmlString.replace(/<[^>]+>/g, (match) => { 
                const id = `[#${counter}#]`; tagMap[id] = match; counter++; return id; 
            });
            return { maskedHtml, tagMap };
        }
        static unmask(translatedHtml, tagMap) {
            let unmasked = translatedHtml;
            for (const [id, originalTag] of Object.entries(tagMap)) {
                const numMatch = id.match(/\d+/);
                if(numMatch) { unmasked = unmasked.replace(new RegExp(`\\[\\s*#\\s*${numMatch[0]}\\s*#\\s*\\]`, 'g'), originalTag); }
            }
            return unmasked;
        }
    }

    class TranslationEngine {
        static async fetchGoogle(text, sourceLang, targetLang) {
            const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t`;
            const responseText = await FetchAPI(url, "q=" + encodeURIComponent(text));
            const data = JSON.parse(responseText);
            let translated = "";
            if (data && data[0]) data[0].forEach(item => { if (item[0]) translated += item[0]; });
            return translated;
        }
    }

    // ==========================================
    // 3. DOM TARAYICI & KUYRUK YÖNETİMİ
    // ==========================================
    class DOMScanner {
        static getContextualBlocks(root) {
            const blocks = [];
            const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
                acceptNode: (node) => {
                    if (node.id === 'kerim-ui-wrapper' || node.closest('#kerim-ui-wrapper')) return NodeFilter.FILTER_REJECT;
                    if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'SVG', 'CANVAS', 'PRE', 'CODE'].includes(node.tagName)) return NodeFilter.FILTER_REJECT;
                    if (node.hasAttribute('data-v-translated') || node.closest('[data-v-translated]')) return NodeFilter.FILTER_REJECT;
                    
                    if (!node.textContent.trim()) return NodeFilter.FILTER_SKIP;

                    let hasTextNode = false; let hasBlockChild = false;
                    for (let child of node.childNodes) {
                        if (child.nodeType === Node.TEXT_NODE && child.nodeValue.trim().length > 0) hasTextNode = true;
                        else if (child.nodeType === Node.ELEMENT_NODE) {
                            if (!CONFIG.inlineTags.has(child.tagName.toUpperCase())) { hasBlockChild = true; break; }
                        }
                    }
                    if (hasTextNode && !hasBlockChild) return NodeFilter.FILTER_ACCEPT;
                    return NodeFilter.FILTER_SKIP;
                }
            });
            let currentNode;
            while ((currentNode = walker.nextNode())) { 
                currentNode.setAttribute('data-v-translated', 'queued'); 
                blocks.push(currentNode); 
            }
            return blocks;
        }

        static startObserver() {
            if (STATE.observer) return;
            STATE.observer = new MutationObserver((mutations) => {
                if (!STATE.isActive) return;
                let newBlocks = [];
                mutations.forEach(m => {
                    m.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE && node.id !== 'kerim-ui-wrapper') {
                            if (node.closest('[data-v-translated]')) return;
                            let isBlock = false;
                            if (node.textContent.trim()) {
                                let hasBlock = false;
                                for(let c of node.childNodes) { 
                                    if(c.nodeType === Node.ELEMENT_NODE && !CONFIG.inlineTags.has(c.tagName)) hasBlock = true; 
                                }
                                if(!hasBlock) isBlock = true;
                            }
                            if (isBlock && !node.hasAttribute('data-v-translated')) {
                                node.setAttribute('data-v-translated', 'queued'); 
                                newBlocks.push(node);
                            } else { 
                                newBlocks.push(...this.getContextualBlocks(node)); 
                            }
                        }
                    });
                });
                if (newBlocks.length > 0) { STATE.totalBlocks += newBlocks.length; queueManager.add(newBlocks); }
            });
            STATE.observer.observe(document.body, { childList: true, subtree: true });
        }

        static stopObserver() {
            if (STATE.observer) { 
                STATE.observer.disconnect(); 
                STATE.observer = null; 
            }
        }
    }

    class QueueManager {
        constructor() { this.queue = []; this.activeCount = 0; }
        add(blocks) { this.queue.push(...blocks); this.process(); }
        async process() {
            if (this.queue.length === 0 || this.activeCount >= CONFIG.maxConcurrent || !STATE.isActive) return;
            let batchData = []; let currentCharCount = 0;

            while (this.queue.length > 0 && batchData.length < CONFIG.batchMaxBlocks && currentCharCount < CONFIG.batchMaxChars) {
                const block = this.queue.shift(); 
                if (!document.contains(block)) { STATE.processedBlocks++; continue; }

                const originalHtml = block.innerHTML.trim();
                if (!block.hasAttribute('data-k-id')) {
                    const bId = (++STATE.blockCounter).toString();
                    block.setAttribute('data-k-id', bId);
                    ORIGINAL_HTML_MAP.set(bId, originalHtml);
                }

                if (!/[a-zA-Z\u00C0-\u024F\u0400-\u04FF]/.test(originalHtml)) {
                    block.setAttribute('data-v-translated', 'done'); STATE.processedBlocks++; continue;
                }
                if (TRANSLATION_CACHE.has(originalHtml)) {
                    DOMScanner.stopObserver(); 
                    block.innerHTML = TRANSLATION_CACHE.get(originalHtml); 
                    block.setAttribute('data-v-translated', 'done'); 
                    DOMScanner.startObserver(); 
                    STATE.processedBlocks++; continue;
                }
                const { maskedHtml, tagMap } = HTMLMasker.mask(originalHtml);
                batchData.push({ block, maskedHtml, tagMap, originalHtml });
                currentCharCount += maskedHtml.length;
            }

            if (batchData.length === 0) { UIManager.updateProgress(); this.process(); return; }

            this.activeCount++;
            const textToTranslate = batchData.map(d => d.maskedHtml).join(CONFIG.batchSeparator);

            try {
                const translatedCombined = await TranslationEngine.fetchGoogle(textToTranslate, STATE.sourceLang, STATE.targetLang);
                const translatedParts = translatedCombined.split(/\|\|\|/g).map(s => s.trim());
                
                DOMScanner.stopObserver(); 

                if (translatedParts.length === batchData.length) {
                    batchData.forEach((d, i) => {
                        const unmasked = HTMLMasker.unmask(translatedParts[i], d.tagMap);
                        d.block.innerHTML = unmasked; 
                        d.block.setAttribute('data-v-translated', 'done'); 
                        TRANSLATION_CACHE.set(d.originalHtml, unmasked);
                    });
                } else {
                    for (let d of batchData) {
                        try {
                            const singleTrans = await TranslationEngine.fetchGoogle(d.maskedHtml, STATE.sourceLang, STATE.targetLang);
                            const unmasked = HTMLMasker.unmask(singleTrans, d.tagMap);
                            d.block.innerHTML = unmasked; 
                            d.block.setAttribute('data-v-translated', 'done'); 
                            TRANSLATION_CACHE.set(d.originalHtml, unmasked);
                        } catch (e) { 
                            d.block.setAttribute('data-v-translated', 'error'); 
                        }
                    }
                }
                
                DOMScanner.startObserver(); 

            } catch (err) { 
                batchData.forEach(d => { 
                    d.block.setAttribute('data-v-translated', 'error'); 
                }); 
            } 
            finally { 
                STATE.processedBlocks += batchData.length; 
                UIManager.updateProgress(); 
                this.activeCount--; 
                this.process(); 
            }
        }
    }
    const queueManager = new QueueManager();

    // ==========================================
    // 4. ARAYÜZ (UI) - Hayalet Modu
    // ==========================================
    class UIManager {
        static isVisible = false;
        static _langPickerMode = 'target';

        static init() {
            if (document.getElementById('kerim-ui-wrapper')) return;
            const style = document.createElement('style');
            style.textContent = `
                #kerim-ui-wrapper { all: initial; position: fixed; top: 0; left: 0; right: 0; display: flex; justify-content: center; z-index: 2147483647; pointer-events: none; font-family: sans-serif; }
                #kerim-panel { pointer-events: auto; background: #202124; border: 1px solid #3c4043; border-top: none; border-radius: 0 0 12px 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.6); width: 92%; max-width: 440px; transform: translateY(-100%); transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; opacity: 0; overflow: hidden; }
                #kerim-panel.visible { transform: translateY(0); opacity: 1; }
                #kerim-main-view { display: flex; flex-direction: column; }
                .kerim-lang-row { display: flex; align-items: center; padding: 10px 12px 4px; gap: 4px; }
                .kerim-lang-btn { flex: 1; background: none; border: none; color: #e8eaed; font-size: 13px; font-weight: 500; padding: 6px 8px; border-radius: 6px; cursor: pointer; text-align: center; transition: background 0.15s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
                .kerim-lang-btn:hover { background: #3c4043; }
                .kerim-lang-btn.active { color: #8ab4f8; }
                .kerim-swap-btn { background: none; border: none; color: #9aa0a6; cursor: pointer; padding: 6px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
                .kerim-divider { height: 1px; background: #3c4043; margin: 4px 12px; }
                .kerim-action-row { display: flex; align-items: center; padding: 4px 8px 8px; gap: 2px; }
                .kerim-action-btn { background: none; border: none; color: #9aa0a6; cursor: pointer; padding: 8px 10px; border-radius: 6px; font-size: 13px; display: flex; align-items: center; gap: 6px; }
                .kerim-action-btn.primary { color: #8ab4f8; margin-left: auto; font-weight: 500; }
                #kerim-progress { height: 2px; background: transparent; position: relative; overflow: hidden; }
                #kerim-progress-fill { height: 100%; width: 0%; background: #8ab4f8; transition: width 0.3s ease; position: absolute; top: 0; left: 0; }
                #kerim-progress.indeterminate #kerim-progress-fill { width: 30% !important; animation: kerim-slide 1.4s ease-in-out infinite; }
                @keyframes kerim-slide { 0% { left: -35%; } 100% { left: 110%; } }
                #kerim-lang-view { display: none; flex-direction: column; max-height: 320px; }
                #kerim-lang-view.open { display: flex; }
                .kerim-lang-header { display: flex; align-items: center; padding: 8px 12px; border-bottom: 1px solid #3c4043; }
                .kerim-lang-title { flex: 1; color: #e8eaed; font-size: 14px; font-weight: 500; }
                .kerim-lang-back { background: none; border: none; color: #9aa0a6; cursor: pointer; padding: 4px; border-radius: 50%; }
                .kerim-lang-search { margin: 8px 12px; background: #3c4043; border: none; border-radius: 8px; color: #e8eaed; font-size: 13px; padding: 8px 12px; outline: none; width: calc(100% - 24px); box-sizing: border-box; }
                .kerim-lang-list { overflow-y: auto; flex: 1; padding: 4px 0; }
                .kerim-lang-item { display: flex; align-items: center; padding: 10px 16px; color: #e8eaed; font-size: 13px; cursor: pointer; justify-content: space-between; }
                .kerim-lang-item.selected { color: #8ab4f8; }
                .kerim-lang-item .kerim-disable-btn { background: none; border: none; color: #9aa0a6; font-size: 11px; cursor: pointer; padding: 2px 6px; border-radius: 4px; }
                .kerim-disabled-badge { font-size: 10px; background: rgba(242,139,130,0.15); color: #f28b82; padding: 1px 5px; border-radius: 4px; margin-right: 4px; }
                
                /* Yüzen Buton (FAB) varsayılan olarak GİZLİ başlar */
                #kerim-trigger { pointer-events: auto; position: fixed; bottom: 20px; right: 20px; width: 46px; height: 46px; background: #000000; border: 1px solid #3c4043; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 2147483646; box-shadow: 0 4px 12px rgba(0,0,0,0.5); transition: transform 0.3s, opacity 0.3s; }
                #kerim-trigger.hidden { transform: scale(0); opacity: 0; pointer-events: none; }
            `;
            document.head.appendChild(style);

            const wrapper = document.createElement('div'); wrapper.id = 'kerim-ui-wrapper';
            wrapper.innerHTML = `
                <div id="kerim-panel">
                    <div id="kerim-main-view">
                        <div class="kerim-lang-row">
                            <button class="kerim-lang-btn" id="kerim-source-btn">Detecting Language…</button>
                            <button class="kerim-swap-btn" id="kerim-swap-btn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z"/></svg></button>
                            <button class="kerim-lang-btn active" id="kerim-target-btn">${getLangName(STATE.targetLang)}</button>
                        </div>
                        <div id="kerim-progress"><div id="kerim-progress-fill"></div></div>
                        <div class="kerim-divider"></div>
                        <div class="kerim-action-row">
                            <button class="kerim-action-btn" id="kerim-undo-btn"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C20.89 11.12 17.06 8 12.5 8z"/></svg> Undo</button>
                            <button class="kerim-action-btn" id="kerim-close-btn"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg> Close</button>
                            <button class="kerim-action-btn primary" id="kerim-translate-btn"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v2h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg> Translate</button>
                        </div>
                    </div>
                    <div id="kerim-lang-view">
                        <div class="kerim-lang-header">
                            <span class="kerim-lang-title" id="kerim-lang-title">Select Target Language</span>
                            <button class="kerim-lang-back" id="kerim-lang-back"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 11H7.83l4.88-4.88c.39-.39.39-1.03 0-1.42-.39-.39-1.02-.39-1.41 0l-6.59 6.59c-.39.39-.39 1.02 0 1.41l6.59 6.59c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L7.83 13H19c.55 0 1-.45 1-1s-.45-1-1-1z"/></svg></button>
                        </div>
                        <input class="kerim-lang-search" id="kerim-lang-search" placeholder="Search language…" type="text">
                        <div class="kerim-lang-list" id="kerim-lang-list"></div>
                    </div>
                </div>
                <div id="kerim-trigger" class="hidden" title="Open Translation Panel">
                    <svg width="22" height="22" viewBox="0 0 24 24" fill="#8ab4f8"><path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v2h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg>
                </div>
            `;
            document.documentElement.appendChild(wrapper);

            document.getElementById('kerim-translate-btn').onclick = () => Controller.start();
            document.getElementById('kerim-undo-btn').onclick = () => Controller.revert();
            document.getElementById('kerim-close-btn').onclick = () => this.toggle(false);
            document.getElementById('kerim-trigger').onclick = () => this.toggle(true);
            document.getElementById('kerim-source-btn').onclick = () => this.openLangPicker('source');
            document.getElementById('kerim-target-btn').onclick = () => this.openLangPicker('target');
            document.getElementById('kerim-lang-back').onclick = () => this.closeLangPicker();
            document.getElementById('kerim-lang-search').oninput = (e) => this.renderLangList(e.target.value);
            
            document.getElementById('kerim-swap-btn').onclick = () => {
                if (STATE.detectedLang && STATE.sourceLang !== 'auto') {
                    const tmp = STATE.sourceLang; STATE.sourceLang = STATE.targetLang; STATE.targetLang = tmp;
                    Store.set('targetLang', STATE.targetLang);
                    document.getElementById('kerim-source-btn').textContent = getLangName(STATE.sourceLang);
                    document.getElementById('kerim-target-btn').textContent = getLangName(STATE.targetLang);
                    Controller.revert();
                }
            };
        }

        static openLangPicker(mode) {
            this._langPickerMode = mode;
            document.getElementById('kerim-lang-title').textContent = mode === 'source' ? 'Select Source Language' : 'Select Target Language';
            document.getElementById('kerim-lang-search').value = '';
            this.renderLangList('');
            document.getElementById('kerim-main-view').style.display = 'none';
            document.getElementById('kerim-lang-view').classList.add('open');
            document.getElementById('kerim-lang-search').focus();
        }

        static closeLangPicker() {
            document.getElementById('kerim-lang-view').classList.remove('open');
            document.getElementById('kerim-main-view').style.display = '';
        }

        static renderLangList(filter) {
            const list = document.getElementById('kerim-lang-list');
            list.innerHTML = '';
            const mode = this._langPickerMode;
            const entries = mode === 'source' ? [['auto', 'Detect Language'], ...Object.entries(LANG_MAP).filter(([k]) => k !== 'auto')] : Object.entries(LANG_MAP).filter(([k]) => k !== 'auto');
            
            for (const [code, name] of entries) {
                if (filter && !name.toLowerCase().includes(filter.toLowerCase()) && !code.includes(filter.toLowerCase())) continue;
                const isDisabled = STATE.disabledLangs.includes(code);
                const isSelected = mode === 'source' ? code === STATE.sourceLang : code === STATE.targetLang;
                const item = document.createElement('div');
                item.className = 'kerim-lang-item' + (isSelected ? ' selected' : '');
                item.innerHTML = `
                    <span>${isDisabled ? '<span class="kerim-disabled-badge">Disabled</span>' : ''}${name}</span>
                    ${mode === 'target' && code !== 'auto' ? `<button class="kerim-disable-btn ${isDisabled ? 'disabled' : ''}" data-code="${code}">${isDisabled ? '✓ Enable' : '⊘ Never Translate'}</button>` : ''}
                `;
                item.addEventListener('click', (e) => {
                    if (e.target.closest('.kerim-disable-btn')) return;
                    if (mode === 'source') { STATE.sourceLang = code; document.getElementById('kerim-source-btn').textContent = name; }
                    else { STATE.targetLang = code; Store.set('targetLang', code); document.getElementById('kerim-target-btn').textContent = name; }
                    this.closeLangPicker();
                });
                
                const disableBtn = item.querySelector('.kerim-disable-btn');
                if (disableBtn) {
                    disableBtn.addEventListener('click', (e) => {
                        e.stopPropagation();
                        const c = e.currentTarget.dataset.code;
                        if (STATE.disabledLangs.includes(c)) STATE.disabledLangs = STATE.disabledLangs.filter(l => l !== c);
                        else STATE.disabledLangs.push(c);
                        Store.set('disabledLangs', STATE.disabledLangs);
                        this.renderLangList(document.getElementById('kerim-lang-search').value);
                    });
                }
                list.appendChild(item);
            }
        }

        static toggle(force) {
            const panel = document.getElementById('kerim-panel');
            const trigger = document.getElementById('kerim-trigger');
            if (!panel) return;

            // Hayalet Modu Kontrolü: Sayfa hedef dille aynıysa buton sürekli saklanır.
            const isNative = (STATE.detectedLang === STATE.targetLang || STATE.disabledLangs.includes(STATE.detectedLang));

            if (force === true) {
                panel.classList.add('visible');
                if(trigger) trigger.classList.add('hidden');
                this.isVisible = true;
            } else if (force === false) {
                panel.classList.remove('visible');
                if(trigger) {
                    if(isNative) trigger.classList.add('hidden');
                    else trigger.classList.remove('hidden');
                }
                this.isVisible = false;
            } else {
                this.isVisible = !this.isVisible;
                panel.classList.toggle('visible', this.isVisible);
                if(trigger) {
                    if (this.isVisible || isNative) trigger.classList.add('hidden');
                    else trigger.classList.remove('hidden');
                }
            }
        }

        static updateDetectedLang(code) {
            document.getElementById('kerim-source-btn').textContent = getLangName(code);
        }

        static updateProgress() {
            const fill = document.getElementById('kerim-progress-fill');
            if (!fill) return;
            if (STATE.totalBlocks === 0) { fill.style.width = '0%'; return; }
            const pct = Math.min(100, Math.round((STATE.processedBlocks / STATE.totalBlocks) * 100));
            fill.style.width = `${pct}%`;
            if (pct >= 100) setTimeout(() => fill.style.width = '0%', 1500);
        }

        static setLoading(on) {
            const bar = document.getElementById('kerim-progress');
            if (bar) bar.classList.toggle('indeterminate', on);
        }
    }

    // ==========================================
    // 5. KONTROLCÜ VE DİL ALGILAMA
    // ==========================================
    class Controller {
        static async detectLanguage() {
            UIManager.setLoading(true);
            
            // FALLBACK: Sayfa HTML'ine gömülü lang etiketini bul
            let fallbackLang = 'auto';
            const htmlLang = document.documentElement.lang;
            if (htmlLang) {
                const baseLang = htmlLang.split('-')[0].toLowerCase();
                if (LANG_MAP[baseLang]) fallbackLang = baseLang;
            }

            // Metin Çekme
            let textSample = "";
            const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
            let node;
            while ((node = walker.nextNode()) && textSample.length < 400) {
                const parent = node.parentNode;
                if (parent && ['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(parent.tagName)) continue;
                const t = node.nodeValue.trim();
                if (t.length > 20) textSample += t + " ";
            }
            
            if (!textSample || textSample.length < 20) {
                UIManager.setLoading(false);
                if (fallbackLang !== 'auto') this.applyLanguage(fallbackLang);
                return;
            }

            try {
                const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=t`;
                const responseText = await FetchAPI(url, "q=" + encodeURIComponent(textSample));
                UIManager.setLoading(false);
                
                const data = JSON.parse(responseText);
                if (data && data[2]) {
                    this.applyLanguage(data[2]);
                } else if (fallbackLang !== 'auto') {
                    this.applyLanguage(fallbackLang);
                }
            } catch (e) {
                UIManager.setLoading(false);
                if (fallbackLang !== 'auto') this.applyLanguage(fallbackLang);
            }
        }
        
        static applyLanguage(lang) {
            STATE.detectedLang = lang;
            STATE.sourceLang = lang;
            UIManager.updateDetectedLang(lang);
            
            if (!STATE.disabledLangs.includes(lang) && lang !== STATE.targetLang) {
                UIManager.toggle(true);
            }
        }

        static start() {
            if (STATE.isActive) return;
            STATE.isActive = true;
            document.getElementById('kerim-translate-btn').style.opacity = '0.5';
            
            const initialBlocks = DOMScanner.getContextualBlocks(document.body);
            STATE.totalBlocks = initialBlocks.length;
            STATE.processedBlocks = 0;
            if (initialBlocks.length > 0) queueManager.add(initialBlocks);
            DOMScanner.startObserver();
        }

        static revert() {
            STATE.isActive = false;
            DOMScanner.stopObserver();
            queueManager.queue = [];
            
            document.querySelectorAll('[data-v-translated]').forEach(node => {
                const bId = node.getAttribute('data-k-id');
                if (bId && ORIGINAL_HTML_MAP.has(bId)) {
                    node.innerHTML = ORIGINAL_HTML_MAP.get(bId);
                }
                node.removeAttribute('data-v-translated');
            });

            STATE.totalBlocks = 0;
            STATE.processedBlocks = 0;
            UIManager.updateProgress();
            const btn = document.getElementById('kerim-translate-btn');
            if (btn) btn.style.opacity = '1';
        }

        static initHooks() {
            if (isGM) {
                GM_registerMenuCommand('🌐 Toggle Translation Panel', () => UIManager.toggle());
            }
            
            let lastScrollY = window.scrollY;
            window.addEventListener('scroll', () => {
                if (!UIManager.isVisible) return;
                const currentScrollY = window.scrollY;
                if (currentScrollY > lastScrollY + 40) UIManager.toggle(false);
                lastScrollY = currentScrollY;
            }, { passive: true });
        }
    }

    function boot() {
        if (!document.documentElement || !document.body) { requestAnimationFrame(boot); return; }
        UIManager.init();
        Controller.initHooks();
        
        if (document.readyState === 'complete') Controller.detectLanguage();
        else window.addEventListener('load', () => Controller.detectLanguage(), { once: true });
        
        console.log('🚀 K-Translate V22 (Ghost UI) is Ready!');
    }
    
    boot();
})();