-
Cards
-
- Karteikarten-App des Vereins mana e.V.
-
+
{t('app.name')}
+
{t('landing.welcome')}
+
{t('landing.intro')}
{#if !devUser.id}
-
Phase 0 — Dev-Login
-
- Bevor mana-auth-Föderation steht (Phase 2), schaltet ein Dev-Stub den Login frei.
- Trag eine User-ID ein (z.B. u-test-1).
-
+
{t('landing.cta_login')}
diff --git a/apps/web/src/routes/account/+page.svelte b/apps/web/src/routes/account/+page.svelte
index 71d39cf..60cd1ec 100644
--- a/apps/web/src/routes/account/+page.svelte
+++ b/apps/web/src/routes/account/+page.svelte
@@ -3,25 +3,14 @@
import { goto } from '$app/navigation';
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import { exportMe, deleteMe } from '$lib/api/me.ts';
- import { listDecks } from '$lib/api/decks.ts';
import { toasts } from '$lib/stores/toasts.svelte.ts';
+ import { t } from '$lib/i18n/index.svelte.ts';
- let stats = $state<{ decks: number; cards: number } | null>(null);
let exporting = $state(false);
let deleting = $state(false);
- onMount(async () => {
- if (!devUser.id) {
- goto('/');
- return;
- }
- try {
- const r = await listDecks();
- const cards = r.decks.reduce((sum) => sum, 0); // listDecks returns total decks; cards via separate calls
- stats = { decks: r.total, cards };
- } catch {
- stats = null;
- }
+ onMount(() => {
+ if (!devUser.id) goto('/');
});
async function onExport() {
@@ -38,28 +27,30 @@
a.remove();
URL.revokeObjectURL(url);
toasts.success(
- `Export geladen: ${data.data.decks.length} Decks, ${data.data.cards.length} Karten, ${data.data.reviews.length} Reviews.`
+ t('account.export_done', {
+ decks: data.data.decks.length,
+ cards: data.data.cards.length,
+ reviews: data.data.reviews.length,
+ })
);
} catch (e) {
- toasts.error(`Export fehlgeschlagen: ${(e as Error).message}`);
+ toasts.error(t('account.export_failed', { msg: (e as Error).message }));
} finally {
exporting = false;
}
}
async function onDelete() {
- const confirmation = prompt(
- 'ALLE deine Cards-Daten werden unwiderruflich gelöscht. Tippe LÖSCHEN zur Bestätigung.'
- );
- if (confirmation !== 'LÖSCHEN') return;
+ const confirmation = prompt(t('account.delete_confirm'));
+ if (confirmation !== t('account.delete_confirm_word')) return;
deleting = true;
try {
const r = await deleteMe();
- toasts.success(`Gelöscht: ${r.counts.decks} Decks, ${r.counts.import_jobs} Import-Jobs.`);
+ toasts.success(t('account.delete_done', { decks: r.counts.decks, imports: r.counts.import_jobs }));
devUser.clear();
goto('/');
} catch (e) {
- toasts.error(`Löschen fehlgeschlagen: ${(e as Error).message}`);
+ toasts.error(t('account.delete_failed', { msg: (e as Error).message }));
deleting = false;
}
}
@@ -71,15 +62,15 @@
- Account · Cards
+ {t('account.title')} · {t('app.title_suffix')}
-
Account
+
{t('account.title')}
-
User-ID
+
{t('account.user_id_label')}
{devUser.id ?? '—'}
@@ -88,45 +79,35 @@
onclick={logout}
class="rounded border border-[var(--color-border)] px-3 py-1.5 text-sm hover:bg-[var(--color-border)]/40"
>
- Abmelden
+ {t('account.logout')}
-
- Phase-2-Hinweis: aktuell ist die Identität ein Dev-Stub (sessionStorage). Mit Auth-Föderation
- wechselt das auf einen mana-auth-Login gegen auth.mana.how.
-
+ {t('account.phase2_hint')}
- Daten-Export
-
- Lade alle deine Cards-Daten als JSON herunter — Decks, Karten, Reviews, Study-Sessions, Tags,
- Media-Refs, Import-Jobs. DSGVO Art. 15/20.
-
+ {t('account.export_title')}
+ {t('account.export_intro')}
- Konto löschen
-
- Löscht unwiderruflich alle deine Cards-Daten. DSGVO Art. 17. Andere mana-Apps (Memoro, Who, …)
- behalten ihre Daten unabhängig — wenn du dort auch löschen willst, mach das jeweils dort oder
- nutze die Verein-DSGVO-Sammelanfrage über mana-admin.
-
+ {t('account.delete_title')}
+ {t('account.delete_intro')}
diff --git a/apps/web/src/routes/cards/[id]/edit/+page.svelte b/apps/web/src/routes/cards/[id]/edit/+page.svelte
index ea10954..bb10b26 100644
--- a/apps/web/src/routes/cards/[id]/edit/+page.svelte
+++ b/apps/web/src/routes/cards/[id]/edit/+page.svelte
@@ -12,6 +12,7 @@
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import { renderMarkdown } from '$lib/markdown.ts';
import { toasts } from '$lib/stores/toasts.svelte.ts';
+ import { t } from '$lib/i18n/index.svelte.ts';
let card = $state
(null);
let cardType = $state('basic');
@@ -78,76 +79,71 @@
: { text: text.trim() }
: { front: front.trim(), back: back.trim() };
const updated = await updateCard(card.id, { fields });
- toasts.success('Karte aktualisiert');
+ toasts.success(t('card_edit.updated'));
goto(`/decks/${updated.deck_id}`);
} catch (e) {
- toasts.error(`Speichern fehlgeschlagen: ${(e as Error).message}`);
+ toasts.error(t('card_edit.save_failed', { msg: (e as Error).message }));
saving = false;
}
}
async function onDelete() {
if (!card) return;
- if (!confirm('Karte wirklich löschen? Reviews werden mit gelöscht.')) return;
+ if (!confirm(t('card_edit.delete_confirm'))) return;
try {
await deleteCard(card.id);
- toasts.success('Karte gelöscht');
+ toasts.success(t('card_edit.deleted'));
goto(`/decks/${card.deck_id}`);
} catch (e) {
- toasts.error(`Löschen fehlgeschlagen: ${(e as Error).message}`);
+ toasts.error(t('card_edit.delete_failed', { msg: (e as Error).message }));
}
}
- Karte bearbeiten · Cards
+ {t('card_edit.title')} · {t('app.title_suffix')}
{#if loading}
-
Lade…
+
{t('decks.loading')}
{:else if error}
-
Fehler: {error}
+
{t('decks.error', { msg: error })}
{:else if card}
← Zurück zum Deck{t('card_edit.back')}
-
Karte bearbeiten
+ {t('card_edit.title')}
{cardType}
-
- Der Card-Type kann nicht geändert werden — die Reviews-Tabelle hängt am Type.
-
+
{t('card_edit.type_locked_help')}
diff --git a/apps/web/src/routes/cards/new/+page.svelte b/apps/web/src/routes/cards/new/+page.svelte
index 4ff7346..cb1cde7 100644
--- a/apps/web/src/routes/cards/new/+page.svelte
+++ b/apps/web/src/routes/cards/new/+page.svelte
@@ -12,6 +12,7 @@
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import { renderMarkdown } from '$lib/markdown.ts';
import { toasts } from '$lib/stores/toasts.svelte.ts';
+ import { t } from '$lib/i18n/index.svelte.ts';
type DeckLite = { id: string; name: string };
@@ -52,7 +53,7 @@
}
}
} catch (e) {
- toasts.error(`Decks konnten nicht geladen werden: ${(e as Error).message}`);
+ toasts.error(t('card_new.decks_load_failed', { msg: (e as Error).message }));
}
});
@@ -78,28 +79,28 @@
const card = await createCard({ deck_id: deckId, type: cardType, fields });
const msg =
cardType === 'cloze'
- ? `${clusterIds.length} Reviews initialisiert (1 pro Cluster)`
+ ? t('card_new.toast_cloze', { n: clusterIds.length })
: cardType === 'basic-reverse'
- ? '2 Reviews initialisiert (front→back, back→front)'
- : 'Karte angelegt';
+ ? t('card_new.toast_basic_reverse')
+ : t('card_new.toast_basic');
toasts.success(msg);
goto(`/decks/${card.deck_id}`);
} catch (e) {
- toasts.error(`Anlegen fehlgeschlagen: ${(e as Error).message}`);
+ toasts.error(t('card_new.create_failed', { msg: (e as Error).message }));
saving = false;
}
}
← Zurück{t('card_new.back')}
-
Neue Karte
+
{t('card_new.title')}
diff --git a/apps/web/src/routes/import/+page.svelte b/apps/web/src/routes/import/+page.svelte
index e00d086..3c18e69 100644
--- a/apps/web/src/routes/import/+page.svelte
+++ b/apps/web/src/routes/import/+page.svelte
@@ -3,6 +3,7 @@
import { goto } from '$app/navigation';
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import AnkiImport from '$lib/components/AnkiImport.svelte';
+ import { t } from '$lib/i18n/index.svelte.ts';
onMount(() => {
if (!devUser.id) {
@@ -12,32 +13,29 @@
- Import · Cards
+ {t('import.title')} · {t('app.title_suffix')}
-
Importieren
-
- Übernimm Decks und Karten aus einer Anki-Datei (.apkg oder .colpkg).
- FSRS-Verlauf wird nicht übernommen — alle Karten starten als „neu".
-
+
{t('import.title')}
+
{t('import.intro')}
diff --git a/apps/web/src/routes/stats/+page.svelte b/apps/web/src/routes/stats/+page.svelte
index d8545be..b2513a3 100644
--- a/apps/web/src/routes/stats/+page.svelte
+++ b/apps/web/src/routes/stats/+page.svelte
@@ -3,6 +3,7 @@
import { goto } from '$app/navigation';
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import { loadStats, type UserStats } from '$lib/api/me.ts';
+ import { i18n, t, tn } from '$lib/i18n/index.svelte.ts';
let stats = $state
(null);
let loading = $state(true);
@@ -34,61 +35,71 @@
function dayLabel(iso: string): string {
const d = new Date(`${iso}T00:00:00Z`);
- return d.toLocaleDateString('de-DE', { weekday: 'short' });
+ const lang = i18n.current === 'en' ? 'en-US' : 'de-DE';
+ return d.toLocaleDateString(lang, { weekday: 'short' });
+ }
+
+ function fullDate(iso: string): string {
+ const d = new Date(iso);
+ const lang = i18n.current === 'en' ? 'en-US' : 'de-DE';
+ return d.toLocaleString(lang);
}
- Statistik · Cards
+ {t('stats.title')} · {t('app.title_suffix')}
-
Statistik
+
{t('stats.title')}
{#if loading}
-
Lade…
+
{t('stats.loading')}
{:else if error}
-
Fehler: {error}
+
{t('stats.error', { msg: error })}
{:else if stats}
- Stand {new Date(stats.generated_at).toLocaleString('de-DE')}
+ {t('stats.generated_at', { date: fullDate(stats.generated_at) })}
-
Decks
+
{t('stats.decks')}
{stats.total_decks}
-
Karten
+
{t('stats.cards')}
{stats.total_cards}
-
Reviews
+
{t('stats.reviews')}
{stats.total_reviews}
-
Fällig jetzt
+
{t('stats.due_now')}
{stats.due_now}
-
Lerntage
+ {t('stats.days_title')}
- Streak: {stats.streak_days}
- {stats.streak_days === 1 ? 'Tag' : 'Tage'} · letzte 7 Tage:
- {reviewedTotal7} Reviews
+ {tn('stats.streak', stats.streak_days, { total: reviewedTotal7 })}
-
+
{#each stats.reviewed_per_day as d (d.day)}
-
+
{d.n || ''}
{dayLabel(d.day)}
@@ -97,25 +108,23 @@
- FSRS-Status
-
- Verteilung deiner Karten-Reviews über die FSRS-Zustände.
-
+ {t('stats.fsrs_title')}
+ {t('stats.fsrs_intro')}
-
- Neu
+ - {t('stats.fsrs_new')}
- {stats.state_counts.new}
-
- Lernend
+ - {t('stats.fsrs_learning')}
- {stats.state_counts.learning}
-
- Review
+ - {t('stats.fsrs_review')}
- {stats.state_counts.review}
-
- Relearning
+ - {t('stats.fsrs_relearning')}
- {stats.state_counts.relearning}
diff --git a/apps/web/src/routes/study/+page.svelte b/apps/web/src/routes/study/+page.svelte
index d062be2..9e23d56 100644
--- a/apps/web/src/routes/study/+page.svelte
+++ b/apps/web/src/routes/study/+page.svelte
@@ -6,6 +6,7 @@
import { listDueReviews } from '$lib/api/reviews.ts';
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import InboxBanner from '$lib/components/InboxBanner.svelte';
+ import { t } from '$lib/i18n/index.svelte.ts';
type Item = { deck: Deck; due: number };
@@ -36,14 +37,14 @@
});
-Lernen
+{t('study.title')}
{#if loading}
- Lade…
+ {t('study_session.loading')}
{:else}
{#each items as it (it.deck.id)}
@@ -52,11 +53,15 @@
>
{#if it.deck.color}
-
+
{/if}
{it.deck.name}
- {it.due} fällig
+ {t('study.due_count', { n: it.due })}
{#if it.due > 0}
@@ -64,10 +69,10 @@
href="/study/{it.deck.id}"
class="rounded bg-[var(--color-primary)] px-3 py-1.5 text-sm text-[var(--color-primary-fg)]"
>
- Starten
+ {t('study.study_now')}
{:else}
- —
+ —
{/if}
{/each}
diff --git a/apps/web/src/routes/study/[deckId]/+page.svelte b/apps/web/src/routes/study/[deckId]/+page.svelte
index 036664b..59c4d77 100644
--- a/apps/web/src/routes/study/[deckId]/+page.svelte
+++ b/apps/web/src/routes/study/[deckId]/+page.svelte
@@ -13,6 +13,7 @@
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import { renderMarkdown } from '$lib/markdown.ts';
import { toasts } from '$lib/stores/toasts.svelte.ts';
+ import { t } from '$lib/i18n/index.svelte.ts';
const deckId = $derived(page.params.deckId ?? '');
@@ -129,7 +130,7 @@
queueIndex += 1;
revealed = false;
} catch (e) {
- toasts.error(`Speichern fehlgeschlagen: ${(e as Error).message}`);
+ toasts.error(t('card_edit.save_failed', { msg: (e as Error).message }));
} finally {
busy = false;
}
@@ -137,45 +138,51 @@
-
← Lernen
+
{t('study_session.back')}
{deckName}
-
+
{#if !loading && !isDone}
{queueIndex + 1} / {queue.length}
{/if}
{#if loading}
-
Lade Sitzung…
+
{t('study_session.loading')}
{:else if queue.length === 0}
-
Keine Karten fällig in diesem Deck. 🎉
+
{t('study.none_due')} 🎉
- Zurück zum Deck →
+ {t('card_edit.back')}
{:else if isDone}
-
Sitzung abgeschlossen
+
{t('study_session.all_done')}
- {stats.reviewed} Reviews erledigt · {stats.again}× nochmal
+ {t('study_session.stats', { reviewed: stats.reviewed, again: stats.again })}
{:else}
+
+ {revealed ? t('card_new.preview_label') : t('study_session.reveal')}
+
{@html promptHtml}
{#if revealed}
@@ -190,17 +197,17 @@
onclick={() => (revealed = true)}
class="rounded bg-[var(--color-primary)] px-6 py-3 text-sm text-[var(--color-primary-fg)]"
>
- Antwort zeigen Space
+ {t('study_session.reveal')} Space
{:else}
-
+
{/if}
- Hotkeys: Space/Enter = aufdecken & gut · 1–4 = bewerten
+ {t('study_session.grade_hint')}
{/if}