fix(admin): i18n all hardcoded OCR strings, fix personName lookup, add empty state
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m17s
CI / OCR Service Tests (push) Successful in 57s
CI / Backend Unit Tests (push) Failing after 2m52s
CI / Unit & Component Tests (pull_request) Failing after 2m47s
CI / OCR Service Tests (pull_request) Successful in 43s
CI / Backend Unit Tests (pull_request) Failing after 2m48s

- Replace hardcoded EN strings in OcrHealthBar/OcrStatCards/OcrModelsTable with
  Paraglide message keys (de/en/es translations added)
- Add role=img + aria-label to OcrHealthBar status dot
- Add {:else} empty-state row in OcrModelsTable
- Fix personName derivation in [personId]/+page.svelte to use params.personId key
  instead of Object.values()[0] (fragile when multiple persons present)
- Update OcrModelsTable spec to assert empty-state row structure (locale-agnostic)
- Add missing availableSegBlocks test to OcrStatCards spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-18 08:59:49 +02:00
parent b879d28761
commit a00617194c
11 changed files with 100 additions and 21 deletions

View File

@@ -3,6 +3,7 @@ import type { PageData } from './$types';
import OcrHealthBar from './OcrHealthBar.svelte';
import OcrStatCards from './OcrStatCards.svelte';
import OcrModelsTable from './OcrModelsTable.svelte';
import * as m from '$lib/paraglide/messages.js';
let { data }: { data: PageData } = $props();
const { trainingInfo } = $derived(data);
@@ -11,7 +12,9 @@ const { trainingInfo } = $derived(data);
<div class="flex flex-col gap-6 p-6">
<!-- Page title + health bar -->
<div class="flex items-center justify-between">
<h1 class="font-sans text-lg font-bold tracking-widest text-brand-navy uppercase">OCR</h1>
<h1 class="font-sans text-lg font-bold tracking-widest text-brand-navy uppercase">
{m.admin_tab_ocr()}
</h1>
<OcrHealthBar ocrServiceAvailable={trainingInfo.ocrServiceAvailable ?? false} />
</div>
@@ -26,12 +29,14 @@ const { trainingInfo } = $derived(data);
<!-- Sender models -->
<div>
<div class="mb-3 flex items-center justify-between">
<h2 class="text-xs font-bold tracking-widest text-gray-400 uppercase">Sender Models</h2>
<h2 class="text-xs font-bold tracking-widest text-gray-400 uppercase">
{m.ocr_sender_models_heading()}
</h2>
<a
href="/admin/ocr/global"
class="text-xs font-medium text-brand-navy/60 transition-colors hover:text-brand-navy"
>
Global history
{m.ocr_global_history_link()}
</a>
</div>
<OcrModelsTable

View File

@@ -1,14 +1,17 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages.js';
let { ocrServiceAvailable }: { ocrServiceAvailable: boolean } = $props();
</script>
<div class="flex items-center gap-2">
<span
role="img"
aria-label={ocrServiceAvailable ? m.ocr_status_online() : m.ocr_status_offline()}
class="inline-block h-2.5 w-2.5 rounded-full {ocrServiceAvailable
? 'bg-green-500'
: 'bg-red-500'}"
></span>
<span class="text-sm font-medium {ocrServiceAvailable ? 'text-green-700' : 'text-red-700'}">
{ocrServiceAvailable ? 'Online' : 'Offline'}
{ocrServiceAvailable ? m.ocr_status_online() : m.ocr_status_offline()}
</span>
</div>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import type { components } from '$lib/generated/api';
import * as m from '$lib/paraglide/messages.js';
type SenderModel = components['schemas']['SenderModel'];
let {
@@ -17,23 +18,23 @@ let {
<tr>
<th
class="border-brand-sand border-b pb-3 text-left text-xs font-bold tracking-widest text-gray-400 uppercase"
>Person</th
>{m.ocr_table_person()}</th
>
<th
class="border-brand-sand border-b pb-3 text-left text-xs font-bold tracking-widest text-gray-400 uppercase"
>CER</th
>{m.ocr_table_cer()}</th
>
<th
class="border-brand-sand border-b pb-3 text-left text-xs font-bold tracking-widest text-gray-400 uppercase"
>Accuracy</th
>{m.ocr_table_accuracy()}</th
>
<th
class="border-brand-sand border-b pb-3 text-left text-xs font-bold tracking-widest text-gray-400 uppercase"
>Lines</th
>{m.ocr_table_lines()}</th
>
<th
class="border-brand-sand border-b pb-3 text-left text-xs font-bold tracking-widest text-gray-400 uppercase"
>Actions</th
>{m.ocr_table_actions()}</th
>
</tr>
</thead>
@@ -57,10 +58,16 @@ let {
<td class="border-brand-sand/50 border-b py-3">
<a
href="/admin/ocr/{model.personId}"
class="font-medium text-brand-navy hover:underline">Details</a
class="font-medium text-brand-navy hover:underline">{m.ocr_table_details()}</a
>
</td>
</tr>
{:else}
<tr>
<td colspan="5" class="py-6 text-center text-sm text-gray-400">
{m.ocr_no_models()}
</td>
</tr>
{/each}
</tbody>
</table>

View File

@@ -36,9 +36,10 @@ describe('OcrModelsTable', () => {
await expect.element(page.getByText(personId)).toBeInTheDocument();
});
it('shows empty state when no models', async () => {
it('shows empty state row when no models', async () => {
render(OcrModelsTable, { senderModels: [], personNames: {} });
const rows = document.querySelectorAll('tbody tr');
expect(rows.length).toBe(0);
expect(rows.length).toBe(1);
expect(rows[0].querySelector('td[colspan="5"]')).not.toBeNull();
});
});

View File

@@ -1,4 +1,6 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages.js';
interface Props {
availableBlocks?: number;
totalOcrBlocks?: number;
@@ -18,19 +20,25 @@ let {
<div class="border-brand-sand rounded-sm border bg-white p-6 shadow-sm">
<div class="text-3xl font-bold text-brand-navy">{availableBlocks}</div>
<div class="mt-2 text-xs font-bold tracking-widest text-gray-400 uppercase">
Training blocks
{m.ocr_stat_training_blocks()}
</div>
</div>
<div class="border-brand-sand rounded-sm border bg-white p-6 shadow-sm">
<div class="text-3xl font-bold text-brand-navy">{totalOcrBlocks}</div>
<div class="mt-2 text-xs font-bold tracking-widest text-gray-400 uppercase">Total blocks</div>
<div class="mt-2 text-xs font-bold tracking-widest text-gray-400 uppercase">
{m.ocr_stat_total_blocks()}
</div>
</div>
<div class="border-brand-sand rounded-sm border bg-white p-6 shadow-sm">
<div class="text-3xl font-bold text-brand-navy">{availableDocuments}</div>
<div class="mt-2 text-xs font-bold tracking-widest text-gray-400 uppercase">Documents</div>
<div class="mt-2 text-xs font-bold tracking-widest text-gray-400 uppercase">
{m.ocr_stat_documents()}
</div>
</div>
<div class="border-brand-sand rounded-sm border bg-white p-6 shadow-sm">
<div class="text-3xl font-bold text-brand-navy">{availableSegBlocks}</div>
<div class="mt-2 text-xs font-bold tracking-widest text-gray-400 uppercase">Seg. blocks</div>
<div class="mt-2 text-xs font-bold tracking-widest text-gray-400 uppercase">
{m.ocr_stat_seg_blocks()}
</div>
</div>
</div>

View File

@@ -27,4 +27,9 @@ describe('OcrStatCards', () => {
render(OcrStatCards, stats);
await expect.element(page.getByText('15')).toBeInTheDocument();
});
it('shows segmentation block count', async () => {
render(OcrStatCards, stats);
await expect.element(page.getByText('8')).toBeInTheDocument();
});
});

View File

@@ -1,9 +1,10 @@
<script lang="ts">
import type { PageData } from './$types';
import TrainingHistory from '$lib/components/TrainingHistory.svelte';
import * as m from '$lib/paraglide/messages.js';
let { data }: { data: PageData } = $props();
const personName = $derived(Object.values(data.history.personNames ?? {})[0] ?? 'Unknown');
let { data, params }: { data: PageData; params: { personId: string } } = $props();
const personName = $derived(data.history.personNames?.[params.personId] ?? 'Unknown');
</script>
<div class="flex flex-col gap-6 p-6">
@@ -20,7 +21,7 @@ const personName = $derived(Object.values(data.history.personNames ?? {})[0] ??
stroke-width="2"
><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" /></svg
>
OCR
{m.admin_tab_ocr()}
</a>
<h1 class="font-sans text-lg font-bold tracking-widest text-brand-navy uppercase">
{personName}

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import type { PageData } from './$types';
import TrainingHistory from '$lib/components/TrainingHistory.svelte';
import * as m from '$lib/paraglide/messages.js';
let { data }: { data: PageData } = $props();
</script>
@@ -19,10 +20,10 @@ let { data }: { data: PageData } = $props();
stroke-width="2"
><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" /></svg
>
OCR
{m.admin_tab_ocr()}
</a>
<h1 class="font-sans text-lg font-bold tracking-widest text-brand-navy uppercase">
Global History
{m.ocr_global_history_heading()}
</h1>
</div>