/** * translate.js — Legal Translation tool handler. * * Single-pass: POST text + language pair → Azure GPT-4o-mini → translated text * with optional legal-term annotations. Streams NDJSON. * * UI strings from window.DBN_LT_I18N (populated by translate.php inline script). */ (function () { 'use strict'; // ── i18n helper ─────────────────────────────────────────────────────────── var I18N = window.DBN_LT_I18N || {}; function t(key, vars) { var s = (I18N && I18N[key]) || key; if (vars) { Object.keys(vars).forEach(function (k) { s = s.split('{' + k + '}').join(String(vars[k])); }); } return s; } // ── Element refs ────────────────────────────────────────────────────────── var form = document.getElementById('ltForm'); var runBtn = document.getElementById('ltRunButton'); var statusEl = document.getElementById('ltStatus'); var resultsEl = document.getElementById('ltResults'); var textarea = document.getElementById('ltInput'); var uploadZone = document.getElementById('ltUploadZone'); var uploadInput = document.getElementById('ltUploadInput'); var uploadPrompt = document.getElementById('ltUploadPrompt'); var uploadFileInfo = document.getElementById('ltUploadFileInfo'); var uploadFileList = document.getElementById('ltUploadFileList'); var uploadClear = document.getElementById('ltUploadClear'); // ── State ───────────────────────────────────────────────────────────────── var _extractedFiles = []; var _currentLang = window.DBN_LT_LANG || window.DBN_CURRENT_LANG || 'no'; var _lastResult = null; // ── Lang switcher ───────────────────────────────────────────────────────── document.querySelectorAll('.lt-lang-btn').forEach(function (btn) { btn.addEventListener('click', function () { var lang = btn.dataset.lang || 'no'; if (lang === _currentLang) return; var url = new URL(window.location.href); url.searchParams.set('lang', lang); window.location.href = url.toString(); }); }); // ── File upload ─────────────────────────────────────────────────────────── if (uploadZone) { uploadZone.addEventListener('dragover', function (e) { e.preventDefault(); uploadZone.classList.add('is-drag-over'); }); uploadZone.addEventListener('dragleave', function (e) { if (!uploadZone.contains(e.relatedTarget)) { uploadZone.classList.remove('is-drag-over'); } }); uploadZone.addEventListener('drop', function (e) { e.preventDefault(); uploadZone.classList.remove('is-drag-over'); if (e.dataTransfer && e.dataTransfer.files.length) { handleFiles(e.dataTransfer.files); } }); var browseLabel = uploadZone.querySelector('label[for="' + (uploadInput && uploadInput.id) + '"]'); if (browseLabel) { browseLabel.addEventListener('click', function (e) { e.stopPropagation(); }); } if (uploadInput) { uploadInput.addEventListener('click', function (e) { e.stopPropagation(); }); } uploadZone.addEventListener('click', function (e) { if (e.target === uploadClear || (uploadClear && uploadClear.contains(e.target))) return; if (e.target === uploadInput) return; var lbl = e.target.closest && e.target.closest('label'); if (lbl && lbl.getAttribute('for') === (uploadInput && uploadInput.id)) return; if (uploadInput) uploadInput.click(); }); } if (uploadInput) { uploadInput.addEventListener('change', function () { if (uploadInput.files && uploadInput.files.length) handleFiles(uploadInput.files); }); } if (uploadClear) { uploadClear.addEventListener('click', function (e) { e.stopPropagation(); resetUpload(); }); } function resetUpload() { _extractedFiles = []; if (uploadInput) uploadInput.value = ''; if (uploadPrompt) uploadPrompt.classList.remove('is-hidden'); if (uploadFileInfo) uploadFileInfo.classList.add('is-hidden'); if (uploadFileList) uploadFileList.innerHTML = ''; } function handleFiles(fileList) { var files = Array.from(fileList).slice(0, 5); if (!files.length) return; setStatus(t('extractingFiles', { n: files.length })); setBusy(true); var promises = files.map(function (file) { var fd = new FormData(); fd.append('file', file); return fetch('api/extract.php', { method: 'POST', credentials: 'same-origin', body: fd }) .then(function (r) { return r.json(); }) .then(function (data) { if (!data.ok) throw new Error(data.error || 'Extraction failed for ' + file.name); return { name: file.name, text: data.text || '', chars: data.chars || 0 }; }); }); Promise.all(promises) .then(function (results) { _extractedFiles = results; renderFileList(results); setStatus(''); setBusy(false); }) .catch(function (err) { setStatus(t('errorPrefix') + ': ' + err.message); setBusy(false); }); } function renderFileList(files) { if (!uploadFileList) return; uploadFileList.innerHTML = files.map(function (f) { return '
  • ' + esc(f.name) + '' + ' ' + f.chars.toLocaleString() + ' chars
  • '; }).join(''); if (uploadPrompt) uploadPrompt.classList.add('is-hidden'); if (uploadFileInfo) uploadFileInfo.classList.remove('is-hidden'); } // ── Form submission ─────────────────────────────────────────────────────── if (form) { form.addEventListener('submit', function (e) { e.preventDefault(); runTranslation(); }); } async function runTranslation() { var pastedText = textarea ? textarea.value.trim() : ''; var fileText = _extractedFiles.map(function (f) { return f.text; }).join('\n\n---\n\n'); var combined = [fileText, pastedText].filter(Boolean).join('\n\n---\n\n'); var docIdsEl = document.getElementById('docPickerIds'); var rawDocIds = docIdsEl ? docIdsEl.value.trim() : ''; var docIds = rawDocIds ? rawDocIds.split(',').map(Number).filter(Boolean) : []; if (!combined && !docIds.length) { setStatus(t('needInput')); return; } var sourceLang = (document.querySelector('input[name="ltSourceLang"]:checked') || {}).value || 'no'; var targetLang = (document.querySelector('input[name="ltTargetLang"]:checked') || {}).value || 'en'; if (sourceLang === targetLang) { setStatus(t('sameLangError')); return; } var docType = (document.querySelector('input[name="ltDocType"]:checked') || {}).value || 'auto'; var payload = { text: combined, language: _currentLang, source_lang: sourceLang, target_lang: targetLang, doc_type: docType, }; if (docIds.length) payload.doc_ids = docIds; _lastResult = null; setBusy(true); setStatus(t('translatingStatus')); showBusy(); try { var resp = await fetch('api/translate.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!resp.ok || !resp.body) { throw new Error(t('serverReturned') + ' ' + resp.status); } var reader = resp.body.getReader(); var decoder = new TextDecoder(); var buffer = ''; while (true) { var _ref = await reader.read(); if (_ref.done) break; buffer += decoder.decode(_ref.value, { stream: true }); var lines = buffer.split('\n'); buffer = lines.pop(); for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (!line) continue; var data; try { data = JSON.parse(line); } catch (_) { continue; } handleEvent(data, payload); } } } catch (err) { showError(err.message || 'Request failed.'); } finally { setBusy(false); setStatus(''); } } function handleEvent(data, payload) { if (data.event === 'progress') { setStatus(data.detail || ''); } else if (data.event === 'final') { _lastResult = data.result || {}; renderResult(_lastResult, payload); if (typeof window.dbnShowSaveResultButton === 'function') { window.dbnShowSaveResultButton( 'translate', payload || {}, _lastResult, { model: (_lastResult && _lastResult.model) || 'gpt-4o-mini', latency_ms: (_lastResult && _lastResult.latency_ms) || 0, }, resultsEl ); } } else if (data.event === 'error') { showError(data.message || data.error || 'An error occurred.'); } } // ── Result rendering ────────────────────────────────────────────────────── function showBusy() { if (!resultsEl) return; resultsEl.innerHTML = '

    ' + esc(t('translatingStatus')) + '

    '; } function renderResult(result, payload) { if (!resultsEl) return; var translatedText = (result.translated_text || '').replace(/\n/g, '
    '); var annotations = Array.isArray(result.annotations) ? result.annotations : []; var disclaimer = result.disclaimer || t('disclaimer'); var sourceLang = (payload && payload.source_lang) || ''; var targetLang = (payload && payload.target_lang) || ''; var copyId = 'ltCopyBtn-' + Date.now(); var html = '
    ' + '
    ' + '

    ' + esc(t('resultTitle')) + '

    ' + (sourceLang && targetLang ? '' + esc(sourceLang.toUpperCase()) + ' → ' + esc(targetLang.toUpperCase()) + '' : '') + '' + '
    ' + '
    ' + translatedText + '
    '; if (annotations.length) { html += '

    ' + esc(t('annotationsTitle')) + '

    '; annotations.forEach(function (ann) { if (!ann || !ann.term) return; html += '
    ' + '' + esc(ann.term) + '' + (ann.explanation ? ' — ' + esc(ann.explanation) : '') + '
    '; }); html += '
    '; } if (disclaimer) { html += '

    ' + esc(disclaimer) + '

    '; } html += '
    '; resultsEl.innerHTML = html; var copyBtn = document.getElementById(copyId); if (copyBtn) { copyBtn.addEventListener('click', function () { var plain = (result.translated_text || '').replace(//gi, '\n'); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(plain).then(function () { copyBtn.textContent = t('copyDone'); setTimeout(function () { copyBtn.textContent = t('copyButton'); }, 2000); }); } }); } } function showError(msg) { if (resultsEl) { resultsEl.innerHTML = '

    ' + esc(t('errorPrefix')) + '

    ' + esc(msg) + '

    '; } setStatus(''); } // ── Helpers ─────────────────────────────────────────────────────────────── function setBusy(on) { if (runBtn) runBtn.disabled = on; if (runBtn) runBtn.textContent = on ? t('runButtonBusy') : t('runButton'); } function setStatus(msg) { if (statusEl) statusEl.textContent = msg; } function esc(s) { return String(s == null ? '' : s) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } }());