@@ -475,29 +559,29 @@
${cited.length ? `
- Cited law (${cited.length}) — each reference traces to a real corpus passage
+ ${esc(t('cited_law_n', { n: cited.length }))} — ${esc(t('cited_law_hint'))}
@@ -605,7 +689,7 @@
btn.addEventListener('click', () => {
const target = btn.dataset.rcopy === 'no' ? draftNo : draftUser;
navigator.clipboard?.writeText(target).then(
- () => { btn.textContent = 'Copied ✓'; setTimeout(() => btn.textContent = 'Copy', 1500); },
+ () => { btn.textContent = t('copied'); setTimeout(() => btn.textContent = t('copy'), 1500); },
() => { btn.textContent = 'Failed'; }
);
});
diff --git a/includes/KorrespondAgent.php b/includes/KorrespondAgent.php
index 08ac036..73f0223 100644
--- a/includes/KorrespondAgent.php
+++ b/includes/KorrespondAgent.php
@@ -63,6 +63,85 @@ final class DbnKorrespondAgent
$this->azure = $azure ?: new DbnAzureOpenAiGateway();
}
+ /** Localized chrome/progress strings, keyed by user UI language. */
+ public static function L(string $key, string $lang, array $vars = []): string
+ {
+ $strings = [
+ 'analyzing' => [
+ 'en' => 'Analyzing the situation…',
+ 'no' => 'Analyserer situasjonen…',
+ 'pl' => 'Analiza sytuacji…',
+ 'uk' => 'Аналізую ситуацію…',
+ ],
+ 'fetching_law' => [
+ 'en' => 'Fetching relevant legal sources…',
+ 'no' => 'Henter relevante lovkilder…',
+ 'pl' => 'Pobieranie odpowiednich źródeł prawnych…',
+ 'uk' => 'Завантажую відповідні юридичні джерела…',
+ ],
+ 'drafting_no' => [
+ 'en' => 'Writing draft in Norwegian (bokmål)…',
+ 'no' => 'Skriver utkast på bokmål…',
+ 'pl' => 'Pisanie projektu po norwesku (bokmål)…',
+ 'uk' => 'Пишу чернетку норвезькою (bokmål)…',
+ ],
+ 'quality_check' => [
+ 'en' => 'Quality-checking the draft…',
+ 'no' => 'Kvalitetskontroll av utkastet…',
+ 'pl' => 'Sprawdzanie jakości projektu…',
+ 'uk' => 'Перевірка якості чернетки…',
+ ],
+ 'translating_to' => [
+ 'en' => 'Translating to {lang}…',
+ 'no' => 'Oversetter til {lang}…',
+ 'pl' => 'Tłumaczenie na {lang}…',
+ 'uk' => 'Переклад на {lang}…',
+ ],
+ 'fetching_for_jur' => [
+ 'en' => 'Fetching authorities for {jur}…',
+ 'no' => 'Henter rettskilder for {jur}…',
+ 'pl' => 'Pobieranie autorytetów dla {jur}…',
+ 'uk' => 'Завантажую джерела для {jur}…',
+ ],
+ 'rewriting_formal' => [
+ 'en' => 'Rewriting with formal citations…',
+ 'no' => 'Skriver om med formelle henvisninger…',
+ 'pl' => 'Przepisywanie z formalnymi cytatami…',
+ 'uk' => 'Переписую з формальними цитатами…',
+ ],
+ 'check_and_authorities' => [
+ 'en' => 'Quality-check and Legal authorities block…',
+ 'no' => 'Kvalitetskontroll og Rettskilder…',
+ 'pl' => 'Kontrola jakości i blok źródeł prawnych…',
+ 'uk' => 'Перевірка якості і блок джерел права…',
+ ],
+ 'file_read' => [
+ 'en' => 'Read {name} ({chars} chars)',
+ 'no' => 'Lest {name} ({chars} tegn)',
+ 'pl' => 'Przeczytano {name} ({chars} znaków)',
+ 'uk' => 'Прочитано {name} ({chars} символів)',
+ ],
+ ];
+ $lang = in_array($lang, ['en', 'no', 'pl', 'uk'], true) ? $lang : 'en';
+ $tmpl = $strings[$key][$lang] ?? $strings[$key]['en'] ?? $key;
+ foreach ($vars as $k => $v) {
+ $tmpl = str_replace('{' . $k . '}', (string)$v, $tmpl);
+ }
+ return $tmpl;
+ }
+
+ /** Localized jurisdiction labels for chrome (status messages). */
+ public static function jurisdictionChromeLabel(string $jurisdiction, string $lang): string
+ {
+ $map = [
+ 'norwegian' => ['en' => 'Norwegian law', 'no' => 'norsk rett', 'pl' => 'prawo norweskie', 'uk' => 'норвезьке право'],
+ 'echr' => ['en' => 'ECHR + HUDOC', 'no' => 'EMK og EMD-praksis', 'pl' => 'EKPC + HUDOC', 'uk' => 'ЄКПЛ + HUDOC'],
+ 'both' => ['en' => 'NO + ECHR', 'no' => 'norsk rett + EMK/EMD', 'pl' => 'NO + EKPC', 'uk' => 'NO + ЄКПЛ'],
+ ];
+ $lang = in_array($lang, ['en', 'no', 'pl', 'uk'], true) ? $lang : 'en';
+ return $map[$jurisdiction][$lang] ?? $map['norwegian']['en'];
+ }
+
/**
* Pass 1 — extract structured facts and identify missing info.
*
@@ -82,6 +161,8 @@ final class DbnKorrespondAgent
$body = $intake['recipient_body'] ?? 'other';
$mode = $intake['mode'] ?? 'initiate';
$bodyLabel = self::BODY_LABELS[$body] ?? 'mottaker';
+ $userLang = dbnToolsNormalizeUiLanguage($intake['language'] ?? 'en');
+ $userLangName = dbnToolsLanguageName($userLang); // e.g. "English", "Norwegian", "Polish", "Ukrainian"
$context = $this->buildContextBlob($intake);
$modeLabel = $mode === 'reply' ? 'svar på et brev/vedtak' : 'innledning av en sak';
@@ -98,8 +179,8 @@ Return JSON only:
"deadlines": ["YYYY-MM-DD", "or relative deadline as plain text"],
"applicable_acts": ["forvaltningsloven", "barnevernsloven", "NAV-loven", "opplæringslova", "barnehageloven", "EMK"],
"jurisdiction": "kommune/fylke if known, else null",
- "missing_facts": [{"key":"deadline","question":"Norwegian bokmål question to user"}],
- "suggested_goal": "One-line concrete goal for this letter, in Norwegian"
+ "missing_facts": [{"key":"deadline","question":""}],
+ "suggested_goal": "One-line concrete goal for this letter, in Norwegian bokmål"
}
Rules:
@@ -108,7 +189,8 @@ Rules:
- missing_facts: include up to 4 items the drafter genuinely needs (date of decision, deadline, case
number, specific decision being appealed, etc.). Leave EMPTY if intake is complete.
- For "reply" mode if no case reference is supplied, missing_facts SHOULD include one for it.
-- Write missing-fact questions in Norwegian bokmål, short and clear.
+- IMPORTANT: write each missing-fact "question" field in **{$userLangName}**, short and clear.
+ Do NOT write the question in Norwegian if the user's language is not Norwegian.
Intake:
{$context}
@@ -156,7 +238,7 @@ PROMPT;
$bodyLabel = self::BODY_LABELS[$body] ?? 'mottaker';
// ── Retrieve law ────────────────────────────────────────────────────────
- if ($emit) { $emit('progress', ['detail' => 'Henter relevante lovkilder…']); }
+ if ($emit) { $emit('progress', ['detail' => self::L('fetching_law', $userLang)]); }
$retrieval = $this->retrieveLaw($body, $classify['applicable_acts'] ?? []);
if ($emit) {
$emit('retrieval', [
@@ -166,19 +248,19 @@ PROMPT;
}
// ── Draft in Norwegian bokmål ───────────────────────────────────────────
- if ($emit) { $emit('progress', ['detail' => 'Skriver utkast på bokmål…']); }
+ if ($emit) { $emit('progress', ['detail' => self::L('drafting_no', $userLang)]); }
$draftNo = $this->draftNorwegian(
$intake, $classify, $retrieval['sources'], $bodyLabel, $outputType, $tone, $goal
);
// ── Self-check: verify citations, deadline, goal, tone ──────────────────
- if ($emit) { $emit('progress', ['detail' => 'Kvalitetskontroll av utkastet…']); }
+ if ($emit) { $emit('progress', ['detail' => self::L('quality_check', $userLang)]); }
$checked = $this->selfCheck($draftNo, $retrieval['sources'], $classify, $goal, $tone);
// ── Translate to user language (if not Norwegian) ───────────────────────
$draftUser = $checked['draft'];
if ($userLang !== 'no') {
- if ($emit) { $emit('progress', ['detail' => 'Oversetter til ' . dbnToolsLanguageName($userLang) . '…']); }
+ if ($emit) { $emit('progress', ['detail' => self::L('translating_to', $userLang, ['lang' => dbnToolsLanguageName($userLang)])]); }
$draftUser = $this->translate($checked['draft'], $userLang, $outputType);
}
@@ -656,7 +738,7 @@ EOT,
$bodyLabel = self::BODY_LABELS[$body] ?? 'mottaker';
$jurisdiction = in_array($jurisdiction, ['norwegian', 'echr', 'both'], true) ? $jurisdiction : 'norwegian';
- if ($emit) { $emit('progress', ['detail' => 'Henter rettskilder for ' . $this->jurisdictionLabelNorsk($jurisdiction) . '…']); }
+ if ($emit) { $emit('progress', ['detail' => self::L('fetching_for_jur', $userLang, ['jur' => self::jurisdictionChromeLabel($jurisdiction, $userLang)])]); }
$retrieval = $this->retrieveLawForJurisdiction($jurisdiction, $body, $classify);
if ($emit) {
$emit('retrieval', [
@@ -666,17 +748,17 @@ EOT,
]);
}
- if ($emit) { $emit('progress', ['detail' => 'Skriver om med formelle henvisninger…']); }
+ if ($emit) { $emit('progress', ['detail' => self::L('rewriting_formal', $userLang)]); }
$refinedNo = $this->rewriteWithFormalCites(
$originalDraftNo, $retrieval['sources'], $bodyLabel, $outputType, $tone, $goal, $jurisdiction
);
- if ($emit) { $emit('progress', ['detail' => 'Kvalitetskontroll og Rettskilder…']); }
+ if ($emit) { $emit('progress', ['detail' => self::L('check_and_authorities', $userLang)]); }
$checked = $this->selfCheck($refinedNo, $retrieval['sources'], $classify, $goal, $tone);
$draftUser = $checked['draft'];
if ($userLang !== 'no') {
- if ($emit) { $emit('progress', ['detail' => 'Oversetter til ' . dbnToolsLanguageName($userLang) . '…']); }
+ if ($emit) { $emit('progress', ['detail' => self::L('translating_to', $userLang, ['lang' => dbnToolsLanguageName($userLang)])]); }
$draftUser = $this->translate($checked['draft'], $userLang, $outputType);
}
diff --git a/korrespond.php b/korrespond.php
index daf4db5..902e96d 100644
--- a/korrespond.php
+++ b/korrespond.php
@@ -118,19 +118,19 @@ require_once __DIR__ . '/includes/layout.php';
-
Before we draft, clarify:
-
Answer what you can, then click Continue draft. Or click Draft anyway to proceed with what we have.
+
Before we draft, clarify:
+
Answer what you can, then click Continue draft. Or click Draft anyway to proceed with what we have.
-
-
+
+
-
Ready
-
Pick a recipient body, describe the situation, choose an output type and tone, then run. Drafts always come back in Norwegian bokmål + your working language, side-by-side, with verified law citations.
+
Ready
+
Pick a recipient body, describe the situation, choose an output type and tone, then run. Drafts always come back in Norwegian bokmål + your working language, side-by-side, with verified law citations.