Korrespond: stop mixing UI languages — all chrome follows user UI lang
Drafts still come back in Norwegian + working language (that is intentional), but every piece of *chrome* now respects the user's UI lang consistently: - Pass 1 classify LLM now writes missing-fact questions in the user's language (not always Norwegian), fixing the case where an English-UI user got "Hva er saksnummeret?" in the clarify panel. - All PHP-emitted progress/status messages go through DbnKorrespondAgent::L() with en/no/pl/uk variants instead of hardcoded Norwegian. - JS introduces an I18N dictionary + t() helper covering status messages, button labels, column headers, flag labels, refine panel title/hint, jurisdiction radio labels, clarify panel title/hint/buttons, the empty-state "Ready" block, and Copy/Copied/Download .txt. - Static clarify and empty-state chrome use [data-i18n] attributes resolved at init and re-applied on every lang-switcher click. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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":"<question in {$userLangName}>"}],
|
||||
"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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user