diff --git a/api/translate.php b/api/translate.php new file mode 100644 index 0000000..ec4920d --- /dev/null +++ b/api/translate.php @@ -0,0 +1,175 @@ + 0) { @ob_end_clean(); } +ob_implicit_flush(true); + +header('Content-Type: application/x-ndjson; charset=utf-8'); +header('Cache-Control: no-store'); +header('X-Accel-Buffering: no'); + +$startTime = microtime(true); +$language = 'en'; + +$emit = function (string $event, array $payload = []) use ($startTime): void { + $payload['event'] = $event; + $payload['t_ms'] = (int)round((microtime(true) - $startTime) * 1000); + echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"; + @flush(); +}; + +try { + $input = dbnToolsJsonInput(400000); + $language = dbnToolsNormalizeLanguage($input['language'] ?? 'en'); + $sourceLang = dbnToolsNormalizeLanguage($input['source_lang'] ?? 'no'); + $targetLang = dbnToolsNormalizeLanguage($input['target_lang'] ?? 'en'); + + $allowedDocTypes = ['auto','barnevernet','adopsjon','emergency','samvær','fylkesnemnd','other']; + $docType = (string)($input['doc_type'] ?? 'auto'); + if (!in_array($docType, $allowedDocTypes, true)) { + $docType = 'auto'; + } + + if ($sourceLang === $targetLang) { + throw new DbnToolsHttpException( + 'Source and target languages must be different.', + 422, 'same_language' + ); + } + + $text = dbnToolsInjectDocContent($input, dbnToolsString($input, 'text', 200000, false)); + if (mb_strlen(trim($text), 'UTF-8') < 10) { + throw new DbnToolsHttpException( + 'Please paste text or upload a file to translate.', + 422, 'empty_text' + ); + } + + $ftUid = dbnToolsFreeTierCheck('translate'); + $ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'translate'); + if ($ftRemaining >= 0) { + header('X-Credits-Remaining: ' . $ftRemaining); + } + + $emit('start', [ + 'mode' => 'translate', + 'language' => $language, + 'source_lang' => $sourceLang, + 'target_lang' => $targetLang, + 'doc_type' => $docType, + 'chars' => mb_strlen($text, 'UTF-8'), + ]); + + $emit('progress', ['step' => 'translating', 'detail' => 'Translating…']); + + $sourceName = dbnToolsLanguageName($sourceLang); + $targetName = dbnToolsLanguageName($targetLang); + + $docTypeHint = $docType !== 'auto' + ? "The document is of type: {$docType}. Apply appropriate Norwegian family-law terminology for this context." + : ''; + + $systemPrompt = <<", + "annotations": [ + {"term": "", "explanation": ""} + ], + "disclaimer": "" +} + +If no terms require annotation, return an empty array for "annotations". +PROMPT; + + $azure = (new DbnAzureOpenAiGateway())->withDeployment('gpt-4o-mini'); + + $chars = mb_strlen($text, 'UTF-8'); + $maxTokens = min(8000, max(1500, (int)($chars * 1.4))); + + $response = $azure->chat([ + ['role' => 'system', 'content' => $systemPrompt], + ['role' => 'user', 'content' => $text], + ], [ + 'json' => true, + 'temperature' => 0.05, + 'max_tokens' => $maxTokens, + 'timeout' => 120, + ]); + + $rawContent = $response['choices'][0]['message']['content'] ?? ''; + $decoded = $azure->decodeJsonObject($rawContent); + + if ($decoded === null || empty($decoded['translated_text'])) { + throw new DbnToolsHttpException( + 'Translation model returned an unexpected response. Please try again.', + 502, 'bad_response' + ); + } + + $result = [ + 'ok' => true, + 'translated_text' => trim((string)($decoded['translated_text'] ?? '')), + 'annotations' => is_array($decoded['annotations'] ?? null) ? $decoded['annotations'] : [], + 'disclaimer' => (string)($decoded['disclaimer'] ?? ''), + 'source_lang' => $sourceLang, + 'target_lang' => $targetLang, + 'doc_type' => $docType, + 'model' => 'gpt-4o-mini', + 'latency_ms' => (int)round((microtime(true) - $startTime) * 1000), + ]; + + dbnToolsLogMetadata([ + 'tool' => 'translate', + 'language' => $language, + 'ok' => true, + 'latency_ms' => $result['latency_ms'], + 'source_lang' => $sourceLang, + 'target_lang' => $targetLang, + 'deployment' => 'gpt-4o-mini', + ]); + + $emit('final', ['result' => $result]); + +} catch (DbnToolsHttpException $e) { + $latency = (int)round((microtime(true) - $startTime) * 1000); + dbnToolsLogMetadata([ + 'tool' => 'translate', + 'language' => $language, + 'ok' => false, + 'latency_ms' => $latency, + 'error_code' => $e->errorCode, + ]); + $emit('error', ['code' => $e->errorCode, 'message' => $e->getMessage(), 'status' => $e->status]); +} catch (Throwable $e) { + error_log('translate fatal: ' . $e->getMessage()); + $latency = (int)round((microtime(true) - $startTime) * 1000); + dbnToolsLogMetadata([ + 'tool' => 'translate', + 'language' => $language, + 'ok' => false, + 'latency_ms' => $latency, + 'error_code' => 'internal_error', + ]); + $emit('error', ['code' => 'internal_error', 'message' => 'Translation could not complete this request.']); +} diff --git a/assets/css/tools.css b/assets/css/tools.css index 00553aa..3f2464b 100644 --- a/assets/css/tools.css +++ b/assets/css/tools.css @@ -9397,3 +9397,57 @@ body.lt-landing { } .la-synthesis h3 { margin-top: 0; color: #0e7490; } .la-synthesis h4 { margin: 0.9rem 0 0.4rem; font-size: 0.95rem; color: #155e75; } + +/* ── Legal Translation ─────────────────────────────────────────────────── */ +.lt-source-target-row { display: flex; gap: 1.5rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.75rem; } +.lt-source-target-row .control-label { min-width: 8rem; font-weight: 600; } +.lt-result { margin-bottom: 1.4rem; } +.lt-result__head { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; + margin-bottom: 0.75rem; +} +.lt-result__head h3 { margin: 0; } +.lt-lang-pair { + font-size: 0.8rem; + font-family: monospace; + background: var(--surface-2, #f1f5f9); + border: 1px solid var(--border, #cbd5e1); + border-radius: 4px; + padding: 0.2rem 0.5rem; + color: #475569; +} +.lt-translated-text { + white-space: pre-wrap; + font-size: 0.95rem; + line-height: 1.75; + border: 1px solid var(--border, #cbd5e1); + border-radius: 8px; + padding: 1.25rem 1.5rem; + background: var(--surface-2, #f8fafc); + color: #1e293b; + max-height: 60vh; + overflow-y: auto; +} +.lt-copy-btn { + margin-left: auto; + font-size: 0.83rem; + padding: 0.3rem 0.75rem; +} +.lt-annotations { margin-top: 1.5rem; } +.lt-annotations h4 { margin: 0 0 0.6rem; font-size: 0.9rem; color: #475569; } +.lt-annotation { + background: var(--surface-2, #f8fafc); + border-left: 3px solid var(--accent, #0f766e); + padding: 0.45rem 0.75rem; + margin-bottom: 0.45rem; + border-radius: 0 6px 6px 0; + font-size: 0.87rem; + line-height: 1.5; + color: #334155; +} +.lt-annotation strong { font-family: monospace; color: #0e7490; } +.lt-busy { padding: 2rem; text-align: center; color: #64748b; animation: pulse 1.5s ease-in-out infinite; } +@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } diff --git a/assets/js/translate.js b/assets/js/translate.js new file mode 100644 index 0000000..3d53ffa --- /dev/null +++ b/assets/js/translate.js @@ -0,0 +1,336 @@ +/** + * 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, '''); + } +}()); diff --git a/case-result.php b/case-result.php index a974a5a..d119f68 100644 --- a/case-result.php +++ b/case-result.php @@ -298,6 +298,33 @@ function crField(string $label, string $value): string { + + +
    +
    +

    Translation

    + + + +
    +
    +
    + + +
    +

    Legal term notes

    + +
    + + +
    + +
    + + +

    + +
    @@ -429,6 +456,7 @@ function crField(string $label, string $value): string { 'redact': '/redact.php', 'transcribe': '/transcribe.php', 'legal-analysis':'/legal-analysis.php', + 'translate': '/translate.php', }[tool] || '/dashboard.php'; window.location.href = path + '?rerun=' + ; }); diff --git a/includes/CaseResults.php b/includes/CaseResults.php index 0af400b..5434dfc 100644 --- a/includes/CaseResults.php +++ b/includes/CaseResults.php @@ -34,6 +34,7 @@ final class CaseResults 'redact', 'transcribe', 'legal-analysis', + 'translate', ]; /** True when the user is on a tier that gets saved results (Plus, Pro, or active Plus trial). */ @@ -240,6 +241,7 @@ final class CaseResults 'redact' => 'Anonymisering', 'transcribe' => 'Transkripsjon', 'legal-analysis' => 'Juridisk analyse', + 'translate' => 'Oversettelse', ][$tool] ?? ucfirst($tool); } @@ -258,6 +260,7 @@ final class CaseResults 'redact' => '🖊️', 'transcribe' => '🎙️', 'legal-analysis' => '⚖️🇳🇴', + 'translate' => '🌐', ][$tool] ?? '📄'; } @@ -276,6 +279,7 @@ final class CaseResults 'redact' => [$input['text'] ?? null], 'transcribe' => [$input['filename'] ?? null], 'legal-analysis' => [$input['doc_type'] ?? null, $input['text'] ?? null], + 'translate' => [$input['source_lang'] ?? null, $input['target_lang'] ?? null, $input['text'] ?? null], default => [$input['title'] ?? null, $input['query'] ?? null, $input['text'] ?? null], }; foreach ($candidates as $c) { diff --git a/includes/FreeTier.php b/includes/FreeTier.php index 14d40f9..efdb2fa 100644 --- a/includes/FreeTier.php +++ b/includes/FreeTier.php @@ -38,6 +38,7 @@ final class FreeTier 'transcribe' => 2, 'discrepancy' => 4, 'korrespond' => 3, + 'translate' => 2, ]; /** Monthly credit allowance per tier. */ diff --git a/includes/i18n.php b/includes/i18n.php index 2510d4a..17e914c 100644 --- a/includes/i18n.php +++ b/includes/i18n.php @@ -435,6 +435,29 @@ function dbnToolsTranslations(): array 'la_addon_button' => '⚖️🇳🇴 Run deep legal analysis on this text', 'la_addon_button_busy' => 'Running deep legal analysis…', 'la_addon_section' => 'Deep Legal Analysis', + + 'lt_source_label' => 'Source language', + 'lt_target_label' => 'Translate to', + 'lt_doc_type_label' => 'Document type', + 'lt_run_button' => 'Translate document', + 'lt_run_button_busy' => 'Translating…', + 'lt_input_label' => 'Paste text to translate', + 'lt_input_hint' => '(optional if uploading files)', + 'lt_input_placeholder' => 'Paste Norwegian legal text here…', + 'lt_translating_status' => 'Translating…', + 'lt_ready_title' => 'Ready to translate', + 'lt_ready_intro' => 'Upload a PDF, DOCX or TXT, or paste text below.', + 'lt_result_title' => 'Translation', + 'lt_annotations_title' => 'Legal term notes', + 'lt_copy_button' => 'Copy translation', + 'lt_copy_done' => 'Copied!', + 'lt_need_input' => 'Please paste text or upload a file.', + 'lt_error_prefix' => 'Error', + 'lt_server_returned' => 'Server returned', + 'lt_extracting_files' => 'Extracting text from {n} file(s)…', + 'lt_engine_hint' => 'Engine: Azure GPT-4o · Legal documents are processed in memory and never stored.', + 'lt_same_lang_error' => 'Source and target languages must be different.', + 'lt_disclaimer' => 'This is an AI-assisted translation. Always verify with a qualified legal interpreter for official use.', ], 'no' => [ 'meta_title' => 'Do Better Norge - juridiske AI-verktøy', @@ -802,6 +825,29 @@ function dbnToolsTranslations(): array 'la_addon_button' => '⚖️🇳🇴 Kjør dyp juridisk analyse på denne teksten', 'la_addon_button_busy' => 'Kjører dyp juridisk analyse…', 'la_addon_section' => 'Dyp juridisk analyse', + + 'lt_source_label' => 'Kildespråk', + 'lt_target_label' => 'Oversett til', + 'lt_doc_type_label' => 'Dokumenttype', + 'lt_run_button' => 'Oversett dokument', + 'lt_run_button_busy' => 'Oversetter…', + 'lt_input_label' => 'Lim inn tekst som skal oversettes', + 'lt_input_hint' => '(valgfritt ved filopplasting)', + 'lt_input_placeholder' => 'Lim inn norsk juridisk tekst her…', + 'lt_translating_status' => 'Oversetter…', + 'lt_ready_title' => 'Klar til å oversette', + 'lt_ready_intro' => 'Last opp PDF, DOCX eller TXT, eller lim inn tekst nedenfor.', + 'lt_result_title' => 'Oversettelse', + 'lt_annotations_title' => 'Juridiske termer', + 'lt_copy_button' => 'Kopier oversettelse', + 'lt_copy_done' => 'Kopiert!', + 'lt_need_input' => 'Lim inn tekst eller last opp en fil.', + 'lt_error_prefix' => 'Feil', + 'lt_server_returned' => 'Serveren svarte', + 'lt_extracting_files' => 'Henter tekst fra {n} fil(er)…', + 'lt_engine_hint' => 'Motor: Azure GPT-4o · Juridiske dokumenter behandles i minnet og lagres aldri.', + 'lt_same_lang_error' => 'Kilde- og målspråk må være forskjellige.', + 'lt_disclaimer' => 'Dette er en AI-assistert oversettelse. Verifiser alltid med en kvalifisert juridisk tolk til offisielt bruk.', ], 'uk' => [ 'meta_title' => 'Do Better Norge - юридичні AI інструменти', @@ -1169,6 +1215,29 @@ function dbnToolsTranslations(): array 'la_addon_button' => '⚖️🇳🇴 Запустити глибокий юридичний аналіз цього тексту', 'la_addon_button_busy' => 'Виконується глибокий юридичний аналіз…', 'la_addon_section' => 'Глибокий юридичний аналіз', + + 'lt_source_label' => 'Мова оригіналу', + 'lt_target_label' => 'Перекласти на', + 'lt_doc_type_label' => 'Тип документу', + 'lt_run_button' => 'Перекласти документ', + 'lt_run_button_busy' => 'Перекладаю…', + 'lt_input_label' => 'Вставте текст для перекладу', + 'lt_input_hint' => '(необов\'язково при завантаженні)', + 'lt_input_placeholder' => 'Вставте норвезький юридичний текст тут…', + 'lt_translating_status' => 'Перекладаю…', + 'lt_ready_title' => 'Готовий до перекладу', + 'lt_ready_intro' => 'Завантажте PDF, DOCX або TXT, або вставте текст нижче.', + 'lt_result_title' => 'Переклад', + 'lt_annotations_title' => 'Юридичні терміни', + 'lt_copy_button' => 'Копіювати переклад', + 'lt_copy_done' => 'Скопійовано!', + 'lt_need_input' => 'Будь ласка, вставте текст або завантажте файл.', + 'lt_error_prefix' => 'Помилка', + 'lt_server_returned' => 'Сервер повернув', + 'lt_extracting_files' => 'Витягую текст з {n} файл(ів)…', + 'lt_engine_hint' => 'Механізм: Azure GPT-4o · Юридичні документи обробляються в пам\'яті та не зберігаються.', + 'lt_same_lang_error' => 'Мова оригіналу та мова перекладу повинні бути різними.', + 'lt_disclaimer' => 'Це переклад за допомогою штучного інтелекту. Завжди перевіряйте з кваліфікованим юридичним перекладачем для офіційного використання.', ], 'pl' => [ 'meta_title' => 'Do Better Norge - prawne narzędzia AI', @@ -1536,6 +1605,29 @@ function dbnToolsTranslations(): array 'la_addon_button' => '⚖️🇳🇴 Uruchom głęboką analizę prawną tego tekstu', 'la_addon_button_busy' => 'Trwa głęboka analiza prawna…', 'la_addon_section' => 'Głęboka analiza prawna', + + 'lt_source_label' => 'Język źródłowy', + 'lt_target_label' => 'Przetłumacz na', + 'lt_doc_type_label' => 'Typ dokumentu', + 'lt_run_button' => 'Przetłumacz dokument', + 'lt_run_button_busy' => 'Tłumaczę…', + 'lt_input_label' => 'Wklej tekst do tłumaczenia', + 'lt_input_hint' => '(opcjonalne przy wgrywaniu pliku)', + 'lt_input_placeholder' => 'Wklej tu norweski tekst prawny…', + 'lt_translating_status' => 'Tłumaczę…', + 'lt_ready_title' => 'Gotowy do tłumaczenia', + 'lt_ready_intro' => 'Prześlij PDF, DOCX lub TXT, lub wklej tekst poniżej.', + 'lt_result_title' => 'Tłumaczenie', + 'lt_annotations_title' => 'Terminy prawne', + 'lt_copy_button' => 'Skopiuj tłumaczenie', + 'lt_copy_done' => 'Skopiowano!', + 'lt_need_input' => 'Wklej tekst lub prześlij plik.', + 'lt_error_prefix' => 'Błąd', + 'lt_server_returned' => 'Serwer zwrócił', + 'lt_extracting_files' => 'Wyodrębniam tekst z {n} plik(ów)…', + 'lt_engine_hint' => 'Silnik: Azure GPT-4o · Dokumenty prawne są przetwarzane w pamięci i nigdy nie zapisywane.', + 'lt_same_lang_error' => 'Języki źródłowy i docelowy muszą być różne.', + 'lt_disclaimer' => 'To jest tłumaczenie wspomagane AI. Zawsze weryfikuj z wykwalifikowanym tłumaczem prawnym do oficjalnego użytku.', ], ]; } @@ -1679,6 +1771,7 @@ function dbnToolsLaunchedTools(?string $language = null): array 'discrepancy' => ['Discrepancy Finder', 'Document comparison', 'Upload two versions of a Barnevernet document and find contradictions, deleted facts, and new allegations.', 'Cross-document AI'], 'corpus' => ['Corpus', 'Legal knowledge base', 'Inspect indexed sources, corpus health, legal categories, and retrieval behavior.', '~220 K passages'], 'citations' => ['Citations', 'Citation graph', 'Browse the legal citation graph — what a statute cites, what cites it, and what implements or amends it.', 'Graph topology'], + 'translate' => ['Translate', 'Legal translation', 'Translate Barnevernet letters and legal documents into your language with legal-terminology annotations.', 'Azure · GPT-4o'], ], 'no' => [ 'transcribe' => ['Transkriber', 'Lyd og møter', 'Gjør lyd eller video om til tekst med talerinndeling og juridisk ordforråd.', 'Whisper / GPU'], @@ -1693,6 +1786,7 @@ function dbnToolsLaunchedTools(?string $language = null): array 'discrepancy' => ['Avviksfinner', 'Dokumentsammenligning', 'Last opp to versjoner av et barneverndokument og finn motsigelser, slettede fakta og nye påstander.', 'Kryssdokument AI'], 'corpus' => ['Korpus', 'Juridisk kunnskapsbase', 'Se indekserte kilder, korpushelse, juridiske kategorier og søkeoppsett.', '~220 K utdrag'], 'citations' => ['Siteringer', 'Siteringsgraf', 'Utforsk siteringsgrafen — hva et dokument siterer, hva som siterer det, og hva som implementerer det.', 'Grafstruktur'], + 'translate' => ['Oversett', 'Juridisk oversettelse', 'Oversett Barnevernet-brev og juridiske dokumenter til ditt språk med juridisk terminologi.', 'Azure · GPT-4o'], ], 'uk' => [ 'transcribe' => ['Транскрипція', 'Аудіо та зустрічі', 'Перетворюйте аудіо або відео на текст із розділенням мовців і юридичною лексикою.', 'Whisper / GPU'], @@ -1707,6 +1801,7 @@ function dbnToolsLaunchedTools(?string $language = null): array 'discrepancy' => ['Пошук розбіжностей', 'Порівняння документів', 'Завантажте дві версії документа Barnevernet і знайдіть суперечності, видалені факти та нові твердження.', 'Міждокументний AI'], 'corpus' => ['Корпус', 'Юридична база знань', 'Переглядайте індексовані джерела, стан корпусу, категорії та поведінку пошуку.', '~220 тис. уривків'], 'citations' => ['Граф цитувань', 'Мережа посилань', 'Граф правових посилань — що цитує документ, хто цитує його, що його реалізує.', 'Граф-топологія'], + 'translate' => ['Перекласти', 'Юридичний переклад', 'Перекладайте листи Barnevernet та юридичні документи на свою мову з юридичними термінами.', 'Azure · GPT-4o'], ], 'pl' => [ 'transcribe' => ['Transkrypcja', 'Audio i spotkania', 'Zamień audio lub wideo na tekst z rozdzieleniem mówców i słownictwem prawnym.', 'Whisper / GPU'], @@ -1721,11 +1816,12 @@ function dbnToolsLaunchedTools(?string $language = null): array 'discrepancy' => ['Wyszukiwacz rozbieżności', 'Porównanie dokumentów', 'Prześlij dwie wersje dokumentu Barnevernet i znajdź sprzeczności, usunięte fakty i nowe zarzuty.', 'AI Między-dokumentowe'], 'corpus' => ['Korpus', 'Prawna baza wiedzy', 'Sprawdzaj indeksowane źródła, stan korpusu, kategorie prawne i działanie wyszukiwania.', '~220 tys. fragmentów'], 'citations' => ['Graf cytowań', 'Sieć cytowań', 'Przeglądaj sieć cytowań — co cytuje dokument, kto go cytuje i co go implementuje.', 'Topologia grafu'], + 'translate' => ['Tłumacz', 'Tłumaczenie prawne', 'Tłumacz listy Barnevernet i dokumenty prawne na swój język z adnotacjami terminologicznymi.', 'Azure · GPT-4o'], ], ]; $selected = $copy[$language] ?? $copy['en']; - $order = ['transcribe', 'timeline', 'redact', 'summarize', 'legal-analysis', 'korrespond', 'barnevernet', 'advocate', 'deep-research', 'discrepancy', 'corpus', 'citations']; + $order = ['transcribe', 'timeline', 'redact', 'summarize', 'legal-analysis', 'korrespond', 'barnevernet', 'advocate', 'deep-research', 'discrepancy', 'corpus', 'citations', 'translate']; $icons = [ 'transcribe' => 'TR', 'timeline' => 'TL', @@ -1739,6 +1835,7 @@ function dbnToolsLaunchedTools(?string $language = null): array 'discrepancy' => 'DC', 'corpus' => 'KB', 'citations' => 'CIT', + 'translate' => 'TX', ]; $out = []; foreach ($order as $slug) { diff --git a/translate.php b/translate.php new file mode 100644 index 0000000..7ec8fed --- /dev/null +++ b/translate.php @@ -0,0 +1,146 @@ + dbnToolsT($k, $ltLang); +// Default source = Norwegian; target = UI language, but if UI=no, default to English +$ltDefaultTarget = ($ltLang === 'no') ? 'en' : $ltLang; +$ltI18n = [ + 'runButton' => $ltT('lt_run_button'), + 'runButtonBusy' => $ltT('lt_run_button_busy'), + 'extractingFiles' => $ltT('lt_extracting_files'), + 'translatingStatus' => $ltT('lt_translating_status'), + 'needInput' => $ltT('lt_need_input'), + 'errorPrefix' => $ltT('lt_error_prefix'), + 'serverReturned' => $ltT('lt_server_returned'), + 'resultTitle' => $ltT('lt_result_title'), + 'annotationsTitle' => $ltT('lt_annotations_title'), + 'copyButton' => $ltT('lt_copy_button'), + 'copyDone' => $ltT('lt_copy_done'), + 'sameLangError' => $ltT('lt_same_lang_error'), + 'disclaimer' => $ltT('lt_disclaimer'), +]; +?> +
    + +
    + + + + +
    + +
    + + + + + +
    + +
    + + + + + +
    + +
    + + + + + + + + +
    + +

    + +
    + +
    + +
    + +
    + +
    + +

    Drop up to 5 files here, or

    +

    PDF, DOCX, TXT — text extracted in memory, never stored

    +
    + +
    + + + + + +
    + +
    +
    +

    +

    +
    +
    + + + + + + + + + + + + + +