Compare commits
6 Commits
main
...
feat/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6832300a4b | ||
|
|
9c5267e1f0 | ||
|
|
4979ae1867 | ||
|
|
29ef82f7b4 | ||
|
|
f458c11a0d | ||
|
|
e615ba1bbf |
@@ -58,3 +58,20 @@ test.describe('Language selector', () => {
|
||||
await expect(deBtn).toHaveClass(/font-bold/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Mobile nav — i18n', () => {
|
||||
test('hamburger button aria-label translates to EN on narrow viewport', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 375, height: 812 },
|
||||
storageState: 'e2e/.auth/user.json'
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto('/');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
await page.getByRole('banner').getByRole('button', { name: 'EN', exact: true }).click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Open menu' })).toBeVisible();
|
||||
|
||||
await context.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"nav_conversations": "Briefwechsel",
|
||||
"nav_admin": "Admin",
|
||||
"nav_logout": "Abmelden",
|
||||
"layout_menu_open": "Menü öffnen",
|
||||
"layout_menu_close": "Menü schließen",
|
||||
"theme_toggle_to_light": "Zu hellem Design wechseln",
|
||||
"theme_toggle_to_dark": "Zu dunklem Design wechseln",
|
||||
"btn_save": "Speichern",
|
||||
@@ -394,6 +396,10 @@
|
||||
"doc_panel_discussion_annotation_tab": "Annotation · Seite {page}",
|
||||
"pdf_annotations_show": "Annotierungen anzeigen",
|
||||
"pdf_annotations_hide": "Annotierungen verbergen",
|
||||
"viewer_previous_page": "Zurück",
|
||||
"viewer_next_page": "Weiter",
|
||||
"viewer_zoom_out": "Verkleinern",
|
||||
"viewer_zoom_in": "Vergrößern",
|
||||
"upload_action": "Hochladen",
|
||||
"upload_drop_hint": "Einzeln oder mehrere Dateien auf einmal hochladen",
|
||||
"upload_accepted_types": "PDF, JPEG, PNG, TIFF",
|
||||
@@ -655,6 +661,7 @@
|
||||
"transcription_block_segmentation_only": "Nur Segmentierung",
|
||||
"training_chip_kurrent": "Kurrent-Erkennung",
|
||||
"training_chip_segmentation": "Segmentierung",
|
||||
"transcribe_mark_for_training": "Für Training vormerken",
|
||||
"training_col_type": "Typ",
|
||||
"training_type_base": "Basis",
|
||||
"training_type_personalized": "Personalisiert",
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"nav_conversations": "Letters",
|
||||
"nav_admin": "Admin",
|
||||
"nav_logout": "Sign out",
|
||||
"layout_menu_open": "Open menu",
|
||||
"layout_menu_close": "Close menu",
|
||||
"theme_toggle_to_light": "Switch to light mode",
|
||||
"theme_toggle_to_dark": "Switch to dark mode",
|
||||
"btn_save": "Save",
|
||||
@@ -394,6 +396,10 @@
|
||||
"doc_panel_discussion_annotation_tab": "Annotation · Page {page}",
|
||||
"pdf_annotations_show": "Show annotations",
|
||||
"pdf_annotations_hide": "Hide annotations",
|
||||
"viewer_previous_page": "Previous page",
|
||||
"viewer_next_page": "Next page",
|
||||
"viewer_zoom_out": "Zoom out",
|
||||
"viewer_zoom_in": "Zoom in",
|
||||
"upload_action": "Upload",
|
||||
"upload_drop_hint": "Drop one or multiple files at once",
|
||||
"upload_accepted_types": "PDF, JPEG, PNG, TIFF",
|
||||
@@ -655,6 +661,7 @@
|
||||
"transcription_block_segmentation_only": "Segmentation only",
|
||||
"training_chip_kurrent": "Kurrent recognition",
|
||||
"training_chip_segmentation": "Segmentation",
|
||||
"transcribe_mark_for_training": "Mark for OCR training",
|
||||
"training_col_type": "Type",
|
||||
"training_type_base": "Base",
|
||||
"training_type_personalized": "Personalized",
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"nav_conversations": "Cartas",
|
||||
"nav_admin": "Admin",
|
||||
"nav_logout": "Cerrar sesión",
|
||||
"layout_menu_open": "Abrir menú",
|
||||
"layout_menu_close": "Cerrar menú",
|
||||
"theme_toggle_to_light": "Cambiar a modo claro",
|
||||
"theme_toggle_to_dark": "Cambiar a modo oscuro",
|
||||
"btn_save": "Guardar",
|
||||
@@ -394,6 +396,10 @@
|
||||
"doc_panel_discussion_annotation_tab": "Anotación · Página {page}",
|
||||
"pdf_annotations_show": "Mostrar anotaciones",
|
||||
"pdf_annotations_hide": "Ocultar anotaciones",
|
||||
"viewer_previous_page": "Página anterior",
|
||||
"viewer_next_page": "Página siguiente",
|
||||
"viewer_zoom_out": "Reducir",
|
||||
"viewer_zoom_in": "Ampliar",
|
||||
"upload_action": "Subir",
|
||||
"upload_drop_hint": "Uno o varios archivos a la vez",
|
||||
"upload_accepted_types": "PDF, JPEG, PNG, TIFF",
|
||||
@@ -655,6 +661,7 @@
|
||||
"transcription_block_segmentation_only": "Solo segmentación",
|
||||
"training_chip_kurrent": "Reconocimiento Kurrent",
|
||||
"training_chip_segmentation": "Segmentación",
|
||||
"transcribe_mark_for_training": "Marcar para entrenamiento de OCR",
|
||||
"training_col_type": "Tipo",
|
||||
"training_type_base": "Base",
|
||||
"training_type_personalized": "Personalizado",
|
||||
|
||||
@@ -303,7 +303,9 @@ async function handleLabelToggle(label: string) {
|
||||
|
||||
{#if canWrite && hasBlocks}
|
||||
<div class="border-t border-line px-4 py-3">
|
||||
<p class="mb-2 font-sans text-xs font-medium text-ink-2">Für Training vormerken</p>
|
||||
<p class="mb-2 font-sans text-xs font-medium text-ink-2">
|
||||
{m.transcribe_mark_for_training()}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each [{ label: 'KURRENT_RECOGNITION', display: m.training_chip_kurrent() }, { label: 'KURRENT_SEGMENTATION', display: m.training_chip_segmentation() }] as chip (chip.label)}
|
||||
<button
|
||||
|
||||
@@ -34,7 +34,7 @@ let {
|
||||
<button
|
||||
onclick={onPrev}
|
||||
disabled={currentPage <= 1}
|
||||
aria-label="Zurück"
|
||||
aria-label={m.viewer_previous_page()}
|
||||
class="min-h-[44px] min-w-[44px] rounded p-2 text-ink-3 transition hover:bg-surface/10 focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-1 disabled:opacity-40"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -51,7 +51,7 @@ let {
|
||||
<button
|
||||
onclick={onNext}
|
||||
disabled={!isLoaded || currentPage >= totalPages}
|
||||
aria-label="Weiter"
|
||||
aria-label={m.viewer_next_page()}
|
||||
class="min-h-[44px] min-w-[44px] rounded p-2 text-ink-3 transition hover:bg-surface/10 focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-1 disabled:opacity-40"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -64,7 +64,7 @@ let {
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
onclick={onZoomOut}
|
||||
aria-label="Verkleinern"
|
||||
aria-label={m.viewer_zoom_out()}
|
||||
class="min-h-[44px] min-w-[44px] rounded p-2 text-ink-3 transition hover:bg-surface/10 focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-1"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -74,7 +74,7 @@ let {
|
||||
</button>
|
||||
<button
|
||||
onclick={onZoomIn}
|
||||
aria-label="Vergrößern"
|
||||
aria-label={m.viewer_zoom_in()}
|
||||
class="min-h-[44px] min-w-[44px] rounded p-2 text-ink-3 transition hover:bg-surface/10 focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-1"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { vi, describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import PdfControls from './PdfControls.svelte';
|
||||
|
||||
afterEach(cleanup);
|
||||
@@ -23,28 +24,28 @@ describe('PdfControls — annotation toggle visibility', () => {
|
||||
it('renders annotation toggle when annotationCount is greater than zero', async () => {
|
||||
render(PdfControls, { ...defaultProps, annotationCount: 3 });
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: /annotierungen anzeigen/i }))
|
||||
.element(page.getByRole('button', { name: m.pdf_annotations_show() }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render annotation toggle when annotationCount is zero', async () => {
|
||||
render(PdfControls, { ...defaultProps, annotationCount: 0 });
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: /annotierungen/i }))
|
||||
.element(page.getByRole('button', { name: m.pdf_annotations_show() }))
|
||||
.not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PdfControls — annotation toggle label', () => {
|
||||
it('shows "Annotierungen anzeigen" label when annotations are hidden', async () => {
|
||||
it('shows show-annotations label when annotations are hidden', async () => {
|
||||
render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: false });
|
||||
const btn = page.getByRole('button', { name: /annotierungen anzeigen/i });
|
||||
const btn = page.getByRole('button', { name: m.pdf_annotations_show() });
|
||||
await expect.element(btn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Annotierungen verbergen" label when annotations are visible', async () => {
|
||||
it('shows hide-annotations label when annotations are visible', async () => {
|
||||
render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: true });
|
||||
const btn = page.getByRole('button', { name: /annotierungen verbergen/i });
|
||||
const btn = page.getByRole('button', { name: m.pdf_annotations_hide() });
|
||||
await expect.element(btn).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -58,7 +59,9 @@ describe('PdfControls — annotation toggle contrast (WCAG 2.1 AA)', () => {
|
||||
});
|
||||
const allButtons = container.querySelectorAll('button');
|
||||
const annotationBtn = Array.from(allButtons).find((b) =>
|
||||
b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen')
|
||||
[m.pdf_annotations_show(), m.pdf_annotations_hide()].includes(
|
||||
b.getAttribute('aria-label') ?? ''
|
||||
)
|
||||
);
|
||||
expect(annotationBtn).not.toBeNull();
|
||||
expect(annotationBtn!.className).toContain('text-primary');
|
||||
@@ -75,7 +78,9 @@ describe('PdfControls — focus rings (WCAG 2.1 §2.4.7)', () => {
|
||||
});
|
||||
const allButtons = container.querySelectorAll('button');
|
||||
const annotationBtn = Array.from(allButtons).find((b) =>
|
||||
b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen')
|
||||
[m.pdf_annotations_show(), m.pdf_annotations_hide()].includes(
|
||||
b.getAttribute('aria-label') ?? ''
|
||||
)
|
||||
);
|
||||
expect(annotationBtn).not.toBeNull();
|
||||
expect(annotationBtn!.className).toContain('focus-visible:ring-2');
|
||||
@@ -86,7 +91,12 @@ describe('PdfControls — focus rings (WCAG 2.1 §2.4.7)', () => {
|
||||
const allButtons = container.querySelectorAll('button');
|
||||
const iconOnlyButtons = Array.from(allButtons).filter((b) => {
|
||||
const label = b.getAttribute('aria-label') ?? '';
|
||||
return ['zurück', 'weiter', 'verkleinern', 'vergrößern'].includes(label.toLowerCase());
|
||||
return [
|
||||
m.viewer_previous_page(),
|
||||
m.viewer_next_page(),
|
||||
m.viewer_zoom_out(),
|
||||
m.viewer_zoom_in()
|
||||
].includes(label);
|
||||
});
|
||||
expect(iconOnlyButtons).toHaveLength(4);
|
||||
for (const btn of iconOnlyButtons) {
|
||||
@@ -104,7 +114,9 @@ describe('PdfControls — touch targets (WCAG 2.2 §2.5.8)', () => {
|
||||
});
|
||||
const allButtons = container.querySelectorAll('button');
|
||||
const annotationBtn = Array.from(allButtons).find((b) =>
|
||||
b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen')
|
||||
[m.pdf_annotations_show(), m.pdf_annotations_hide()].includes(
|
||||
b.getAttribute('aria-label') ?? ''
|
||||
)
|
||||
);
|
||||
expect(annotationBtn).not.toBeNull();
|
||||
expect(annotationBtn!.className).toContain('min-h-[44px]');
|
||||
@@ -118,7 +130,9 @@ describe('PdfControls — touch targets (WCAG 2.2 §2.5.8)', () => {
|
||||
});
|
||||
const allButtons = container.querySelectorAll('button');
|
||||
const annotationBtn = Array.from(allButtons).find((b) =>
|
||||
b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen')
|
||||
[m.pdf_annotations_show(), m.pdf_annotations_hide()].includes(
|
||||
b.getAttribute('aria-label') ?? ''
|
||||
)
|
||||
);
|
||||
expect(annotationBtn).not.toBeNull();
|
||||
expect(annotationBtn!.className).toContain('min-w-[44px]');
|
||||
@@ -131,7 +145,9 @@ describe('PdfControls — touch targets (WCAG 2.2 §2.5.8)', () => {
|
||||
showAnnotations: false
|
||||
});
|
||||
const btn1 = Array.from(c1.querySelectorAll('button')).find((b) =>
|
||||
b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen')
|
||||
[m.pdf_annotations_show(), m.pdf_annotations_hide()].includes(
|
||||
b.getAttribute('aria-label') ?? ''
|
||||
)
|
||||
);
|
||||
expect(btn1!.getAttribute('aria-pressed')).toBe('false');
|
||||
cleanup();
|
||||
@@ -142,7 +158,9 @@ describe('PdfControls — touch targets (WCAG 2.2 §2.5.8)', () => {
|
||||
showAnnotations: true
|
||||
});
|
||||
const btn2 = Array.from(c2.querySelectorAll('button')).find((b) =>
|
||||
b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen')
|
||||
[m.pdf_annotations_show(), m.pdf_annotations_hide()].includes(
|
||||
b.getAttribute('aria-label') ?? ''
|
||||
)
|
||||
);
|
||||
expect(btn2!.getAttribute('aria-pressed')).toBe('true');
|
||||
});
|
||||
@@ -152,7 +170,12 @@ describe('PdfControls — touch targets (WCAG 2.2 §2.5.8)', () => {
|
||||
const allButtons = container.querySelectorAll('button');
|
||||
const iconOnlyButtons = Array.from(allButtons).filter((b) => {
|
||||
const label = b.getAttribute('aria-label') ?? '';
|
||||
return ['zurück', 'weiter', 'verkleinern', 'vergrößern'].includes(label.toLowerCase());
|
||||
return [
|
||||
m.viewer_previous_page(),
|
||||
m.viewer_next_page(),
|
||||
m.viewer_zoom_out(),
|
||||
m.viewer_zoom_in()
|
||||
].includes(label);
|
||||
});
|
||||
expect(iconOnlyButtons).toHaveLength(4);
|
||||
for (const btn of iconOnlyButtons) {
|
||||
@@ -165,7 +188,12 @@ describe('PdfControls — touch targets (WCAG 2.2 §2.5.8)', () => {
|
||||
const allButtons = container.querySelectorAll('button');
|
||||
const iconOnlyButtons = Array.from(allButtons).filter((b) => {
|
||||
const label = b.getAttribute('aria-label') ?? '';
|
||||
return ['zurück', 'weiter', 'verkleinern', 'vergrößern'].includes(label.toLowerCase());
|
||||
return [
|
||||
m.viewer_previous_page(),
|
||||
m.viewer_next_page(),
|
||||
m.viewer_zoom_out(),
|
||||
m.viewer_zoom_in()
|
||||
].includes(label);
|
||||
});
|
||||
expect(iconOnlyButtons).toHaveLength(4);
|
||||
for (const btn of iconOnlyButtons) {
|
||||
|
||||
43
frontend/src/lib/messages.spec.ts
Normal file
43
frontend/src/lib/messages.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import de from '../../messages/de.json';
|
||||
import en from '../../messages/en.json';
|
||||
import es from '../../messages/es.json';
|
||||
|
||||
describe('message key parity', () => {
|
||||
it('de, en, and es have identical key sets', () => {
|
||||
const deKeys = Object.keys(de).sort();
|
||||
const enKeys = Object.keys(en).sort();
|
||||
const esKeys = Object.keys(es).sort();
|
||||
expect(enKeys).toEqual(deKeys);
|
||||
expect(esKeys).toEqual(deKeys);
|
||||
});
|
||||
|
||||
it('viewer navigation keys are present in all locales', () => {
|
||||
const requiredViewerKeys = [
|
||||
'viewer_previous_page',
|
||||
'viewer_next_page',
|
||||
'viewer_zoom_out',
|
||||
'viewer_zoom_in'
|
||||
];
|
||||
for (const key of requiredViewerKeys) {
|
||||
expect(de, `missing key in de: ${key}`).toHaveProperty(key);
|
||||
expect(en, `missing key in en: ${key}`).toHaveProperty(key);
|
||||
expect(es, `missing key in es: ${key}`).toHaveProperty(key);
|
||||
}
|
||||
});
|
||||
|
||||
it('transcribe mark-for-training key is present in all locales', () => {
|
||||
expect(de).toHaveProperty('transcribe_mark_for_training');
|
||||
expect(en).toHaveProperty('transcribe_mark_for_training');
|
||||
expect(es).toHaveProperty('transcribe_mark_for_training');
|
||||
});
|
||||
|
||||
it('layout menu open/close keys are present in all locales', () => {
|
||||
expect(de).toHaveProperty('layout_menu_open');
|
||||
expect(de).toHaveProperty('layout_menu_close');
|
||||
expect(en).toHaveProperty('layout_menu_open');
|
||||
expect(en).toHaveProperty('layout_menu_close');
|
||||
expect(es).toHaveProperty('layout_menu_open');
|
||||
expect(es).toHaveProperty('layout_menu_close');
|
||||
});
|
||||
});
|
||||
@@ -94,7 +94,7 @@ function handleOverlayKeydown(event: KeyboardEvent) {
|
||||
<!-- Hamburger toggle (mobile only) -->
|
||||
<button
|
||||
class="ml-auto flex h-11 w-11 items-center justify-center self-center rounded text-white/70 transition-colors hover:bg-white/10 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring lg:hidden"
|
||||
aria-label={mobileNavOpen ? 'Menü schließen' : 'Menü öffnen'}
|
||||
aria-label={mobileNavOpen ? m.layout_menu_close() : m.layout_menu_open()}
|
||||
aria-expanded={mobileNavOpen}
|
||||
aria-controls="mobile-nav"
|
||||
onclick={() => (mobileNavOpen = !mobileNavOpen)}
|
||||
|
||||
Reference in New Issue
Block a user