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
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:
@@ -300,6 +300,22 @@
|
||||
"history_field_tags": "Schlagworte",
|
||||
"admin_tab_system": "System",
|
||||
"admin_tab_ocr": "OCR",
|
||||
"ocr_status_online": "Online",
|
||||
"ocr_status_offline": "Offline",
|
||||
"ocr_stat_training_blocks": "Trainingsblöcke",
|
||||
"ocr_stat_total_blocks": "Gesamt Blöcke",
|
||||
"ocr_stat_documents": "Dokumente",
|
||||
"ocr_stat_seg_blocks": "Seg.-Blöcke",
|
||||
"ocr_table_person": "Person",
|
||||
"ocr_table_cer": "ZFR",
|
||||
"ocr_table_accuracy": "Genauigkeit",
|
||||
"ocr_table_lines": "Zeilen",
|
||||
"ocr_table_actions": "Aktionen",
|
||||
"ocr_table_details": "Details",
|
||||
"ocr_no_models": "Noch keine Sender-Modelle trainiert.",
|
||||
"ocr_sender_models_heading": "Sender-Modelle",
|
||||
"ocr_global_history_link": "Globaler Verlauf →",
|
||||
"ocr_global_history_heading": "Globaler Verlauf",
|
||||
"admin_system_backfill_heading": "Verlaufsdaten auffüllen",
|
||||
"admin_system_backfill_description": "Erstellt einen initialen Verlaufseintrag für alle Dokumente, die noch keinen Verlauf haben (z.B. importierte Dokumente). Dadurch werden beim nächsten Bearbeiten nur die tatsächlich geänderten Felder hervorgehoben.",
|
||||
"admin_system_backfill_btn": "Jetzt auffüllen",
|
||||
|
||||
@@ -300,6 +300,22 @@
|
||||
"history_field_tags": "Tags",
|
||||
"admin_tab_system": "System",
|
||||
"admin_tab_ocr": "OCR",
|
||||
"ocr_status_online": "Online",
|
||||
"ocr_status_offline": "Offline",
|
||||
"ocr_stat_training_blocks": "Training blocks",
|
||||
"ocr_stat_total_blocks": "Total blocks",
|
||||
"ocr_stat_documents": "Documents",
|
||||
"ocr_stat_seg_blocks": "Seg. blocks",
|
||||
"ocr_table_person": "Person",
|
||||
"ocr_table_cer": "CER",
|
||||
"ocr_table_accuracy": "Accuracy",
|
||||
"ocr_table_lines": "Lines",
|
||||
"ocr_table_actions": "Actions",
|
||||
"ocr_table_details": "Details",
|
||||
"ocr_no_models": "No sender models trained yet.",
|
||||
"ocr_sender_models_heading": "Sender Models",
|
||||
"ocr_global_history_link": "Global history →",
|
||||
"ocr_global_history_heading": "Global History",
|
||||
"admin_system_backfill_heading": "Backfill history data",
|
||||
"admin_system_backfill_description": "Creates an initial history entry for all documents that do not have one yet (e.g. imported documents). This ensures that future edits only highlight actually changed fields.",
|
||||
"admin_system_backfill_btn": "Backfill now",
|
||||
|
||||
@@ -300,6 +300,22 @@
|
||||
"history_field_tags": "Etiquetas",
|
||||
"admin_tab_system": "Sistema",
|
||||
"admin_tab_ocr": "OCR",
|
||||
"ocr_status_online": "En línea",
|
||||
"ocr_status_offline": "Sin conexión",
|
||||
"ocr_stat_training_blocks": "Bloques de entrenamiento",
|
||||
"ocr_stat_total_blocks": "Total de bloques",
|
||||
"ocr_stat_documents": "Documentos",
|
||||
"ocr_stat_seg_blocks": "Bloques de seg.",
|
||||
"ocr_table_person": "Persona",
|
||||
"ocr_table_cer": "TCE",
|
||||
"ocr_table_accuracy": "Precisión",
|
||||
"ocr_table_lines": "Líneas",
|
||||
"ocr_table_actions": "Acciones",
|
||||
"ocr_table_details": "Detalles",
|
||||
"ocr_no_models": "Aún no hay modelos de remitente entrenados.",
|
||||
"ocr_sender_models_heading": "Modelos de remitente",
|
||||
"ocr_global_history_link": "Historial global →",
|
||||
"ocr_global_history_heading": "Historial global",
|
||||
"admin_system_backfill_heading": "Completar datos de historial",
|
||||
"admin_system_backfill_description": "Crea una entrada de historial inicial para todos los documentos que aún no tienen ninguna (p.ej. documentos importados). Así, en la próxima edición solo se resaltarán los campos realmente modificados.",
|
||||
"admin_system_backfill_btn": "Completar ahora",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user