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,5 +1,6 @@
<script lang="ts">
import type { components } from '$lib/generated/api';
import { m } from '$lib/paraglide/messages.js';
type Person = components['schemas']['Person'];
interface Props {
@@ -76,7 +77,7 @@
type="button"
onclick={() => removePerson(person.id)}
class="text-brand-navy/50 hover:text-red-500 focus:outline-none ml-0.5"
aria-label="Entfernen"
aria-label={m.comp_multiselect_remove()}
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
@@ -92,7 +93,7 @@
bind:value={searchTerm}
oninput={handleInput}
onfocus={() => { updateDropdownPosition(); showDropdown = true; }}
placeholder={selectedPersons.length === 0 ? 'Namen tippen...' : ''}
placeholder={selectedPersons.length === 0 ? m.comp_multiselect_placeholder() : ''}
class="flex-1 min-w-[120px] border-none p-1 focus:ring-0 text-sm bg-transparent outline-none"
/>
</div>
@@ -103,7 +104,7 @@
class="z-50 bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm"
>
{#if loading}
<div class="p-2 text-gray-500 text-sm">Suche...</div>
<div class="p-2 text-gray-500 text-sm">{m.comp_multiselect_loading()}</div>
{:else}
{#each results as person}
<div

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import type { components } from '$lib/generated/api';
import { m } from '$lib/paraglide/messages.js';
type Person = components['schemas']['Person'];
interface Props {
@@ -95,7 +96,7 @@
bind:value={searchTerm}
oninput={handleInput}
onfocus={() => { updateDropdownPosition(); showDropdown = true; }}
placeholder="Namen tippen..."
placeholder={m.comp_typeahead_placeholder()}
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm border p-2 focus:ring-blue-500 focus:border-blue-500"
/>
@@ -105,7 +106,7 @@
class="z-50 bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
{#if loading}
<div class="p-2 text-gray-500 text-sm">Suche...</div>
<div class="p-2 text-gray-500 text-sm">{m.comp_typeahead_loading()}</div>
{:else}
{#each results as person}
<div

View File

@@ -1,4 +1,6 @@
<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
interface Props {
tags?: string[];
allowCreation?: boolean;
@@ -88,7 +90,7 @@
<button
type="button"
onclick={() => removeTag(i)}
aria-label="Schlagwort entfernen"
aria-label={m.comp_taginput_remove()}
class="text-brand-navy/50 hover:text-red-500 focus:outline-none"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"
@@ -113,8 +115,8 @@
onfocus={() => fetchSuggestions(inputVal)}
placeholder={tags.length === 0
? allowCreation
? 'Schlagworte hinzufügen...'
: 'Nach Schlagworten filtern...'
? m.comp_taginput_placeholder_create()
: m.comp_taginput_placeholder_filter()
: ''}
class="w-full h-full border-none p-1 focus:ring-0 text-sm bg-transparent outline-none"
/>
@@ -143,6 +145,6 @@
</div>
</div>
{#if allowCreation}
<p class="text-xs text-gray-400 mt-1">Enter drücken um Schlagwort zu erstellen.</p>
<p class="text-xs text-gray-400 mt-1">{m.comp_taginput_create_hint()}</p>
{/if}
</div>