Auto-sync enabled

FlatlyPage

Version 1.0.1 • 59 files • 798.19 KB
assets/js/translate.js
(function() {
    'use strict';

    let lastSourceLang = localStorage.getItem('translator_source') || 'pl';
    let lastTargetLang = localStorage.getItem('translator_target') || 'en';
    let selectedAPI = localStorage.getItem('translator_api') || 'mymemory';
    let apiConfig = null;

    const LANGUAGES = {
        'en': 'English', 'pl': 'Polish', 'de': 'German', 'es': 'Spanish',
        'fr': 'French', 'it': 'Italian', 'pt': 'Portuguese', 'ru': 'Russian',
        'ja': 'Japanese', 'zh': 'Chinese', 'ar': 'Arabic', 'nl': 'Dutch',
        'sv': 'Swedish', 'no': 'Norwegian', 'da': 'Danish', 'fi': 'Finnish',
        'cs': 'Czech', 'sk': 'Slovak', 'hu': 'Hungarian', 'ro': 'Romanian',
        'tr': 'Turkish', 'uk': 'Ukrainian', 'ko': 'Korean'
    };

    // SVG Icons
    const ICONS = {
        translate: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/></svg>',
        globe: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>',
        search: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>',
        settings: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6m5.2-14.8L13.4 7.6m-2.8 8.8-3.8 3.8M23 12h-6m-6 0H1m17.8 5.2-3.8-3.8m-8.8-2.8L1.2 6.8"/></svg>'
    };

    const style = document.createElement('style');
    style.textContent = `
    .translate-btn {
        position: absolute;
        right: 10px;
        top: 10px;
        background: var(--bg-elevated);
        border: 1px solid var(--border);
        color: var(--text);
        border-radius: var(--radius-sm);
        padding: 6px 14px;
        cursor: pointer;
        font-size: 0.6875rem;
        font-weight: 500;
        opacity: 0;
        transition: all var(--transition-smooth);
        z-index: 10;
        display: flex;
        align-items: center;
        gap: 6px;
        pointer-events: auto;
    }

    .translate-btn.below-position {
        position: absolute;
        top: calc(100% + 6px);
        right: 10px;
        transform: none;
    }

    .translate-btn svg {
        flex-shrink: 0;
    }

    *:hover > .translate-btn,
    textarea:focus ~ .translate-btn,
    .translate-btn:hover {
        opacity: 1;
        transform: translateY(0);
    }

    .translate-btn.below-position:hover {
        transform: translateY(0);
    }

    .translate-btn:hover {
        border-color: var(--blue);
        background: var(--bg-hover);
        box-shadow: 0 0 15px rgba(59, 130, 246, 0.2);
    }

    .tr-overlay {
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.75);
        backdrop-filter: blur(12px);
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 1000;
        padding: 20px;
        animation: fadeIn var(--transition-base) ease-out;
    }

    [data-theme="light"] .tr-overlay {
        background: rgba(15, 23, 42, 0.5);
    }

    .tr-modal {
        background: var(--bg-elevated);
        border: 1px solid var(--border);
        border-radius: var(--radius);
        width: 100%;
        max-width: 580px;
        padding: 24px;
        box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
        font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
        color: var(--text);
        animation: modalSlideIn var(--transition-smooth) cubic-bezier(0.16, 1, 0.3, 1);
    }

    [data-theme="light"] .tr-modal {
        box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
    }

    .tr-title {
        font-size: 1rem;
        font-weight: 600;
        margin: 0 0 20px 0;
        display: flex;
        align-items: center;
        justify-content: space-between;
    }

    .tr-title-left {
        display: flex;
        align-items: center;
        gap: 10px;
    }

    .tr-title svg {
        color: var(--blue);
    }

    .tr-search {
        width: 100%;
        background: var(--bg-card);
        border: 1px solid var(--border);
        color: var(--text);
        padding: 12px 12px 12px 40px;
        border-radius: var(--radius-sm);
        margin-bottom: 20px;
        outline: none;
        transition: border-color var(--transition-base);
        font-size: 0.875rem;
    }

    .tr-search-wrapper {
        position: relative;
    }

    .tr-search-icon {
        position: absolute;
        left: 12px;
        top: 22px;
        transform: translateY(-50%);
        color: var(--text-muted);
        pointer-events: none;
        z-index: 1;
    }

    .tr-search:focus {
        border-color: var(--border-focus);
    }

    .tr-label {
        font-size: 0.6875rem;
        font-weight: 600;
        color: var(--text-subtle);
        text-transform: uppercase;
        letter-spacing: 0.05em;
        margin-bottom: 10px;
        display: block;
    }

    .tr-api-selector {
        display: flex;
        gap: 8px;
        margin-bottom: 20px;
        padding: 4px;
        background: var(--bg-card);
        border-radius: var(--radius-sm);
    }

    .tr-api-option {
        flex: 1;
        padding: 10px;
        border-radius: var(--radius-sm);
        cursor: pointer;
        text-align: center;
        font-size: 0.8125rem;
        color: var(--text-muted);
        transition: all var(--transition-base);
        border: 1px solid transparent;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 4px;
    }

    .tr-api-option:hover {
        color: var(--text);
        background: var(--bg-hover);
    }

    .tr-api-option.selected {
        background: rgba(59, 130, 246, 0.1);
        border-color: var(--blue);
        color: var(--text);
        font-weight: 600;
    }

    .tr-api-badge {
        display: inline-block;
        font-size: 0.625rem;
        padding: 2px 6px;
        background: var(--bg-elevated);
        border-radius: 4px;
        margin-left: 0;
    }

    .tr-api-badge.free {
        background: var(--success);
        color: white;
    }

    .tr-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
        gap: 8px;
        max-height: 140px;
        overflow-y: auto;
        padding-right: 5px;
        margin-bottom: 20px;
    }

    .tr-grid::-webkit-scrollbar {
        width: 4px;
    }

    .tr-grid::-webkit-scrollbar-thumb {
        background: var(--border);
        border-radius: 10px;
    }

    .tr-grid::-webkit-scrollbar-thumb:hover {
        background: var(--text-muted);
    }

    .tr-opt {
        background: var(--bg-card);
        border: 1px solid var(--border);
        padding: 10px;
        border-radius: var(--radius-sm);
        cursor: pointer;
        text-align: center;
        font-size: 0.8125rem;
        color: var(--text-muted);
        transition: all var(--transition-base);
    }

    .tr-opt:hover {
        background: var(--bg-hover);
        color: var(--text);
        border-color: var(--border-focus);
    }

    .tr-opt.selected {
        background: rgba(59, 130, 246, 0.1);
        border-color: var(--blue);
        color: var(--text);
        font-weight: 500;
        box-shadow: inset 0 0 10px rgba(59, 130, 246, 0.1);
    }

    .tr-footer {
        display: flex;
        justify-content: flex-end;
        gap: 12px;
        margin-top: 24px;
    }

    .tr-btn-main {
        padding: 12px 24px;
        border-radius: var(--radius-sm);
        border: none;
        font-weight: 600;
        cursor: pointer;
        transition: all var(--transition-base);
        font-size: 0.875rem;
    }

    .tr-btn-cancel {
        background: transparent;
        color: var(--text-muted);
    }

    .tr-btn-cancel:hover {
        color: var(--text);
    }

    .tr-btn-confirm {
        background: var(--blue);
        color: white;
        box-shadow: 0 4px 14px 0 rgba(59, 130, 246, 0.39);
    }

    .tr-btn-confirm:hover {
        transform: translateY(-1px);
        box-shadow: 0 6px 20px rgba(59, 130, 246, 0.23);
    }

    .tr-btn-confirm:active {
        transform: scale(0.98);
    }

    .loader-spin {
        width: 14px;
        height: 14px;
        border: 2px solid rgba(255, 255, 255, 0.2);
        border-top-color: white;
        border-radius: 50%;
        animation: spin 0.8s linear infinite;
    }

    @keyframes spin {
        to {
            transform: rotate(360deg);
        }
    }

    @media (max-width: 768px) {
        .tr-modal {
            max-width: 100%;
            border-radius: var(--radius) var(--radius) 0 0;
            padding: 20px;
        }

        .tr-title {
            font-size: 1.125rem;
        }

        .tr-api-option {
            font-size: 0.75rem;
            padding: 8px;
        }

        .tr-grid {
            grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
        }

        .tr-footer {
            flex-direction: column;
            gap: 10px;
        }

        .tr-btn-main {
            width: 100%;
        }
    }
    `;
    document.head.appendChild(style);

    async function loadAPIConfig() {
        try {
            const response = await fetch('../api/get-translate-config');
            return await response.json();
        } catch (e) {
            console.error('Failed to load API config:', e);
            return {
                'mymemory': {
                    name: 'MyMemory',
                    free: true,
                    rateLimit: '1000/day'
                }
            };
        }
    }

    function showModal(callback) {
        const overlay = document.createElement('div');
        overlay.className = 'tr-overlay';
        
        const apiButtons = apiConfig ? Object.entries(apiConfig).map(([id, config]) => `
            <div class="tr-api-option ${id === selectedAPI ? 'selected' : ''}" data-api="${id}">
                ${config.name}
                ${config.free ? '<span class="tr-api-badge free">FREE</span>' : '<span class="tr-api-badge">PRO</span>'}
            </div>
        `).join('') : '';

        overlay.innerHTML = `
            <div class="tr-modal">
                <div class="tr-title">
                    <div class="tr-title-left">
                        ${ICONS.globe} Smart Translator
                    </div>
                </div>

                ${apiConfig ? `
                    <div class="tr-label">Translation Provider</div>
                    <div class="tr-api-selector">
                        ${apiButtons}
                    </div>
                ` : ''}
                
                <div class="tr-search-wrapper">
                    <span class="tr-search-icon">${ICONS.search}</span>
                    <input type="text" class="tr-search" placeholder="Search languages...">
                </div>
                
                <span class="tr-label">Source Language</span>
                <div class="tr-grid" id="src-grid"></div>

                <span class="tr-label">Target Language</span>
                <div class="tr-grid" id="trg-grid"></div>

                <div class="tr-footer">
                    <button class="tr-btn-main tr-btn-cancel">Close</button>
                    <button class="tr-btn-main tr-btn-confirm">Translate Content</button>
                </div>
            </div>`;
        document.body.appendChild(overlay);

        let sSrc = lastSourceLang;
        let sTrg = lastTargetLang;
        let sAPI = selectedAPI;

        overlay.querySelectorAll('.tr-api-option').forEach(opt => {
            opt.onclick = () => {
                sAPI = opt.dataset.api;
                overlay.querySelectorAll('.tr-api-option').forEach(o => o.classList.remove('selected'));
                opt.classList.add('selected');
            };
        });

        const render = (filter = '') => {
            const srcG = overlay.querySelector('#src-grid');
            const trgG = overlay.querySelector('#trg-grid');
            srcG.innerHTML = ''; trgG.innerHTML = '';

            Object.entries(LANGUAGES).forEach(([code, name]) => {
                if (!name.toLowerCase().includes(filter.toLowerCase())) return;

                [ {g: srcG, cur: sSrc, set: (c)=>sSrc=c}, {g: trgG, cur: sTrg, set: (c)=>sTrg=c} ].forEach(config => {
                    const el = document.createElement('div');
                    el.className = `tr-opt ${code === config.cur ? 'selected' : ''}`;
                    el.textContent = name;
                    el.onclick = () => { config.set(code); render(filter); };
                    config.g.appendChild(el);
                });
            });
        };

        overlay.querySelector('.tr-search').oninput = (e) => render(e.target.value);
        overlay.querySelector('.tr-btn-cancel').onclick = () => overlay.remove();
        overlay.querySelector('.tr-btn-confirm').onclick = () => {
            lastSourceLang = sSrc; lastTargetLang = sTrg; selectedAPI = sAPI;
            localStorage.setItem('translator_source', sSrc);
            localStorage.setItem('translator_target', sTrg);
            localStorage.setItem('translator_api', sAPI);
            overlay.remove();
            callback(sSrc, sTrg, sAPI);
        };

        render();
    }

    async function translateText(text, src, trg, api) {
        const response = await fetch('../api/translate', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                text: text,
                source: src,
                target: trg,
                provider: api
            })
        });

        if (!response.ok) {
            const error = await response.json();
            console.error('❌ Backend error:', error);
            throw new Error(error.error || 'Translation failed');
        }

        const data = await response.json();
        return data.translatedText;
    }

    async function handleTranslate(field, btn) {
        const val = field.value.trim();
        
        if (!val) {
            alert('Please enter text to translate');
            return;
        }

        showModal(async (src, trg, api) => {
            const original = btn.innerHTML;
            btn.innerHTML = `<div class="loader-spin"></div>`;
            btn.classList.add('loading');
            btn.disabled = true;

            try {
                const translated = await translateText(val, src, trg, api);
                
                if (translated) {
                    field.value = translated;
                    field.dispatchEvent(new Event('input', { bubbles: true }));
                } else {
                    alert('Translation failed or limit exceeded');
                }
            } catch (e) {
                console.error('❌ Translation error:', e);
                alert('Connection failed: ' + e.message);
            } finally {
                btn.innerHTML = original;
                btn.classList.remove('loading');
                btn.disabled = false;
            }
        });
    }

    function hasBlockActions(field) {
        const parent = field.parentElement;
        return parent && parent.querySelector('.block-actions') !== null;
    }

    function inject(field) {
        if (field.dataset.hasTr === 'true' || 
            field.type === 'hidden' || 
            field.readOnly || 
            field.closest('.tr-modal')) {
            return;
        }

        const btn = document.createElement('button');
        btn.type = 'button'; 
        btn.className = 'translate-btn';
        
        if (hasBlockActions(field)) {
            btn.classList.add('below-position');
        }
        
        btn.innerHTML = `${ICONS.translate} Translate`;
        btn.onclick = (e) => { 
            e.preventDefault(); 
            e.stopPropagation();
            handleTranslate(field, btn); 
        };
        
        if (getComputedStyle(field.parentElement).position === 'static') {
            field.parentElement.style.position = 'relative';
        }
        
        field.after(btn);
        field.dataset.hasTr = 'true';
    }

    (async function init() {
        apiConfig = await loadAPIConfig();
        const scan = () => document.querySelectorAll('textarea, input[type="text"]').forEach(inject);
        setInterval(scan, 3000);
        scan();
    })();
})();