Torn Chat Translator

Add a translate icon to the chat in Torn

// ==UserScript==
// @name         Torn Chat Translator
// @namespace    http://tampermonkey.net/
// @version      1.0.3
// @description  Add a translate icon to the chat in Torn
// @author       JESUUS [2353554]
// @license      MIT
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';
    
    const CONFIG = {
        translateIconSvg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-translate" viewBox="0 0 16 16"><path d="M4.545 6.714 4.11 8H3l1.862-5h1.284L8 8H6.833l-.435-1.286zm1.634-.736L5.5 3.956h-.049l-.679 2.022z"/><path d="M0 2a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v3h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-3H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zm7.138 9.995q.289.451.63.846c-.748.575-1.673 1.001-2.768 1.292.178.217.451.635.555.867 1.125-.359 2.08-.844 2.886-1.494.777.665 1.739 1.165 2.93 1.472.133-.254.414-.673.629-.89-1.125-.253-2.057-.694-2.82-1.284.681-.747 1.222-1.651 1.621-2.757H14V8h-3v1.047h.765c-.318.844-.74 1.546-1.272 2.13a6 6 0 0 1-.415-.492 2 2 0 0 1-.94.31"/></svg>',
        targetLanguage: 'en',
        sourceLanguage: 'auto'
    };
    
    function insertTextNaturally(textarea, text) {
        try {
            if (document.execCommand && document.queryCommandSupported('insertText')) {
                const success = document.execCommand('insertText', false, text);
                if (success) {
                    return;
                }
            }
        } catch (e) {
            console.log('execCommand failed, using fallback');
        }
        
        const originalValue = textarea.value;
        textarea.value = text;
        textarea.selectionStart = textarea.selectionEnd = text.length;
        
        textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
        textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
        textarea.dispatchEvent(new Event('keyup', { bubbles: true, cancelable: true }));
        textarea.dispatchEvent(new Event('blur', { bubbles: true, cancelable: true }));
        textarea.dispatchEvent(new Event('focus', { bubbles: true, cancelable: true }));
    }
    
    async function translateText(text, sourceLang = 'auto', targetLang = 'en') {
        try {
            const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
            
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    onload: function(response) {
                        try {
                            const data = JSON.parse(response.responseText);
                            const translatedText = data[0][0][0];
                            resolve(translatedText);
                        } catch (error) {
                            reject(new Error('Translation error: ' + error.message));
                        }
                    },
                    onerror: function(error) {
                        reject(new Error('Connection error: ' + error));
                    }
                });
            });
        } catch (error) {
            throw new Error('Translation error: ' + error.message);
        }
    }
    
    function setupAutoClear(textarea) {
        const parent = textarea.closest('.chat-box-footer, .tt-chat-autocomp, .chat-input-container') || textarea.parentElement;
        
        const sendButtons = parent.querySelectorAll('button, input[type="submit"], .send-button, [class*="send"]');
        
        sendButtons.forEach(button => {
            button.addEventListener('click', () => {
                setTimeout(() => {
                    if (textarea.value.trim() === '') {
                        textarea.blur();
                        textarea.focus();
                    }
                }, 200);
            });
        });
        
        textarea.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                setTimeout(() => {
                    if (textarea.value.trim() === '') {
                        textarea.blur();
                        textarea.focus();
                    }
                }, 200);
            }
        });
    }
    
    function createTranslateIcon() {
        const icon = document.createElement('button');
        icon.className = 'translate-icon';
        icon.title = 'Translate to English';
        icon.style.cssText = `
            position: absolute;
            right: 50px;
            top: 50%;
            transform: translateY(-50%);
            background: none;
            border: none;
            cursor: pointer;
            padding: 5px;
            border-radius: 3px;
            transition: background-color 0.2s;
            z-index: 1000;
        `;
        
        icon.innerHTML = CONFIG.translateIconSvg;
        icon.style.cssText += `
            color: rgba(255, 255, 255, 0.7);
        `;
        
        icon.addEventListener('mouseenter', () => {
            icon.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
            icon.style.color = 'rgba(255, 255, 255, 1)';
        });
        
        icon.addEventListener('mouseleave', () => {
            icon.style.backgroundColor = 'transparent';
            icon.style.color = 'rgba(255, 255, 255, 0.7)';
        });
        
        return icon;
    }
    
    function addTranslateIconToChat() {
        const chatSelectors = [
            'textarea[placeholder*="message"]',
            'textarea[placeholder*="Type your message"]',
            '.chat-box-footer textarea',
            '.tt-chat-autocomp',
            'textarea.chat-input'
        ];
        
        chatSelectors.forEach(selector => {
            const textareas = document.querySelectorAll(selector);
            
            textareas.forEach(textarea => {
                if (textarea.parentElement.querySelector('.translate-icon')) {
                    return;
                }
                
                const parent = textarea.parentElement;
                if (getComputedStyle(parent).position === 'static') {
                    parent.style.position = 'relative';
                }
                
                const translateIcon = createTranslateIcon();
                
                setupAutoClear(textarea);
                
                translateIcon.addEventListener('click', async (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    
                    const textToTranslate = textarea.value.trim();
                    
                    if (!textToTranslate) {
                        showNotification('Nothing to translate!', 'warning');
                        return;
                    }
                    
                    try {
                        const originalHtml = translateIcon.innerHTML;
                        translateIcon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="2"><animate attributeName="stroke-dasharray" values="0 38;19 19;0 38;0 38" dur="2s" repeatCount="indefinite"/><animate attributeName="stroke-dashoffset" values="0;0;-19;-38" dur="2s" repeatCount="indefinite"/></circle></svg>';
                        translateIcon.disabled = true;
                        
                        const translatedText = await translateText(textToTranslate, CONFIG.sourceLanguage, CONFIG.targetLanguage);
                        
                        textarea.focus();
                        textarea.select();
                        insertTextNaturally(textarea, translatedText);
                        textarea.focus();
                        
                        const protectTranslation = () => {
                            if (textarea.value !== translatedText && textarea.value.trim() === '') {
                                textarea.value = translatedText;
                            }
                        };
                        
                        const protectionInterval = setInterval(protectTranslation, 100);
                        setTimeout(() => clearInterval(protectionInterval), 2000);
                        
                        showNotification('Text translated successfully!', 'success');
                        
                    } catch (error) {
                        console.error('Translation error:', error);
                        showNotification('Translation error: ' + error, 'error');
                    } finally {
                        translateIcon.innerHTML = CONFIG.translateIconSvg;
                        translateIcon.disabled = false;
                    }
                });
                
                parent.appendChild(translateIcon);
            });
        });
    }
    
    function showNotification(message, type = 'info') {
        const notification = document.createElement('div');
        notification.textContent = message;
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 10px 15px;
            border-radius: 5px;
            color: white;
            font-weight: bold;
            z-index: 10000;
            animation: slideIn 0.3s ease-out;
        `;
        
        const colors = {
            success: '#4CAF50',
            error: '#f44336',
            warning: '#ff9800',
            info: '#2196F3'
        };
        
        notification.style.backgroundColor = colors[type] || colors.info;
        
        const style = document.createElement('style');
        style.textContent = `
            @keyframes slideIn {
                from { transform: translateX(100%); opacity: 0; }
                to { transform: translateX(0); opacity: 1; }
            }
        `;
        document.head.appendChild(style);
        
        document.body.appendChild(notification);
        
        setTimeout(() => {
            notification.remove();
            style.remove();
        }, 3000);
    }
    
    function observeDOM() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            setTimeout(addTranslateIconToChat, 100);
                        }
                    });
                }
            });
        });
        
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }
    
    function initialize() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                setTimeout(addTranslateIconToChat, 1000);
                observeDOM();
            });
        } else {
            setTimeout(addTranslateIconToChat, 1000);
            observeDOM();
        }
        
        setInterval(addTranslateIconToChat, 5000);
    }
    
    initialize();
    })();