K-Translate

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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