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

@@ -5,6 +5,7 @@
import PersonMultiSelect from '$lib/components/PersonMultiSelect.svelte';
import { untrack } from 'svelte';
import { isoToGerman, germanToIso } from '$lib/utils';
import { m } from '$lib/paraglide/messages.js';
let { data, form } = $props();
@@ -43,10 +44,10 @@
<div class="mb-6">
<a href="/documents/{doc.id}" class="inline-flex items-center text-xs font-bold uppercase tracking-widest text-gray-500 hover:text-brand-navy transition-colors group mb-4">
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" alt="" aria-hidden="true" class="w-4 h-4 mr-2 transform group-hover:-translate-x-1 transition-transform" />
Zurück zum Dokument
{m.btn_back_to_document()}
</a>
<h1 class="text-3xl font-serif text-brand-navy">
Bearbeiten<span class="text-brand-navy/70">{doc.title || doc.originalFilename}</span>
{m.doc_edit_heading()}<span class="text-brand-navy/70">{doc.title || doc.originalFilename}</span>
</h1>
</div>
@@ -58,20 +59,20 @@
<!-- ── Section 1: Wer & Wann ── -->
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Wer &amp; Wann</h2>
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.doc_section_who_when()}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<!-- Datum -->
<div>
<label for="documentDate" class="block text-sm font-medium text-gray-700 mb-1">Datum</label>
<label for="documentDate" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_date()}</label>
<input
id="documentDate"
type="text"
inputmode="numeric"
value={dateDisplay}
oninput={handleDateInput}
placeholder="TT.MM.JJJJ"
placeholder={m.form_placeholder_date()}
maxlength="10"
class="block w-full rounded border-gray-300 shadow-sm p-2 border text-sm
{dateInvalid ? 'border-red-400 focus:border-red-500 focus:ring-red-500' : 'focus:border-brand-navy focus:ring-brand-navy'}"
@@ -79,19 +80,19 @@
/>
<input type="hidden" name="documentDate" value={dateIso} />
{#if dateInvalid}
<p id="date-error" class="mt-1 text-xs text-red-600">Bitte im Format TT.MM.JJJJ eingeben, z.B. 20.12.2026</p>
<p id="date-error" class="mt-1 text-xs text-red-600">{m.form_date_error()}</p>
{/if}
</div>
<!-- Ort -->
<div>
<label for="location" class="block text-sm font-medium text-gray-700 mb-1">Ort</label>
<label for="location" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_location()}</label>
<input
id="location"
type="text"
name="location"
value={doc.location || ''}
placeholder="z.B. Berlin, Wien…"
placeholder={m.form_placeholder_location()}
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm"
/>
</div>
@@ -100,7 +101,7 @@
<div>
<PersonTypeahead
name="senderId"
label="Absender"
label={m.form_label_sender()}
bind:value={senderId}
initialName={doc.sender ? `${doc.sender.firstName} ${doc.sender.lastName}` : ''}
/>
@@ -108,7 +109,7 @@
<!-- Empfänger -->
<div>
<p class="block text-sm font-medium text-gray-700 mb-1">Empfänger</p>
<p class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_receivers()}</p>
<PersonMultiSelect bind:selectedPersons={selectedReceivers} />
</div>
@@ -117,13 +118,13 @@
<!-- ── Section 2: Beschreibung ── -->
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Beschreibung</h2>
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.doc_section_description()}</h2>
<div class="space-y-5">
<!-- Titel -->
<div>
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Titel *</label>
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_title()} *</label>
<input
id="title"
type="text"
@@ -136,33 +137,33 @@
<!-- Aufbewahrungsort -->
<div>
<label for="documentLocation" class="block text-sm font-medium text-gray-700 mb-1">Aufbewahrungsort</label>
<label for="documentLocation" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_archive_location()}</label>
<input
id="documentLocation"
type="text"
name="documentLocation"
value={doc.documentLocation || ''}
placeholder="z.B. Schrank 3, Mappe B"
placeholder={m.form_placeholder_archive_location()}
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm"
/>
<p class="mt-1 text-xs text-gray-400">Wo befindet sich das Originaldokument?</p>
<p class="mt-1 text-xs text-gray-400">{m.form_helper_archive_location()}</p>
</div>
<!-- Schlagworte -->
<div>
<p class="block text-sm font-medium text-gray-700 mb-1">Schlagworte</p>
<p class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_tags()}</p>
<TagInput bind:tags />
<input type="hidden" name="tags" value={tags.join(',')} />
</div>
<!-- Inhalt -->
<div>
<label for="summary" class="block text-sm font-medium text-gray-700 mb-1">Inhalt</label>
<label for="summary" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_content()}</label>
<textarea
id="summary"
name="summary"
rows="5"
placeholder="Kurze Beschreibung des Inhalts…"
placeholder={m.form_placeholder_content()}
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm font-serif"
>{doc.summary || ''}</textarea>
</div>
@@ -172,27 +173,27 @@
<!-- ── Section 3: Transkription ── -->
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Transkription</h2>
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.form_label_transcription()}</h2>
<textarea
id="transcription"
name="transcription"
rows="12"
placeholder="Vollständiger Text des Dokuments…"
placeholder={m.form_placeholder_transcription()}
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm font-serif"
>{doc.transcription || ''}</textarea>
</div>
<!-- ── Section 4: Datei ── -->
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Datei</h2>
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.doc_section_file()}</h2>
<div class="flex items-center gap-3 mb-4 text-sm text-gray-600 bg-brand-sand/20 rounded px-3 py-2">
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/PDF-Document-MD.svg" alt="" aria-hidden="true" class="w-4 h-4 flex-shrink-0" />
<span>Aktuelle Datei: <strong class="text-brand-navy font-medium">{doc.originalFilename}</strong></span>
<span>{m.doc_current_file_label()} <strong class="text-brand-navy font-medium">{doc.originalFilename}</strong></span>
</div>
<label for="file-upload" class="block text-sm font-medium text-gray-700 mb-1">
Neue Datei hochladen <span class="font-normal text-gray-400">(ersetzt die aktuelle Datei)</span>
{m.doc_file_replace_label()} <span class="font-normal text-gray-400">({m.doc_file_replace_note()})</span>
</label>
<input
id="file-upload"
@@ -213,13 +214,13 @@
href="/documents/{doc.id}"
class="text-sm text-gray-500 hover:text-brand-navy transition-colors font-medium"
>
Abbrechen
{m.btn_cancel()}
</a>
<button
type="submit"
class="px-6 py-2 bg-brand-navy text-white text-sm font-bold uppercase tracking-widest rounded hover:bg-brand-navy/80 transition-colors"
>
Speichern
{m.btn_save()}
</button>
</div>