feat: implement i18n — extract all UI strings, add EN + ES-MX translations, add language selector
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:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
@@ -41,7 +42,7 @@
|
||||
|
||||
<div class="max-w-7xl mx-auto py-8 sm:px-6 lg:px-8 font-sans">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-3xl font-serif text-brand-navy">Admin Dashboard</h1>
|
||||
<h1 class="text-3xl font-serif text-brand-navy">{m.admin_heading()}</h1>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex bg-white rounded-lg p-1 shadow-sm border border-gray-200">
|
||||
@@ -50,21 +51,21 @@
|
||||
'users'
|
||||
? 'bg-brand-navy text-white'
|
||||
: 'text-gray-500 hover:text-brand-navy'}"
|
||||
onclick={() => (activeTab = 'users')}>Benutzer</button
|
||||
onclick={() => (activeTab = 'users')}>{m.admin_tab_users()}</button
|
||||
>
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
|
||||
'groups'
|
||||
? 'bg-brand-navy text-white'
|
||||
: 'text-gray-500 hover:text-brand-navy'}"
|
||||
onclick={() => (activeTab = 'groups')}>Gruppen</button
|
||||
onclick={() => (activeTab = 'groups')}>{m.admin_tab_groups()}</button
|
||||
>
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
|
||||
'tags'
|
||||
? 'bg-brand-navy text-white'
|
||||
: 'text-gray-500 hover:text-brand-navy'}"
|
||||
onclick={() => (activeTab = 'tags')}>Schlagworte</button
|
||||
onclick={() => (activeTab = 'tags')}>{m.admin_tab_tags()}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,22 +79,22 @@
|
||||
{#if activeTab === 'users'}
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
||||
<h2 class="text-lg font-bold text-gray-700">Benutzerverwaltung</h2>
|
||||
<h2 class="text-lg font-bold text-gray-700">{m.admin_section_users()}</h2>
|
||||
</div>
|
||||
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Login</th
|
||||
>{m.admin_col_login()}</th
|
||||
>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Gruppen</th
|
||||
>{m.admin_col_groups()}</th
|
||||
>
|
||||
{#if editingUserId}
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Passwort</th
|
||||
>{m.admin_col_password()}</th
|
||||
>
|
||||
{/if}
|
||||
</tr>
|
||||
@@ -129,7 +130,7 @@
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<p class="text-[10px] text-gray-400 mt-1">Strg+Klick für Auswahl</p>
|
||||
<p class="text-[10px] text-gray-400 mt-1">{m.admin_multiselect_hint()}</p>
|
||||
</td>
|
||||
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right align-top">
|
||||
@@ -147,7 +148,7 @@
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Neues PW (optional)"
|
||||
placeholder={m.admin_password_placeholder()}
|
||||
class="w-32 py-1 px-2 text-xs border border-brand-mint rounded"
|
||||
/>
|
||||
|
||||
@@ -156,14 +157,14 @@
|
||||
type="submit"
|
||||
class="bg-green-600 text-white px-2 py-1 rounded text-xs font-bold uppercase hover:bg-green-700"
|
||||
>
|
||||
Speichern
|
||||
{m.btn_save()}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={cancelEditUser}
|
||||
class="bg-gray-200 text-gray-600 px-2 py-1 rounded text-xs font-bold uppercase hover:bg-gray-300"
|
||||
>
|
||||
Abbrechen
|
||||
{m.btn_cancel()}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -184,7 +185,7 @@
|
||||
</span>
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-xs text-gray-400 italic">Keine Gruppen</span>
|
||||
<span class="text-xs text-gray-400 italic">{m.admin_no_groups()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
@@ -194,7 +195,7 @@
|
||||
onclick={() => startEditUser(user.id)}
|
||||
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
|
||||
>
|
||||
Bearbeiten
|
||||
{m.btn_edit()}
|
||||
</button>
|
||||
|
||||
<form
|
||||
@@ -213,7 +214,7 @@
|
||||
<input type="hidden" name="id" value={user.id} />
|
||||
<button
|
||||
class="text-gray-300 hover:text-red-600 transition-colors p-1"
|
||||
title="Benutzer löschen"
|
||||
title={m.admin_btn_delete_user_title()}
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -236,7 +237,7 @@
|
||||
<!-- Create User Form -->
|
||||
<div class="p-6 bg-gray-50 border-t border-gray-200">
|
||||
<h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide">
|
||||
Neuen Benutzer anlegen
|
||||
{m.admin_section_new_user()}
|
||||
</h3>
|
||||
<form
|
||||
method="POST"
|
||||
@@ -254,7 +255,7 @@
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Passwort"
|
||||
placeholder={m.admin_col_password()}
|
||||
required
|
||||
class="rounded border-gray-300 text-sm w-full"
|
||||
/>
|
||||
@@ -265,19 +266,19 @@
|
||||
multiple
|
||||
class="rounded border-gray-300 text-sm w-full h-[42px] py-1"
|
||||
required
|
||||
title="Strg+Klick für mehrere"
|
||||
title={m.admin_multiselect_hint_multi()}
|
||||
>
|
||||
{#each data.groups as group}
|
||||
<option value={group.id}>{group.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<p class="text-[10px] text-gray-400 mt-1">Strg+Klick für Mehrfachauswahl</p>
|
||||
<p class="text-[10px] text-gray-400 mt-1">{m.admin_multiselect_hint_full()}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-brand-navy text-white h-[42px] rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full"
|
||||
>Anlegen</button
|
||||
>{m.btn_create()}</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
@@ -285,9 +286,9 @@
|
||||
{:else if activeTab === 'tags'}
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
|
||||
<div class="p-6 border-b border-gray-100 bg-yellow-50/50">
|
||||
<h2 class="text-lg font-bold text-gray-700">Schlagworte</h2>
|
||||
<h2 class="text-lg font-bold text-gray-700">{m.admin_section_tags()}</h2>
|
||||
<p class="text-xs text-yellow-800 mt-1">
|
||||
Warnung: Umbenennen oder Löschen wirkt sich auf alle verknüpften Dokumente aus.
|
||||
{m.admin_tags_warning()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -312,7 +313,7 @@
|
||||
bind:value={editingTagName}
|
||||
class="flex-1 border-brand-mint ring-1 ring-brand-mint rounded px-2 py-1 text-sm"
|
||||
/>
|
||||
<button aria-label="Speichern" class="text-green-600 hover:text-green-800"
|
||||
<button aria-label={m.btn_save()} class="text-green-600 hover:text-green-800"
|
||||
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
@@ -325,7 +326,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={cancelEditTag}
|
||||
aria-label="Abbrechen"
|
||||
aria-label={m.btn_cancel()}
|
||||
class="text-gray-400 hover:text-gray-600"
|
||||
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
@@ -346,7 +347,7 @@
|
||||
>
|
||||
<button
|
||||
onclick={() => startEditTag(tag)}
|
||||
aria-label="Schlagwort bearbeiten"
|
||||
aria-label={m.admin_btn_edit_tag_label()}
|
||||
class="p-1 text-gray-400 hover:text-brand-navy"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
@@ -363,9 +364,7 @@
|
||||
action="?/deleteTag"
|
||||
use:enhance={({ cancel }) => {
|
||||
if (
|
||||
!confirm(
|
||||
'Wirklich löschen? Das Schlagwort wird aus allen Dokumenten entfernt.'
|
||||
)
|
||||
!confirm(m.admin_tag_delete_confirm())
|
||||
) {
|
||||
cancel();
|
||||
}
|
||||
@@ -376,7 +375,7 @@
|
||||
class="inline"
|
||||
>
|
||||
<input type="hidden" name="id" value={tag.id} />
|
||||
<button aria-label="Schlagwort löschen" class="p-1 text-gray-400 hover:text-red-600">
|
||||
<button aria-label={m.admin_btn_delete_tag_label()} class="p-1 text-gray-400 hover:text-red-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
@@ -396,21 +395,21 @@
|
||||
{:else if activeTab === 'groups'}
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
||||
<h2 class="text-lg font-bold text-gray-700">Gruppenverwaltung</h2>
|
||||
<h2 class="text-lg font-bold text-gray-700">{m.admin_section_groups()}</h2>
|
||||
</div>
|
||||
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Name</th
|
||||
>{m.admin_col_name()}</th
|
||||
>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Berechtigungen</th
|
||||
>{m.admin_col_permissions()}</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Aktionen</th
|
||||
>{m.admin_col_actions()}</th
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -460,7 +459,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 self-start sm:self-center">
|
||||
<button type="submit" aria-label="Speichern" class="text-green-600 hover:text-green-800 p-1">
|
||||
<button type="submit" aria-label={m.btn_save()} class="text-green-600 hover:text-green-800 p-1">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
@@ -473,7 +472,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={cancelEditGroup}
|
||||
aria-label="Abbrechen"
|
||||
aria-label={m.btn_cancel()}
|
||||
class="text-gray-400 hover:text-red-500 p-1"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
@@ -513,14 +512,14 @@
|
||||
onclick={() => startEditGroup(group.id)}
|
||||
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
|
||||
>
|
||||
Bearbeiten
|
||||
{m.btn_edit()}
|
||||
</button>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteGroup"
|
||||
use:enhance={({ cancel }) => {
|
||||
if (!confirm('Gruppe wirklich löschen?')) {
|
||||
if (!confirm(m.admin_group_delete_confirm())) {
|
||||
cancel();
|
||||
}
|
||||
return async ({ update }) => {
|
||||
@@ -531,7 +530,7 @@
|
||||
<input type="hidden" name="id" value={group.id} />
|
||||
<button
|
||||
class="text-gray-300 hover:text-red-600 p-1 transition-colors"
|
||||
title="Löschen"
|
||||
title={m.btn_delete()}
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -554,7 +553,7 @@
|
||||
<!-- CREATE GROUP FORM -->
|
||||
<div class="p-6 bg-gray-50 border-t border-gray-200">
|
||||
<h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide">
|
||||
Neue Gruppe anlegen
|
||||
{m.admin_section_new_group()}
|
||||
</h3>
|
||||
<form
|
||||
method="POST"
|
||||
@@ -566,7 +565,7 @@
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Gruppenname (z.B. Editoren)"
|
||||
placeholder={m.admin_group_name_placeholder()}
|
||||
required
|
||||
class="rounded border-gray-300 text-sm w-full"
|
||||
/>
|
||||
@@ -590,7 +589,7 @@
|
||||
type="submit"
|
||||
class="bg-brand-navy text-white px-6 py-2 rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full md:w-auto"
|
||||
>
|
||||
Anlegen
|
||||
{m.btn_create()}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user