feat: add i18n localization with language switcher to all web apps

- Add svelte-i18n configuration with SSR support to all web apps
- Create LanguageSelector component for each app with brand colors
- Add German and English locale files
- Integrate language switcher into login pages via headerControls snippet
- Fix Tailwind v4 @source directives for shared package scanning
- Update AppSlider styling to match login container design

Apps updated:
- Memoro (gold #f8d62b)
- Märchenzauber (pink #FF6B9D)
- ManaDeck (purple #8b5cf6)
- ManaCore (indigo #6366f1)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-25 01:41:25 +01:00
parent bd869dfe09
commit 926ca231b5
147 changed files with 7090 additions and 2276 deletions

View file

@ -137,6 +137,8 @@
<!-- Actions -->
{#if actions}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="data-card__actions flex-shrink-0 flex items-center gap-1" onclick={(e) => e.stopPropagation()}>
{@render actions()}
</div>

View file

@ -13,6 +13,8 @@
required?: boolean;
autocomplete?: HTMLInputAttributes['autocomplete'];
class?: string;
id?: string;
name?: string;
}
let {
@ -26,7 +28,9 @@
disabled = false,
required = false,
autocomplete,
class: className = ''
class: className = '',
id = `input-${Math.random().toString(36).slice(2, 9)}`,
name
}: Props = $props();
function handleInput(e: Event) {
@ -43,15 +47,17 @@
<div class="flex flex-col gap-1.5 {className}">
{#if label}
<label class="text-sm font-medium text-theme">
<label for={id} class="text-sm font-medium text-theme">
{label}
{#if required}
<span class="text-red-500">*</span>
{/if}
</label>
{/if}
<input
{id}
{name}
{type}
{value}
{placeholder}

View file

@ -1,9 +1,5 @@
<script lang="ts">
export interface SelectOption {
value: string;
label: string;
disabled?: boolean;
}
import type { SelectOption } from './Select.types';
interface Props {
/** Current selected value */
@ -24,6 +20,8 @@
required?: boolean;
/** Additional CSS classes */
class?: string;
/** Unique ID for accessibility */
id?: string;
}
let {
@ -35,7 +33,8 @@
error,
disabled = false,
required = false,
class: className = ''
class: className = '',
id = `select-${Math.random().toString(36).slice(2, 9)}`
}: Props = $props();
function handleChange(e: Event) {
@ -47,7 +46,7 @@
<div class="select-wrapper {className}">
{#if label}
<label class="select-label">
<label for={id} class="select-label">
{label}
{#if required}
<span class="select-required">*</span>
@ -57,6 +56,7 @@
<div class="select-container">
<select
{id}
{value}
{disabled}
{required}

View file

@ -0,0 +1,5 @@
export interface SelectOption {
value: string;
label: string;
disabled?: boolean;
}

View file

@ -26,6 +26,8 @@
autoResize?: boolean;
/** Additional CSS classes */
class?: string;
/** Unique ID for accessibility */
id?: string;
}
let {
@ -41,7 +43,8 @@
disabled = false,
required = false,
autoResize = false,
class: className = ''
class: className = '',
id = `textarea-${Math.random().toString(36).slice(2, 9)}`
}: Props = $props();
let textareaElement: HTMLTextAreaElement | null = $state(null);
@ -68,7 +71,7 @@
<div class="textarea-wrapper {className}">
{#if label}
<label class="textarea-label">
<label for={id} class="textarea-label">
{label}
{#if required}
<span class="textarea-required">*</span>
@ -77,6 +80,7 @@
{/if}
<textarea
{id}
bind:this={textareaElement}
{value}
{placeholder}

View file

@ -27,6 +27,7 @@
: 'bg-menu'} {disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}"
role="switch"
aria-checked={isOn}
aria-label="Toggle"
{disabled}
>
<span

View file

@ -69,7 +69,7 @@
onclick={handleClick}
onkeydown={handleKeyDown}
role={clickable ? 'button' : undefined}
tabindex={clickable ? 0 : -1}
tabindex={clickable ? 0 : undefined}
>
<!-- Color indicator dot -->
<div class="h-2 w-2 rounded-full" style="background-color: {tagColor}"></div>