If the page is already in the target language, the interface and floating button remain completely hidden. Based on Armored Engine V21.
// ==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();
})();