diff --git a/frontend/messages/de.json b/frontend/messages/de.json
index 37bd55cd..14269eed 100644
--- a/frontend/messages/de.json
+++ b/frontend/messages/de.json
@@ -603,6 +603,8 @@
"doc_details_no_tags": "Keine Schlagwörter zugeordnet",
"doc_details_more_receivers": "+{count} weitere",
"transcription_mode_label": "Transkribieren",
+ "transcription_read_label": "Transkription lesen",
+ "transcription_panel_title": "Transkription",
"transcription_mode_stop": "Fertig",
"transcription_block_placeholder": "Text eingeben — mit @Name eine Person aus dem Archiv verknüpfen",
"transcription_block_save_saving": "Speichere...",
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 0f5f862f..b7de0948 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -603,6 +603,8 @@
"doc_details_no_tags": "No tags assigned",
"doc_details_more_receivers": "+{count} more",
"transcription_mode_label": "Transcribe",
+ "transcription_read_label": "Read transcription",
+ "transcription_panel_title": "Transcription",
"transcription_mode_stop": "Done",
"transcription_block_placeholder": "Type text — use @name to link a person from the archive",
"transcription_block_save_saving": "Saving...",
diff --git a/frontend/messages/es.json b/frontend/messages/es.json
index 3d9e92ab..ee584c40 100644
--- a/frontend/messages/es.json
+++ b/frontend/messages/es.json
@@ -603,6 +603,8 @@
"doc_details_no_tags": "No hay etiquetas asignadas",
"doc_details_more_receivers": "+{count} más",
"transcription_mode_label": "Transcribir",
+ "transcription_read_label": "Leer transcripción",
+ "transcription_panel_title": "Transcripción",
"transcription_mode_stop": "Listo",
"transcription_block_placeholder": "Escriba el texto — use @nombre para vincular a una persona del archivo",
"transcription_block_save_saving": "Guardando...",
diff --git a/frontend/src/lib/document/DocumentMobileMenu.svelte b/frontend/src/lib/document/DocumentMobileMenu.svelte
index c2892bbd..42c6df5f 100644
--- a/frontend/src/lib/document/DocumentMobileMenu.svelte
+++ b/frontend/src/lib/document/DocumentMobileMenu.svelte
@@ -5,6 +5,7 @@ import { clickOutside } from '$lib/shared/actions/clickOutside';
type Props = {
canWrite: boolean;
isPdf: boolean;
+ hasTranscription: boolean;
transcribeMode: boolean;
filePath?: string | null;
originalFilename?: string | null;
@@ -14,12 +15,18 @@ type Props = {
let {
canWrite,
isPdf,
+ hasTranscription,
transcribeMode = $bindable(),
filePath = null,
originalFilename = null,
fileUrl
}: Props = $props();
+const canOpenTranscription = $derived((canWrite || hasTranscription) && isPdf);
+const transcriptionLabel = $derived(
+ canWrite ? m.transcription_mode_label() : m.transcription_read_label()
+);
+
let mobileMenuOpen = $state(false);
function startTranscribe() {
@@ -50,10 +57,10 @@ function startTranscribe() {
role="menu"
class="absolute top-full right-0 z-50 mt-1 min-w-[200px] rounded-md border border-line bg-surface p-2 shadow-lg"
>
- {#if canWrite && isPdf && !transcribeMode}
+ {#if canOpenTranscription && !transcribeMode}
{/if}
diff --git a/frontend/src/lib/document/DocumentMobileMenu.svelte.test.ts b/frontend/src/lib/document/DocumentMobileMenu.svelte.test.ts
index 9b561aa5..c3c3f0a0 100644
--- a/frontend/src/lib/document/DocumentMobileMenu.svelte.test.ts
+++ b/frontend/src/lib/document/DocumentMobileMenu.svelte.test.ts
@@ -8,6 +8,7 @@ afterEach(cleanup);
const baseProps = {
canWrite: false,
isPdf: false,
+ hasTranscription: false,
transcribeMode: false,
filePath: null as string | null,
originalFilename: 'brief.pdf' as string | null,
@@ -49,6 +50,43 @@ describe('DocumentMobileMenu', () => {
await expect.element(page.getByRole('button', { name: /transkribieren/i })).toBeVisible();
});
+ it('shows no transcription action for a read-only user when no transcription exists', async () => {
+ render(DocumentMobileMenu, {
+ props: {
+ ...baseProps,
+ canWrite: false,
+ isPdf: true,
+ hasTranscription: false,
+ filePath: 'docs/x.pdf'
+ }
+ });
+
+ await page.getByRole('button', { name: /weitere aktionen/i }).click();
+
+ await expect
+ .element(page.getByRole('button', { name: /transkribieren/i }))
+ .not.toBeInTheDocument();
+ await expect
+ .element(page.getByRole('button', { name: /transkription lesen/i }))
+ .not.toBeInTheDocument();
+ });
+
+ it('shows the read-transcription action for a read-only user when a transcription exists', async () => {
+ render(DocumentMobileMenu, {
+ props: {
+ ...baseProps,
+ canWrite: false,
+ isPdf: true,
+ hasTranscription: true,
+ filePath: 'docs/x.pdf'
+ }
+ });
+
+ await page.getByRole('button', { name: /weitere aktionen/i }).click();
+
+ await expect.element(page.getByRole('button', { name: /transkription lesen/i })).toBeVisible();
+ });
+
it('hides the transcribe action when already in transcribeMode', async () => {
render(DocumentMobileMenu, {
props: {
diff --git a/frontend/src/lib/document/DocumentTopBar.svelte b/frontend/src/lib/document/DocumentTopBar.svelte
index 7766e376..0c935f46 100644
--- a/frontend/src/lib/document/DocumentTopBar.svelte
+++ b/frontend/src/lib/document/DocumentTopBar.svelte
@@ -28,6 +28,7 @@ type Doc = {
location?: string | null;
status?: string | null;
tags?: Tag[] | null;
+ hasTranscription?: boolean;
};
type GeschichteSummary = {
@@ -132,6 +133,7 @@ const overflowPersons = $derived(receivers.slice(2));
documentId={doc.id}
canWrite={canWrite}
isPdf={!!isPdf}
+ hasTranscription={!!doc.hasTranscription}
bind:transcribeMode={transcribeMode}
filePath={doc.filePath}
originalFilename={doc.originalFilename}
@@ -143,6 +145,7 @@ const overflowPersons = $derived(receivers.slice(2));
-{#if canWrite && isPdf && !transcribeMode}
+{#if canOpenTranscription && !transcribeMode}
{/if}
diff --git a/frontend/src/lib/document/DocumentTopBarActions.svelte.test.ts b/frontend/src/lib/document/DocumentTopBarActions.svelte.test.ts
index 280a3245..1b59d0e2 100644
--- a/frontend/src/lib/document/DocumentTopBarActions.svelte.test.ts
+++ b/frontend/src/lib/document/DocumentTopBarActions.svelte.test.ts
@@ -9,6 +9,7 @@ const baseProps = {
documentId: 'd1',
canWrite: false,
isPdf: false,
+ hasTranscription: false,
transcribeMode: false,
filePath: null as string | null,
originalFilename: 'brief.pdf' as string | null,
@@ -34,6 +35,56 @@ describe('DocumentTopBarActions', () => {
await expect.element(page.getByRole('button', { name: /transkribieren/i })).toBeVisible();
});
+ it('shows no transcription control for a read-only user when no transcription exists', async () => {
+ render(DocumentTopBarActions, {
+ props: {
+ ...baseProps,
+ canWrite: false,
+ isPdf: true,
+ hasTranscription: false,
+ filePath: 'docs/x.pdf'
+ }
+ });
+
+ await expect
+ .element(page.getByRole('button', { name: /transkribieren/i }))
+ .not.toBeInTheDocument();
+ await expect
+ .element(page.getByRole('button', { name: /transkription lesen/i }))
+ .not.toBeInTheDocument();
+ });
+
+ it('shows the read-transcription button for a read-only user when a transcription exists', async () => {
+ render(DocumentTopBarActions, {
+ props: {
+ ...baseProps,
+ canWrite: false,
+ isPdf: true,
+ hasTranscription: true,
+ filePath: 'docs/x.pdf'
+ }
+ });
+
+ await expect.element(page.getByRole('button', { name: /transkription lesen/i })).toBeVisible();
+ await expect
+ .element(page.getByRole('button', { name: /^transkribieren$/i }))
+ .not.toBeInTheDocument();
+ });
+
+ it('shows the transcribe (edit) label for a writer regardless of hasTranscription', async () => {
+ render(DocumentTopBarActions, {
+ props: {
+ ...baseProps,
+ canWrite: true,
+ isPdf: true,
+ hasTranscription: false,
+ filePath: 'docs/x.pdf'
+ }
+ });
+
+ await expect.element(page.getByRole('button', { name: /transkribieren/i })).toBeVisible();
+ });
+
it('omits the transcribe button when not a PDF', async () => {
render(DocumentTopBarActions, {
props: { ...baseProps, canWrite: true, isPdf: false, filePath: 'docs/x.jpg' }