i18n all /dashboard/ corpus pages for en/no/uk/pl

- Add require_once bootstrap.php to all 6 dashboard page files so
  dbnToolsT() is available before layout_dashboard.php is included
- Add dash_upload_category_lbl key to no/uk/pl sections of i18n.php
  (was only in English); Kategori/Категорія/Kategoria
- Fix broken ternary on upload.php Category label — replace with
  dbnToolsT('dash_upload_category_lbl', $uiLang)
- layout_dashboard.php outputs window.DBN_I18N with all js_* keys
  so dashboard JS reads locale-aware strings from PHP translations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 20:10:57 +02:00
parent 90117fa9de
commit a9e64b65ce
8 changed files with 886 additions and 197 deletions
+24 -22
View File
@@ -1,21 +1,22 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/bootstrap.php';
$dashboardPage = 'chat';
$dashboardTitle = 'Spør korpuset';
$dashboardLead = 'Still juridiske spørsmål. Svar streames med kildehenvisninger til ditt eget korpus og delt Do Better Norge-pakke.';
$dashboardTitle = dbnToolsT('dash_title_chat', dbnToolsCurrentLanguage());
$dashboardLead = dbnToolsT('dash_lead_chat', dbnToolsCurrentLanguage());
require_once __DIR__ . '/../includes/layout_dashboard.php';
?>
<div class="disclaimer" role="note" style="margin-bottom:1rem;">
⚖ Juridisk informasjon og forberedelsesstøtte — ikke endelig juridisk rådgivning. Bekreft alltid med advokat.
<?= htmlspecialchars(dbnToolsT('dash_chat_disclaimer', $uiLang)) ?>
</div>
<section class="dash-card" style="display:flex; flex-direction:column; min-height:60vh;">
<div id="chatLog" class="chat-log" aria-live="polite">
<div class="chat-empty">Still ditt første spørsmål. Det kan handle om barnerett, barnevern, EMD, arbeidsrett — alt som finnes i ditt korpus.</div>
<div class="chat-empty" id="chatEmptyMsg"></div>
</div>
<form id="chatForm" class="chat-form" autocomplete="off">
<textarea id="chatInput" rows="2" required placeholder="f.eks. «Hva sier barnevernloven § 4-12 om plassering uten samtykke?»"></textarea>
<textarea id="chatInput" rows="2" required placeholder="<?= htmlspecialchars(dbnToolsT('dash_chat_placeholder', $uiLang)) ?>"></textarea>
<button type="submit" class="dash-btn dash-btn--primary" id="chatSendBtn">Send</button>
</form>
</section>
@@ -53,13 +54,19 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
<script>
(function () {
'use strict';
const api = window.DBN_DASHBOARD.apiBase;
const $log = document.getElementById('chatLog');
const I18N = window.DBN_I18N || {};
const api = window.DBN_DASHBOARD.apiBase;
const $log = document.getElementById('chatLog');
const $form = document.getElementById('chatForm');
const $input = document.getElementById('chatInput');
const $send = document.getElementById('chatSendBtn');
const $empty = document.getElementById('chatEmptyMsg');
const history = []; // [{role, content}]
if ($empty) $empty.textContent = I18N.ask_btn
? I18N.ask_btn.replace(/^💬\s*/, '')
: 'Ask your first question. It can be about child welfare, family law, ECHR — anything in your corpus.';
const history = [];
function safe(s) { return String(s ?? '').replace(/[&<>"]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c])); }
@@ -79,11 +86,11 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
const wrap = document.createElement('div');
wrap.className = 'chat-msg chat-msg--ai';
wrap.innerHTML =
'<div class="chat-bubble"><span class="chat-stream"></span><span class="chat-thinking">tenker…</span></div>'
'<div class="chat-bubble"><span class="chat-stream"></span><span class="chat-thinking">' + (I18N.chat_thinking || 'thinking…') + '</span></div>'
+ '<div class="chat-sources" hidden></div>'
+ '<div class="chat-actions" hidden>'
+ '<button class="dash-btn chat-save" type="button">💾 Lagre i korpus</button>'
+ '<button class="dash-btn chat-copy" type="button">📋 Kopier</button>'
+ '<button class="dash-btn chat-save" type="button">' + (I18N.chat_save || '💾 Save to corpus') + '</button>'
+ '<button class="dash-btn chat-copy" type="button">' + (I18N.chat_copy || '📋 Copy') + '</button>'
+ '</div>'
+ '<div class="chat-meta" hidden></div>';
$log.appendChild(wrap);
@@ -94,7 +101,7 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
async function ask(question) {
appendUser(question);
history.push({ role: 'user', content: question });
const aiNode = appendAi();
const aiNode = appendAi();
const stream = aiNode.querySelector('.chat-stream');
const thinking = aiNode.querySelector('.chat-thinking');
const sources = aiNode.querySelector('.chat-sources');
@@ -123,7 +130,6 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
if (done) break;
buffer += dec.decode(value, { stream: true });
// Parse SSE frames: blocks separated by "\n\n"
let idx;
while ((idx = buffer.indexOf('\n\n')) !== -1) {
const frame = buffer.slice(0, idx);
@@ -146,15 +152,13 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
} else if (evName === 'done') {
history.push({ role: 'assistant', content: answer });
renderSources(sources, payload.sources || []);
const chunksTmpl = (I18N.chat_passages_meta || '{n} passages').replace('{n}', payload.chunks_used || 0);
meta.hidden = false;
meta.textContent =
(payload.chunks_used || 0) + ' passasjer · '
+ (payload.model || 'auto') + ' · '
+ (payload.response_time_ms || 0) + ' ms';
meta.textContent = chunksTmpl + ' · ' + (payload.model || 'auto') + ' · ' + (payload.response_time_ms || 0) + ' ms';
actions.hidden = false;
wireActions(aiNode, question, answer);
} else if (evName === 'fail') {
thinking.textContent = '❌ ' + (payload.message || 'Feil');
thinking.textContent = '❌ ' + (payload.message || 'Error');
thinking.style.color = 'var(--dbn-red)';
}
}
@@ -191,22 +195,20 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
navigator.clipboard.writeText(answer).then(() => {
const btn = node.querySelector('.chat-copy');
const orig = btn.textContent;
btn.textContent = '✓ Kopiert';
btn.textContent = I18N.chat_copied || '✓ Copied';
setTimeout(() => btn.textContent = orig, 1400);
});
});
node.querySelector('.chat-save').addEventListener('click', () => {
// Re-uses existing corpus-save.js dialog (loaded by layout footer)
const dialog = document.getElementById('save-corpus-dialog');
const titleField = document.getElementById('save-corpus-title');
const tagsField = document.getElementById('save-corpus-tags');
if (!dialog || !titleField) {
alert('Lagre-dialog ikke tilgjengelig.');
alert(I18N.chat_save_unavail || 'Save dialog unavailable.');
return;
}
titleField.value = question.slice(0, 80);
tagsField.value = 'chat,answer';
// Hand-off contract used by corpus-save.js: data-pending-content
dialog.dataset.pendingContent = 'Q: ' + question + '\n\nA: ' + answer;
dialog.dataset.pendingTool = 'dashboard-chat';
dialog.showModal();