v3.0.0.0
FlatlyPage
- (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'
- };
-
- 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: fixed;
- top: 0;
- left: 0;
- z-index: 150;
- display: none;
- align-items: center;
- gap: 6px;
- height: 28px;
- padding: 0 12px;
- background: var(--bg-elevated);
- border: 1px solid var(--border-focus);
- color: var(--text);
- border-radius: 8px;
- font-family: inherit;
- font-size: 0.6875rem;
- font-weight: 600;
- cursor: pointer;
- box-shadow: var(--shadow-md);
- transition: border-color var(--transition-base), background var(--transition-base);
- }
-
- .translate-btn.visible {
- display: inline-flex;
- }
-
- .translate-btn svg {
- flex-shrink: 0;
- }
-
- .translate-btn:hover {
- border-color: var(--accent);
- background: var(--bg-hover);
- }
-
- .translate-btn:disabled {
- cursor: progress;
- opacity: 0.8;
- }
-
- .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: var(--font-interface, 'Space Grotesk'), -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;
- }
- });
- }
-
- const FIELD_SELECTOR = 'textarea, input[type="text"]';
- const BLOCK_SELECTOR = '.blocks-container .block-content';
- const GAP = 6;
- const EDGE = 8;
-
- let activeField = null;
- let floatBtn = null;
- let busy = false;
-
- const PAGE_TEXT_FIELDS = /^(page_title|page_description|product_title|product_description)$/;
- const NON_TEXT_KEY = /(url|link|href|src|image|icon|poster|slug|color|class|embed)$/i;
- const ID_KEY = /(^|_)id$|[a-z]Id$/;
- const URL_LIKE = /^\s*(https?:\/\/|\/|#|mailto:|tel:|www\.)/i;
-
- function fieldKey(el) {
- const m = (el.getAttribute('oninput') || '').match(/'([A-Za-z0-9_]+)'\s*,\s*this\.value/);
- return m ? m[1] : '';
- }
-
- function isTranslatable(el) {
- if (!(el instanceof HTMLElement) ||
- !el.matches(FIELD_SELECTOR) ||
- el.readOnly ||
- el.disabled ||
- el.closest('.tr-modal') ||
- el.dataset.noTranslate !== undefined) {
- return false;
- }
-
- if (PAGE_TEXT_FIELDS.test(el.name || '')) {
- return true;
- }
-
- if (el.closest(BLOCK_SELECTOR) === null) {
- return false;
- }
-
- if (el.closest('.link-input-wrapper') || el.closest('.image-input-group')) {
- return false;
- }
- const key = fieldKey(el);
- if (NON_TEXT_KEY.test(key) || ID_KEY.test(key)) {
- return false;
- }
- if (el.type === 'url' || URL_LIKE.test(el.value) || /^\s*https?:\/\//i.test(el.placeholder || '')) {
- return false;
- }
- return true;
- }
-
- function ensureButton() {
- if (floatBtn) return floatBtn;
- floatBtn = document.createElement('button');
- floatBtn.type = 'button';
- floatBtn.className = 'translate-btn';
- floatBtn.innerHTML = `${ICONS.translate} Translate`;
- floatBtn.addEventListener('mousedown', (e) => e.preventDefault());
- floatBtn.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (activeField && !busy) {
- const field = activeField;
- busy = true;
- handleTranslate(field, floatBtn).finally(() => {
- busy = false;
- position();
- });
- }
- });
- document.body.appendChild(floatBtn);
- return floatBtn;
- }
-
- function hide() {
- if (floatBtn) floatBtn.classList.remove('visible');
- }
-
- function position() {
- if (!activeField || !document.contains(activeField)) {
- activeField = null;
- hide();
- return;
- }
-
- const btn = ensureButton();
- const rect = activeField.getBoundingClientRect();
- const vw = window.innerWidth;
- const vh = window.innerHeight;
-
- if (rect.bottom < 0 || rect.top > vh || rect.width === 0) {
- hide();
- return;
- }
-
- btn.classList.add('visible');
- const w = btn.offsetWidth;
- const h = btn.offsetHeight;
-
- let left = rect.right - w;
- left = Math.max(EDGE, Math.min(left, vw - w - EDGE));
-
- let top = rect.top - h - GAP;
- const header = document.querySelector('.main-header');
- const minTop = header ? header.getBoundingClientRect().bottom + 2 : EDGE;
- if (top < minTop) {
- top = Math.min(minTop, vh - h - EDGE);
- }
-
- btn.style.left = left + 'px';
- btn.style.top = top + 'px';
- }
-
- document.addEventListener('focusin', (e) => {
- if (floatBtn && (e.target === floatBtn || floatBtn.contains(e.target))) return;
- if (isTranslatable(e.target)) {
- activeField = e.target;
- position();
- } else if (!(e.target.closest && e.target.closest('.tr-modal'))) {
- activeField = null;
- hide();
- }
- });
-
- document.addEventListener('focusout', (e) => {
- if (e.target !== activeField || busy) return;
- setTimeout(() => {
- if (busy) return;
- const now = document.activeElement;
- if (now !== activeField && !(floatBtn && floatBtn.contains(now))) {
- activeField = null;
- hide();
- }
- }, 150);
- });
-
- let positionFrame = 0;
- const schedulePosition = () => {
- if (!activeField || positionFrame) return;
- positionFrame = requestAnimationFrame(() => {
- positionFrame = 0;
- position();
- });
- };
- ['scroll', 'resize'].forEach((evt) => window.addEventListener(evt, schedulePosition, { capture: true, passive: true }));
- document.addEventListener('input', (e) => {
- if (e.target === activeField) position();
- });
-
- (async function init() {
- apiConfig = await loadAPIConfig();
- })();
- })();
-