// translation.js // Shared translation logic for all pages. // Requires these vars injected by partials/translation_ui.php before this file loads: // OLLAMA_HOST, OLLAMA_MODEL, SAVE_URL, STATIC_LANGS, CURRENT_LANG // Optional per-page var: // OTHER_PAGES — array of URLs to also translate in background (e.g. ['/cv.php']) const LANG_NAMES = { en: 'English', es: 'Spanish', hu: 'Hungarian', ro: 'Romanian', hi: 'Hindi', fr: 'French', de: 'German', pt: 'Portuguese', it: 'Italian', zh: 'Chinese', ja: 'Japanese', ko: 'Korean', ar: 'Arabic', ru: 'Russian', pl: 'Polish', tr: 'Turkish', sv: 'Swedish', nl: 'Dutch', fi: 'Finnish', cs: 'Czech', sk: 'Slovak', nah: 'Nahuatl', }; const urlLang = new URLSearchParams(window.location.search).get('lang'); const browserLang = (urlLang || navigator.language).slice(0, 2).toLowerCase(); const langName = LANG_NAMES[browserLang] || browserLang.toUpperCase(); const badge = document.getElementById('translation-badge'); const prompt = document.getElementById('translate-prompt'); const btn = document.getElementById('translate-btn'); const actionSpan = document.getElementById('translate-action'); const detectedSpan = document.getElementById('detected-lang-name'); function showBadge(msg) { if (badge) { badge.textContent = msg; badge.style.display = 'block'; } } function hidePrompt() { if (prompt) prompt.style.display = 'none'; } // Send all texts in one Ollama request, get back a JSON array async function translateBatch(texts, targetLang) { const name = LANG_NAMES[targetLang] || targetLang; const prompt = `Translate each item to ${name}. Return ONLY a valid JSON array of translated strings in the same order, no explanations, no markdown. Example input: ["Hello", "How are you"] Example output: ["Bonjour", "Comment allez-vous"] Input: ${JSON.stringify(texts)}`; const response = await fetch(`${OLLAMA_HOST}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: OLLAMA_MODEL, prompt, stream: false }), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); const raw = data.response.trim().replace(/```json|```/g, '').trim(); const result = JSON.parse(raw); if (!Array.isArray(result) || result.length !== texts.length) { throw new Error(`Unexpected response: got ${result.length}, expected ${texts.length}`); } return result; } // Save translated keys — merges with en.php base on the server async function saveLang(lang, translations) { try { const res = await fetch(SAVE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lang, translations }) }); const data = await res.json(); if (data.success) { console.log(`Saved lang/${lang}.php — static next visit`); } else { console.warn('Save failed:', data.error); } } catch (e) { console.warn('Could not save lang file:', e.message); } } // Collect translations from any document's [data-translate] elements // Always reads data-en (English source) regardless of displayed language async function collectPageTranslations(doc, targetLang) { const elements = [...doc.querySelectorAll('[data-translate]')]; if (!elements.length) return {}; const texts = elements.map(el => el.getAttribute('data-en') || el.textContent.trim()); const translated = await translateBatch(texts, targetLang); const result = {}; elements.forEach((el, i) => { const key = el.getAttribute('data-key') || el.getAttribute('data-en'); result[key] = translated[i]; }); return result; } // Main translation flow — triggered by button click async function doTranslation() { hidePrompt(); if (btn) btn.disabled = true; const elements = [...document.querySelectorAll('[data-translate]')]; if (!elements.length) return; showBadge('Translating page...'); elements.forEach(el => el.style.opacity = '0.4'); const allTranslations = {}; // Step 1 — batch translate current page elements live on screen // Always uses data-en (English) as source, not whatever is displayed try { const texts = elements.map(el => el.getAttribute('data-en') || el.textContent.trim()); const translated = await translateBatch(texts, browserLang); elements.forEach((el, i) => { el.textContent = translated[i]; const key = el.getAttribute('data-key') || el.getAttribute('data-en'); allTranslations[key] = translated[i]; }); } catch (e) { console.warn('Page translation failed:', e.message); elements.forEach(el => el.style.opacity = '1'); showBadge('Translation failed — showing original'); return; } elements.forEach(el => el.style.opacity = '1'); // Step 2 — fetch and batch translate OTHER_PAGES in virtual DOM if (typeof OTHER_PAGES !== 'undefined' && OTHER_PAGES.length > 0) { for (const url of OTHER_PAGES) { showBadge(`Translating ${url}...`); try { const res = await fetch(url); const html = await res.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const pageTranslations = await collectPageTranslations(doc, browserLang); Object.assign(allTranslations, pageTranslations); } catch (e) { console.warn(`Could not translate ${url}:`, e.message); } } } // Step 3 — save everything in one call await saveLang(browserLang, allTranslations); showBadge(`Translated by Ollama / ${OLLAMA_MODEL}`); } // Decide what to show based on three cases: // // Case 1 — Page is Nahuatl AND browser is not Nahuatl // → "Switch to English" button (redirect, no Ollama) // // Case 2 — Browser lang already has a static file // → nothing shown (PHP already served it) // // Case 3 — Browser lang is unsupported // → "Translate to " button (calls Ollama, saves file) function initTranslation() { // Case 1 — Nahuatl default page, non-Nahuatl visitor if (CURRENT_LANG === 'nah' && browserLang !== 'nah') { if (actionSpan) actionSpan.textContent = 'Switch to'; if (detectedSpan) detectedSpan.textContent = 'English'; if (prompt) prompt.style.display = 'block'; if (btn) btn.addEventListener('click', () => { window.location.href = window.location.pathname + '?lang=en'; }); return; } // Case 2 — already supported, PHP served it if (STATIC_LANGS.includes(browserLang)) return; // Case 3 — unsupported lang, offer Ollama translation if (browserLang === 'nah') return; // actual Nahuatl browser, nothing to do if (actionSpan) actionSpan.textContent = 'Translate to'; if (detectedSpan) detectedSpan.textContent = langName; if (prompt) prompt.style.display = 'block'; if (btn) btn.addEventListener('click', doTranslation); } initTranslation();