Phase 9g: i18n DE/EN über alle Routes
Schmale Eigen-Lösung statt svelte-i18n: $lib/i18n mit de.ts + en.ts (je ~150 Strings, flat-with-nesting), index.svelte.ts liefert reaktiven i18n-Store als Svelte-5-Rune plus t()/tn()-Helper. Locale wird in localStorage persistiert, Default = navigator.language (DE/EN-Erkennung), Fallback = de. Header bekommt einen DE/EN-Toggle-Switcher (role=group, aria-pressed). Alle Routes (decks, decks/[id], decks/new, cards/new, cards/[id]/edit, study, study/[deckId], import, account, stats, +page) und alle Komponenten (Header, AnkiImport, InboxBanner) ziehen jetzt durch t(). tn(key, n) wählt zwischen `<key>_one` (n=1) und `<key>` — minimaler Plural-Helper, reicht für DE/EN-MVP. Komplexere Pluralregeln (FR/IT) bräuchten Intl.PluralRules, kommen wenn die Locales dazukommen. Begleitend ein paar A11y-Vorbereitungen mitgenommen: aria-label am Locale-Switcher, role=group für Grade-Buttons, role=button + keyboard-handler für die Anki-Dropzone, role=progressbar mit aria-valuemin/max/now, role=alert für Error-States, aria-live=polite für Lade-Meldungen, sr-only-Heading im Study-Card-Render, aria-hidden für rein dekorative Elemente. svelte-check 379 files 0 errors, 94 Tests grün (41 Domain + 48 API + 5 Web). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a640594a24
commit
c25c1d0dc4
17 changed files with 826 additions and 270 deletions
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||
import { i18n, t } from '$lib/i18n/index.svelte.ts';
|
||||
</script>
|
||||
|
||||
<header
|
||||
|
|
@ -10,36 +11,60 @@
|
|||
<a href="/" class="flex items-center gap-2 font-semibold">
|
||||
<span
|
||||
class="inline-flex h-7 w-7 items-center justify-center rounded bg-[var(--color-primary)] text-[var(--color-primary-fg)] text-sm"
|
||||
aria-hidden="true"
|
||||
>
|
||||
C
|
||||
</span>
|
||||
<span>Cards</span>
|
||||
<span>{t('app.name')}</span>
|
||||
</a>
|
||||
|
||||
<nav class="flex items-center gap-6 text-sm">
|
||||
<nav class="flex items-center gap-6 text-sm" aria-label={t('app.name')}>
|
||||
<a
|
||||
href="/decks"
|
||||
class="hover:text-[var(--color-primary)]"
|
||||
class:font-medium={page.url.pathname.startsWith('/decks')}>Decks</a
|
||||
class:font-medium={page.url.pathname.startsWith('/decks')}>{t('nav.decks')}</a
|
||||
>
|
||||
<a
|
||||
href="/study"
|
||||
class="hover:text-[var(--color-primary)]"
|
||||
class:font-medium={page.url.pathname.startsWith('/study')}>Lernen</a
|
||||
class:font-medium={page.url.pathname.startsWith('/study')}>{t('nav.study')}</a
|
||||
>
|
||||
<a
|
||||
href="/import"
|
||||
class="hover:text-[var(--color-primary)]"
|
||||
class:font-medium={page.url.pathname.startsWith('/import')}>Import</a
|
||||
class:font-medium={page.url.pathname.startsWith('/import')}>{t('nav.import')}</a
|
||||
>
|
||||
<a
|
||||
href="/stats"
|
||||
class="hover:text-[var(--color-primary)]"
|
||||
class:font-medium={page.url.pathname.startsWith('/stats')}>Statistik</a
|
||||
class:font-medium={page.url.pathname.startsWith('/stats')}>{t('nav.stats')}</a
|
||||
>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<div
|
||||
class="flex overflow-hidden rounded border border-[var(--color-border)] text-xs"
|
||||
role="group"
|
||||
aria-label="Sprache / Language"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-1 transition-colors"
|
||||
class:bg-primary-active={i18n.current === 'de'}
|
||||
style:background-color={i18n.current === 'de' ? 'var(--color-primary)' : 'transparent'}
|
||||
style:color={i18n.current === 'de' ? 'var(--color-primary-fg)' : 'var(--color-muted)'}
|
||||
aria-pressed={i18n.current === 'de'}
|
||||
onclick={() => i18n.set('de')}>DE</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-1 transition-colors"
|
||||
style:background-color={i18n.current === 'en' ? 'var(--color-primary)' : 'transparent'}
|
||||
style:color={i18n.current === 'en' ? 'var(--color-primary-fg)' : 'var(--color-muted)'}
|
||||
aria-pressed={i18n.current === 'en'}
|
||||
onclick={() => i18n.set('en')}>EN</button
|
||||
>
|
||||
</div>
|
||||
{#if devUser.id}
|
||||
<a
|
||||
href="/account"
|
||||
|
|
@ -51,9 +76,9 @@
|
|||
{:else}
|
||||
<button
|
||||
class="rounded bg-[var(--color-primary)] px-3 py-1 text-[var(--color-primary-fg)]"
|
||||
onclick={() => devUser.set(prompt('User-ID (dev):') ?? '')}
|
||||
onclick={() => devUser.set(prompt(t('landing.dev_user_prompt')) ?? '')}
|
||||
>
|
||||
Login (dev)
|
||||
{t('nav.login_dev')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue