feat: Legal Tools v1 — multilingual landing, dashboard, SSO bridge

- Public landing page at / for unauthenticated users (EN/NO/UK/PL)
- Authenticated / shows Case Workbench dashboard with manifesto strip,
  stats, and launched-tool grid (Transcribe, Timeline, BVJ, Advocate,
  Deep Research, Corpus)
- Added includes/i18n.php with full 4-language translation layer
- Extended layout.php to Case Workbench shell with tool rail, lang switcher
- AI output language normalization extended to en/no/uk/pl in PHP agents
- SSO token validation in bootstrap.php / index.php (dobetternorge.no bridge)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 22:53:27 +02:00
parent ba6c197f1b
commit a3d46f9756
19 changed files with 1149 additions and 238 deletions
+3 -1
View File
@@ -3,7 +3,7 @@
'use strict';
const els = {};
let lang = 'en';
let lang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
let uploadFiles = [];
let lastResult = null;
let branchContext = null;
@@ -121,10 +121,12 @@
function bindLang() {
els.langButtons.forEach((b) => {
b.classList.toggle('is-active', b.dataset.lang === lang);
b.addEventListener('click', () => {
els.langButtons.forEach((x) => x.classList.remove('is-active'));
b.classList.add('is-active');
lang = b.dataset.lang || 'en';
localStorage.setItem('dbn-ui-lang', lang);
});
});
}
+3 -1
View File
@@ -3,7 +3,7 @@
'use strict';
const els = {};
let lang = 'en';
let lang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
let uploadFiles = [];
let lastResult = null;
let branchContext = null;
@@ -148,10 +148,12 @@
function bindLang() {
els.langButtons.forEach((b) => {
b.classList.toggle('is-active', b.dataset.lang === lang);
b.addEventListener('click', () => {
els.langButtons.forEach((x) => x.classList.remove('is-active'));
b.classList.add('is-active');
lang = b.dataset.lang || 'en';
localStorage.setItem('dbn-ui-lang', lang);
});
});
}
+3 -1
View File
@@ -3,7 +3,7 @@
'use strict';
const els = {};
let lang = 'en';
let lang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
let uploadFiles = [];
let lastResult = null;
let branchContext = null;
@@ -102,10 +102,12 @@
function bindLang() {
els.langButtons.forEach((b) => {
b.classList.toggle('is-active', b.dataset.lang === lang);
b.addEventListener('click', () => {
els.langButtons.forEach((x) => x.classList.remove('is-active'));
b.classList.add('is-active');
lang = b.dataset.lang || 'en';
localStorage.setItem('dbn-ui-lang', lang);
});
});
}
+36 -9
View File
@@ -391,7 +391,14 @@ const VOCAB_PRESETS = {
custom: '',
};
let uiLang = localStorage.getItem('dbn-ui-lang') || 'en';
let uiLang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
function syncOutputLanguage(lang) {
const normalized = ['en', 'no', 'uk', 'pl'].includes(lang) ? lang : 'en';
document.querySelectorAll('input[name="language"]').forEach((input) => {
if (input.value === normalized) input.checked = true;
});
}
const TRANSCRIBE_I18N = {
en: {
@@ -670,6 +677,7 @@ function currentUiT(key, ...args) {
function applyTranscribeI18n(lang) {
uiLang = lang;
localStorage.setItem('dbn-ui-lang', lang);
syncOutputLanguage(lang);
document.querySelectorAll('[data-i18n]').forEach((el) => {
const text = currentUiT(el.dataset.i18n);
if (text != null) el.textContent = text;
@@ -695,6 +703,7 @@ function currentRedactT(key) {
function applyRedactI18n(lang) {
uiLang = lang;
localStorage.setItem('dbn-ui-lang', lang);
syncOutputLanguage(lang);
document.querySelectorAll('[data-i18n]').forEach((el) => {
const text = currentRedactT(el.dataset.i18n);
if (text != null) el.textContent = text;
@@ -750,6 +759,7 @@ function currentTimelineT(key) {
function applyTimelineI18n(lang) {
uiLang = lang;
localStorage.setItem('dbn-ui-lang', lang);
syncOutputLanguage(lang);
document.querySelectorAll('[data-i18n]').forEach((el) => {
const text = currentTimelineT(el.dataset.i18n);
if (text != null) el.textContent = text;
@@ -952,7 +962,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
els.form?.addEventListener('submit', runTool);
els.passcodeForm?.addEventListener('submit', submitPasscode);
els.healthButton.addEventListener('click', checkHealth);
els.healthButton?.addEventListener('click', checkHealth);
setupUpload();
setupAliases();
setupAudio();
@@ -968,7 +978,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('uiLangSwitcher')) {
applyTranscribeI18n(uiLang);
}
els.results.addEventListener('click', (e) => {
els.results?.addEventListener('click', (e) => {
if (e.target.closest('#exportCsvBtn')) exportTimelineCSV(lastTimelineEvents);
if (e.target.closest('#dlTxt')) downloadTranscriptTxt();
if (e.target.closest('#dlSrt')) downloadTranscriptSrt();
@@ -978,7 +988,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (e.target.closest('#rdlDocx')) downloadRedactedDocx();
});
const activeTool = document.body.dataset.activeTool || state.activeTool;
setTool(activeTool);
if (els.form && tools[activeTool]) {
setTool(activeTool);
}
if (state.authenticated) {
checkHealth();
@@ -990,18 +1002,24 @@ document.addEventListener('DOMContentLoaded', () => {
function setTool(toolName) {
state.activeTool = toolName;
const tool = tools[toolName];
if (!tool || !els.toolKind || !els.input) return;
const serverRenderedShell = els.tabs.some((tab) => tab.tagName === 'A');
els.tabs.forEach((button) => {
const active = button.dataset.tool === toolName;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', String(active));
});
els.toolKind.textContent = tool.kind;
els.toolTitle.textContent = tool.title;
els.toolBadge.textContent = tool.badge;
els.inputLabel.textContent = tool.label;
if (!serverRenderedShell) {
els.toolKind.textContent = tool.kind;
els.toolTitle.textContent = tool.title;
els.toolBadge.textContent = tool.badge;
els.inputLabel.textContent = tool.label;
}
els.input.value = '';
els.input.placeholder = tool.placeholder;
if (!serverRenderedShell) {
els.input.placeholder = tool.placeholder;
}
els.languageControl.classList.toggle('is-hidden', !tool.usesLanguage);
els.redactionControl.classList.toggle('is-hidden', toolName !== 'redact');
els.uploadZone.classList.toggle('is-hidden', toolName !== 'redact' && toolName !== 'timeline');
@@ -1031,6 +1049,12 @@ async function submitPasscode(event) {
throw new Error(data.error?.message || 'Credentials were not accepted.');
}
state.authenticated = true;
if (!els.app) {
const params = new URLSearchParams(window.location.search);
const dest = params.get('return') || '/';
window.location.href = dest.startsWith('/') && !dest.startsWith('//') ? dest : '/';
return;
}
els.gate.classList.add('is-hidden');
els.app.classList.remove('is-hidden');
els.loginPassword.value = '';
@@ -1116,6 +1140,7 @@ function resetUpload() {
}
function setupUpload() {
if (!els.uploadZone || !els.uploadInput) return;
els.uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
els.uploadZone.classList.add('is-drag-over');
@@ -1217,6 +1242,7 @@ async function handleFiles(fileList) {
}
function setupAliases() {
if (!els.addAliasRow || !els.aliasRows) return;
els.addAliasRow.addEventListener('click', () => {
const row = document.createElement('div');
row.className = 'alias-row';
@@ -1287,6 +1313,7 @@ async function postJson(url, payload) {
function setBusy(isBusy) {
const button = document.querySelector('#runButton');
if (!button) return;
button.disabled = isBusy;
if (state.activeTool === 'transcribe') {
button.textContent = isBusy ? currentUiT('running') : currentUiT('run');