Legal Analysis: full language follow-through (UI + LLM)

The tool now respects the chosen UI language end-to-end — even if the
source document is Norwegian, a user on EN/UK/PL gets the analysis in
their language. Norwegian statute references (barnevernsloven § 4-25,
EMK Art. 8) and case names (Strand Lobben mot Norge 37283/13) are kept
verbatim because they are proper nouns.

LLM (LegalAnalysisAgent.php):
- extractIssues: prompt asks for question + brief_context in user's
  language; statute refs preserved
- answerIssue: Norwegian core system prompt (keeps fine-tune precision)
  + language-coercion line for non-NO; localised context/source labels
- synthesise: overall_assessment, next_steps, disclaimer in user's
  language; explicit per-language disclaimer text
- runFullAnalysis empty-case fallback also localised
- what_to_check translated per language

UI:
- 40 new la_* translation keys in i18n.php × 4 languages (NO/EN/UK/PL)
- legal-analysis.php: 4-way lang switcher, dbnToolsT() for every label,
  emits window.DBN_LA_I18N for runtime JS strings
- legal-analysis.js: t() helper reads from window.DBN_LA_I18N
- layout_footer.php: emits window.DBN_CURRENT_LANG +
  window.DBN_ADDON_I18N so the legal-analysis add-on button works in
  the page's language no matter which tool it's invoked from
- tools.js add-on: reads from DBN_ADDON_I18N, passes DBN_CURRENT_LANG
  to /api/legal-analysis.php so server responds in same language

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 08:43:15 +02:00
parent 2509a596c1
commit 21c092e0d0
6 changed files with 397 additions and 89 deletions
+29 -22
View File
@@ -2579,6 +2579,14 @@ window.dbnShowSaveResultButton = showSaveResultButton;
// Injects a button that, when clicked, streams the two-pass legal-analysis flow
// (extract issues → ask dbn-legal-agent-v3 → synthesise) into a sub-section
// below the host container.
function _laT(key, vars) {
const map = window.DBN_ADDON_I18N || {};
let s = map[key] || key;
if (vars) {
Object.keys(vars).forEach((k) => { s = s.split('{' + k + '}').join(String(vars[k])); });
}
return s;
}
function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) {
if (!containerEl) containerEl = document.getElementById('results');
if (!containerEl || !text || text.length < 80) return;
@@ -2595,9 +2603,9 @@ function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) {
section.style.borderRadius = '8px';
section.style.background = '#f0fdfa';
section.innerHTML =
'<h3 style="margin-top:0;color:#0e7490;">⚖️🇳🇴 Deep Legal Analysis</h3>'
'<h3 style="margin-top:0;color:#0e7490;">⚖️🇳🇴 ' + escapeHtml(_laT('addonSection')) + '</h3>'
+ '<div class="la-pipeline" style="margin-bottom:1rem;">'
+ '<div class="la-step running"><strong>Pass 1</strong> — Extracting legal issues…</div>'
+ '<div class="la-step running"><strong>' + escapeHtml(_laT('pass1')) + '</strong> — ' + escapeHtml(_laT('pass1Extracting')) + '</div>'
+ '</div>'
+ '<ol id="laAddonIssues" class="la-issues" style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:1rem;"></ol>';
containerEl.parentNode.appendChild(section);
@@ -2608,7 +2616,7 @@ function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) {
const payload = {
text: text,
language: lang || 'no',
language: lang || window.DBN_CURRENT_LANG || 'no',
doc_type: 'auto',
source_tool: sourceTool || 'addon',
};
@@ -2638,7 +2646,7 @@ function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) {
}
}
}).catch(function (err) {
section.innerHTML += '<p style="color:#b91c1c;margin-top:0.5rem;">Error: ' + escapeHtml(err.message || String(err)) + '</p>';
section.innerHTML += '<p style="color:#b91c1c;margin-top:0.5rem;">' + escapeHtml(_laT('errorPrefix')) + ' ' + escapeHtml(err.message || String(err)) + '</p>';
});
}
@@ -2655,21 +2663,21 @@ function laAddonEvent(data, listEl, pipelineEl, cards) {
+ '<span class="la-severity la-severity-' + escapeHtml(sev) + '">' + escapeHtml(sev.toUpperCase()) + '</span></div>'
+ '<h4 class="la-issue__q">' + escapeHtml(issue.question || '') + '</h4>'
+ (issue.brief_context ? '<p class="la-issue__ctx"><em>' + escapeHtml(issue.brief_context) + '</em></p>' : '')
+ '<div class="la-issue__status">Waiting…</div>';
+ '<div class="la-issue__status">' + escapeHtml(_laT('waiting')) + '</div>';
listEl.appendChild(li);
cards[issue.id] = li;
});
pipelineEl.innerHTML =
'<div class="la-step done"><strong>Pass 1</strong> — Found ' + (data.issues || []).length + ' issue(s)</div>'
+ '<div class="la-step running"><strong>Pass 2</strong> — Asking dbn-legal-agent-v3…</div>';
'<div class="la-step done"><strong>' + escapeHtml(_laT('pass1')) + '</strong> — ' + escapeHtml(_laT('pass1Found', { n: (data.issues || []).length })) + '</div>'
+ '<div class="la-step running"><strong>' + escapeHtml(_laT('pass2')) + '</strong> — ' + escapeHtml(_laT('pass2Asking')) + '</div>';
} else if (data.event === 'progress') {
const card = cards[data.issue_id];
if (card) {
const status = card.querySelector('.la-issue__status');
if (status) {
status.textContent = data.step === 'issue_searching_corpus'
? 'Searching legal corpus'
: 'Asking dbn-legal-agent-v3…';
? _laT('searchingCorpus')
: _laT('askingFinetune');
}
card.classList.add('running');
}
@@ -2687,22 +2695,22 @@ function laAddonEvent(data, listEl, pipelineEl, cards) {
}
const statusEl = card.querySelector('.la-issue__status');
if (statusEl) statusEl.remove();
let html = '<div class="la-issue__answer"><h5>Svar</h5><p>' + escapeHtml(iss.answer || '').replace(/\n/g, '<br>') + '</p></div>';
if (iss.legal_basis) html += '<div class="la-issue__basis"><strong>Lovgrunnlag:</strong> ' + escapeHtml(iss.legal_basis) + '</div>';
let html = '<div class="la-issue__answer"><h5>' + escapeHtml(_laT('answerHeader')) + '</h5><p>' + escapeHtml(iss.answer || '').replace(/\n/g, '<br>') + '</p></div>';
if (iss.legal_basis) html += '<div class="la-issue__basis"><strong>' + escapeHtml(_laT('legalBasis')) + '</strong> ' + escapeHtml(iss.legal_basis) + '</div>';
if (iss.what_to_check) html += '<p class="la-issue__check"><em>' + escapeHtml(iss.what_to_check) + '</em></p>';
card.insertAdjacentHTML('beforeend', html);
} else if (data.event === 'final' && data.result) {
const res = data.result;
pipelineEl.innerHTML =
'<div class="la-step done"><strong>Pass 1</strong> — Issues extracted</div>'
+ '<div class="la-step done"><strong>Pass 2</strong> — Specialist answered ' + (res.issues || []).length + '</div>'
+ '<div class="la-step done"><strong>Pass 3</strong> — Synthesis complete</div>';
'<div class="la-step done"><strong>' + escapeHtml(_laT('pass1')) + '</strong> — ' + escapeHtml(_laT('pass1Found', { n: (res.issues || []).length })) + '</div>'
+ '<div class="la-step done"><strong>' + escapeHtml(_laT('pass2')) + '</strong> — ' + escapeHtml(_laT('pass2Answered', { n: (res.issues || []).length })) + '</div>'
+ '<div class="la-step done"><strong>' + escapeHtml(_laT('pass3')) + '</strong> — ' + escapeHtml(_laT('pass3Synthesis')) + '</div>';
if (res.overall_assessment) {
let syn = '<section class="la-synthesis" style="margin-bottom:1rem;padding:0.85rem 1.1rem;border-left:4px solid #0f766e;background:#ecfeff;border-radius:6px;">'
+ '<h3 style="margin-top:0;color:#0e7490;">Overall assessment</h3>'
+ '<h3 style="margin-top:0;color:#0e7490;">' + escapeHtml(_laT('overall')) + '</h3>'
+ '<p>' + escapeHtml(res.overall_assessment) + '</p>';
if (Array.isArray(res.next_steps) && res.next_steps.length) {
syn += '<h4 style="margin:0.7rem 0 0.3rem;color:#155e75;">Next steps</h4><ul>'
syn += '<h4 style="margin:0.7rem 0 0.3rem;color:#155e75;">' + escapeHtml(_laT('nextSteps')) + '</h4><ul>'
+ res.next_steps.map(function (s) { return '<li>' + escapeHtml(s) + '</li>'; }).join('')
+ '</ul>';
}
@@ -2717,25 +2725,24 @@ function laAddonEvent(data, listEl, pipelineEl, cards) {
const containerSection = pipelineEl.parentNode;
window.dbnShowSaveResultButton(
'legal-analysis',
{ text: '', language: 'no', doc_type: 'auto', source_tool: 'addon' }, // input was the prior tool's text
{ text: '', language: window.DBN_CURRENT_LANG || 'no', doc_type: 'auto', source_tool: 'addon' },
res,
{ model: res.model || 'dbn-legal-agent-v3', latency_ms: res.latency_ms || 0 },
containerSection
);
}
} else if (data.event === 'error') {
pipelineEl.innerHTML += '<div style="color:#b91c1c;margin-top:0.4rem;">Error: ' + escapeHtml(data.message || data.error || 'unknown') + '</div>';
pipelineEl.innerHTML += '<div style="color:#b91c1c;margin-top:0.4rem;">' + escapeHtml(_laT('errorPrefix')) + ' ' + escapeHtml(data.message || data.error || 'unknown') + '</div>';
}
}
function dbnInjectLegalAnalysisButton(text, lang, sourceTool, containerEl, options) {
if (!containerEl || !text || text.length < 80) return;
// Avoid duplicate buttons
if (containerEl.querySelector('.la-addon-btn')) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'la-addon-btn';
btn.textContent = (options && options.label) || '⚖️🇳🇴 Run deep legal analysis on this text';
btn.textContent = (options && options.label) || _laT('addonButton');
btn.style.cssText = 'display:block;margin:1rem auto 0;padding:0.6rem 1.2rem;font-size:0.92rem;'
+ 'background:#0f766e;color:#fff;border:none;border-radius:6px;cursor:pointer;'
+ 'font-weight:600;letter-spacing:0.02em;';
@@ -2743,9 +2750,9 @@ function dbnInjectLegalAnalysisButton(text, lang, sourceTool, containerEl, optio
btn.addEventListener('mouseleave', function () { btn.style.background = '#0f766e'; });
btn.addEventListener('click', function () {
btn.disabled = true;
btn.textContent = 'Running deep legal analysis…';
btn.textContent = _laT('addonButtonBusy');
btn.style.background = '#94a3b8';
dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl);
dbnRunLegalAnalysisAddon(text, lang || window.DBN_CURRENT_LANG || 'no', sourceTool, containerEl);
});
containerEl.appendChild(btn);
}