feat: implement i18n — extract all UI strings, add EN + ES-MX translations, add language selector
Some checks failed
CI / Unit & Component Tests (push) Successful in 9m36s
CI / Backend Unit Tests (push) Successful in 2m15s
CI / E2E Tests (push) Failing after 14m41s

Extract all hardcoded German strings from every .svelte file and component
into Paraglide message keys. Add complete translations for all keys in
messages/en.json (English) and messages/es.json (Spanish/Mexico).

Changes:
- messages/de.json: 100+ keys covering navigation, buttons, form labels,
  placeholders, section headings, empty states, and error messages
- messages/en.json, messages/es.json: complete translations for all keys
- project.inlang/settings.json: change baseLocale from "en" to "de"
- +layout.svelte: add DE/EN/ES language selector in header using setLocale();
  active language is bold, choice persists via Paraglide cookie strategy
- All 10 route pages + 3 shared components: replace hardcoded German with m.key()
- e2e/lang.spec.ts: E2E tests for language selector visibility, switching,
  persistence across navigation, and active state highlighting

Closes #2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #9.
This commit is contained in:
Marcel
2026-03-19 12:39:36 +01:00
committed by marcel
parent db6dc28528
commit 0e76be5672
20 changed files with 733 additions and 199 deletions

View File

@@ -1,4 +1,6 @@
<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
let { data } = $props();
const doc = $derived(data.document);
@@ -53,7 +55,7 @@
>
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" alt="" aria-hidden="true" class="w-4 h-4" />
</div>
<span>Zurück</span>
<span>{m.btn_back()}</span>
</a>
<div class="flex items-center gap-4 overflow-hidden border-l border-gray-200 pl-6">
@@ -77,7 +79,7 @@
class="text-brand-navy bg-transparent border border-brand-navy hover:bg-brand-navy hover:text-white px-4 py-2 rounded text-sm font-medium transition flex items-center gap-2"
>
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg" alt="" aria-hidden="true" class="w-4 h-4" />
Bearbeiten
{m.btn_edit()}
</a>
{#if doc.filePath}
@@ -105,7 +107,7 @@
<h3
class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2"
>
Details
{m.doc_section_details()}
</h3>
<div class="space-y-5">
<!-- Date -->
@@ -117,7 +119,7 @@
<span class="block font-serif text-lg text-brand-navy">
{doc.documentDate ? new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(doc.documentDate + 'T12:00:00')) : '—'}
</span>
<span class="text-xs font-sans text-gray-500">Dokumentendatum</span>
<span class="text-xs font-sans text-gray-500">{m.doc_label_document_date()}</span>
</div>
</div>
@@ -130,7 +132,7 @@
<span class="block font-serif text-lg text-brand-navy">
{doc.location ? doc.location : '—'}
</span>
<span class="text-xs font-sans text-gray-500">Erstellungsort</span>
<span class="text-xs font-sans text-gray-500">{m.doc_label_creation_location()}</span>
</div>
</div>
@@ -144,7 +146,7 @@
<span class="block font-serif text-lg text-brand-navy">
{doc.documentLocation}
</span>
<span class="text-xs font-sans text-gray-500">Aufbewahrungsort (Original)</span>
<span class="text-xs font-sans text-gray-500">{m.doc_label_archive_location_original()}</span>
</div>
</div>
{/if}
@@ -167,7 +169,7 @@
</a>
{/each}
</div>
<span class="text-xs font-sans text-gray-500">Schlagworte</span>
<span class="text-xs font-sans text-gray-500">{m.form_label_tags()}</span>
</div>
</div>
{/if}
@@ -179,11 +181,11 @@
<h3
class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2"
>
Personen
{m.doc_section_persons()}
</h3>
<div class="mb-6">
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Absender</span>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.form_label_sender()}</span>
{#if doc.sender}
<a
href="/persons/{doc.sender.id}"
@@ -209,12 +211,12 @@
</div>
</a>
{:else}
<span class="text-sm font-serif text-gray-400 italic">Nicht angegeben</span>
<span class="text-sm font-serif text-gray-400 italic">{m.doc_sender_not_specified()}</span>
{/if}
</div>
<div>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Empfänger</span>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.form_label_receivers()}</span>
{#if doc.receivers && doc.receivers.length > 0}
<div class="space-y-2">
{#each doc.receivers as receiver}
@@ -248,7 +250,7 @@
{/each}
</div>
{:else}
<span class="text-sm font-serif text-gray-400 italic">Keine Empfänger</span>
<span class="text-sm font-serif text-gray-400 italic">{m.doc_no_receivers()}</span>
{/if}
</div>
</div>
@@ -259,13 +261,13 @@
<h3
class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2"
>
Inhalt
{m.doc_section_content()}
</h3>
<div class="space-y-6">
{#if doc.summary}
<div>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Zusammenfassung</span>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.doc_label_summary()}</span>
<div
class="bg-brand-sand/30 p-5 rounded border border-brand-sand text-sm font-serif text-brand-navy leading-relaxed whitespace-pre-wrap"
>
@@ -276,7 +278,7 @@
{#if doc.transcription}
<div>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Transkription</span>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.form_label_transcription()}</span>
<div
class="bg-brand-sand/30 p-5 rounded border border-brand-sand text-sm font-serif text-brand-navy leading-relaxed whitespace-pre-wrap"
>
@@ -314,7 +316,7 @@
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span class="font-sans text-sm tracking-wide">Lade Dokument...</span>
<span class="font-sans text-sm tracking-wide">{m.doc_loading()}</span>
</div>
{:else if error}
<div class="text-gray-400 text-center px-4">
@@ -325,7 +327,7 @@
target="_blank"
class="underline hover:text-white text-sm"
>
Direkter Download versuchen
{m.doc_download_link()}
</a>
{/if}
</div>
@@ -334,7 +336,7 @@
<div class="bg-white/5 p-8 rounded-full mb-6">
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/PDF-Document-MD.svg" alt="" aria-hidden="true" class="w-12 h-12 opacity-50 invert" />
</div>
<p class="font-sans text-sm tracking-wide uppercase">Kein Scan vorhanden</p>
<p class="font-sans text-sm tracking-wide uppercase">{m.doc_no_scan()}</p>
</div>
{:else if fileUrl && doc.originalFilename.toLowerCase().endsWith('.pdf')}
<iframe