feat: enhance accessibility and testing with data-test attributes and improve error handling
Some checks failed
Workflow / ⚫️ Test (push) Has been cancelled
Workflow / ʦ TypeScript (push) Has been cancelled

This commit is contained in:
T. Zehetbauer
2026-04-01 10:46:44 +02:00
parent 3bcc5c70a3
commit abac22feb1
55 changed files with 1622 additions and 128 deletions

View File

@@ -39,7 +39,7 @@ export default async function GuestsPage({ params }: PageProps) {
<div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between">
<p className="text-muted-foreground">Gästeverwaltung</p>
<Button>
<Button data-test="guests-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neuer Gast
</Button>

View File

@@ -90,9 +90,9 @@ export default async function BookingsPage({
// Post-filter by search query (guest name or room name/number)
if (searchQuery) {
const q = searchQuery.toLowerCase();
bookingsData = bookingsData.filter((b) => {
const room = b.room as Record<string, string> | null;
const guest = b.guest as Record<string, string> | null;
bookingsData = bookingsData.filter((booking) => {
const room = booking.room as Record<string, string> | null;
const guest = booking.guest as Record<string, string> | null;
const roomName = (room?.name ?? '').toLowerCase();
const roomNumber = (room?.room_number ?? '').toLowerCase();
const guestFirst = (guest?.first_name ?? '').toLowerCase();
@@ -107,7 +107,8 @@ export default async function BookingsPage({
}
const activeBookings = bookingsData.filter(
(b) => b.status === 'confirmed' || b.status === 'checked_in',
(booking) =>
booking.status === 'confirmed' || booking.status === 'checked_in',
);
const totalPages = Math.ceil(total / PAGE_SIZE);
@@ -122,7 +123,7 @@ export default async function BookingsPage({
</p>
<Link href={`/home/${account}/bookings/new`}>
<Button>
<Button data-test="bookings-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neue Buchung
</Button>

View File

@@ -40,7 +40,7 @@ export default async function RoomsPage({ params }: PageProps) {
<div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between">
<p className="text-muted-foreground">Zimmerverwaltung</p>
<Button>
<Button data-test="rooms-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neues Zimmer
</Button>

View File

@@ -6,6 +6,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Badge } from '@kit/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state';
@@ -29,7 +30,7 @@ export default async function AttendancePage({
api.getParticipants(courseId),
]);
if (!course) return <div>Kurs nicht gefunden</div>;
if (!course) return <AccountNotFound />;
const selectedSessionId =
(search.session as string) ??

View File

@@ -16,6 +16,7 @@ import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
interface PageProps {
@@ -52,12 +53,12 @@ export default async function CourseDetailPage({ params }: PageProps) {
api.getSessions(courseId),
]);
if (!course) return <div>Kurs nicht gefunden</div>;
if (!course) return <AccountNotFound />;
const c = course as Record<string, unknown>;
const courseData = course as Record<string, unknown>;
return (
<CmsPageShell account={account} title={String(c.name)}>
<CmsPageShell account={account} title={String(courseData.name)}>
<div className="flex w-full flex-col gap-6">
{/* Summary Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
@@ -66,7 +67,7 @@ export default async function CourseDetailPage({ params }: PageProps) {
<GraduationCap className="text-primary h-5 w-5" />
<div>
<p className="text-muted-foreground text-xs">Name</p>
<p className="font-semibold">{String(c.name)}</p>
<p className="font-semibold">{String(courseData.name)}</p>
</div>
</CardContent>
</Card>
@@ -76,9 +77,12 @@ export default async function CourseDetailPage({ params }: PageProps) {
<div>
<p className="text-muted-foreground text-xs">Status</p>
<Badge
variant={STATUS_VARIANT[String(c.status)] ?? 'secondary'}
variant={
STATUS_VARIANT[String(courseData.status)] ?? 'secondary'
}
>
{STATUS_LABEL[String(c.status)] ?? String(c.status)}
{STATUS_LABEL[String(courseData.status)] ??
String(courseData.status)}
</Badge>
</div>
</CardContent>
@@ -89,7 +93,7 @@ export default async function CourseDetailPage({ params }: PageProps) {
<div>
<p className="text-muted-foreground text-xs">Dozent</p>
<p className="font-semibold">
{String(c.instructor_id ?? '—')}
{String(courseData.instructor_id ?? '—')}
</p>
</div>
</CardContent>
@@ -100,9 +104,9 @@ export default async function CourseDetailPage({ params }: PageProps) {
<div>
<p className="text-muted-foreground text-xs">Beginn Ende</p>
<p className="font-semibold">
{formatDate(c.start_date as string)}
{formatDate(courseData.start_date as string)}
{' '}
{formatDate(c.end_date as string)}
{formatDate(courseData.end_date as string)}
</p>
</div>
</CardContent>
@@ -113,7 +117,9 @@ export default async function CourseDetailPage({ params }: PageProps) {
<div>
<p className="text-muted-foreground text-xs">Gebühr</p>
<p className="font-semibold">
{c.fee != null ? `${Number(c.fee).toFixed(2)}` : '—'}
{courseData.fee != null
? `${Number(courseData.fee).toFixed(2)}`
: '—'}
</p>
</div>
</CardContent>
@@ -124,7 +130,7 @@ export default async function CourseDetailPage({ params }: PageProps) {
<div>
<p className="text-muted-foreground text-xs">Teilnehmer</p>
<p className="font-semibold">
{participants.length} / {String(c.capacity ?? '∞')}
{participants.length} / {String(courseData.capacity ?? '∞')}
</p>
</div>
</CardContent>

View File

@@ -9,6 +9,7 @@ import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state';
@@ -43,7 +44,7 @@ export default async function ParticipantsPage({ params }: PageProps) {
api.getParticipants(courseId),
]);
if (!course) return <div>Kurs nicht gefunden</div>;
if (!course) return <AccountNotFound />;
return (
<CmsPageShell account={account} title="Teilnehmer">
@@ -56,7 +57,7 @@ export default async function ParticipantsPage({ params }: PageProps) {
{participants.length} Teilnehmer
</p>
</div>
<Button>
<Button data-test="participants-add-btn">
<Plus className="mr-2 h-4 w-4" />
Teilnehmer anmelden
</Button>

View File

@@ -67,10 +67,14 @@ export default async function CourseCalendarPage({ params }: PageProps) {
const courseDates = new Set<number>();
for (const course of courses.data) {
const c = course as Record<string, unknown>;
if (c.status === 'cancelled') continue;
const startDate = c.start_date ? new Date(String(c.start_date)) : null;
const endDate = c.end_date ? new Date(String(c.end_date)) : null;
const courseItem = course as Record<string, unknown>;
if (courseItem.status === 'cancelled') continue;
const startDate = courseItem.start_date
? new Date(String(courseItem.start_date))
: null;
const endDate = courseItem.end_date
? new Date(String(courseItem.end_date))
: null;
if (!startDate) continue;
@@ -112,8 +116,8 @@ export default async function CourseCalendarPage({ params }: PageProps) {
}
const activeCourses = courses.data.filter(
(c: Record<string, unknown>) =>
c.status === 'open' || c.status === 'running',
(courseItem: Record<string, unknown>) =>
courseItem.status === 'open' || courseItem.status === 'running',
);
return (

View File

@@ -33,7 +33,7 @@ export default async function CategoriesPage({ params }: PageProps) {
<div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between">
<p className="text-muted-foreground">Kurskategorien verwalten</p>
<Button>
<Button data-test="categories-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neue Kategorie
</Button>

View File

@@ -33,7 +33,7 @@ export default async function InstructorsPage({ params }: PageProps) {
<div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between">
<p className="text-muted-foreground">Dozentenpool verwalten</p>
<Button>
<Button data-test="instructors-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neuer Dozent
</Button>

View File

@@ -35,7 +35,7 @@ export default async function LocationsPage({ params }: PageProps) {
<p className="text-muted-foreground">
Kurs- und Veranstaltungsorte verwalten
</p>
<Button>
<Button data-test="locations-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neuer Ort
</Button>

View File

@@ -64,7 +64,7 @@ export default async function CoursesPage({ params, searchParams }: PageProps) {
<p className="text-muted-foreground">Kursangebot verwalten</p>
<Link href={`/home/${account}/courses/new`}>
<Button>
<Button data-test="courses-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neuer Kurs
</Button>

View File

@@ -68,6 +68,7 @@ export function GenerateDocumentForm({ accountSlug, initialType }: Props) {
<select
id="documentType"
name="documentType"
data-test="document-type-select"
value={selectedType}
onChange={(e) => {
setSelectedType(e.target.value);
@@ -190,7 +191,11 @@ export function GenerateDocumentForm({ accountSlug, initialType }: Props) {
{/* Submit button */}
<div className="flex justify-end">
<Button type="submit" disabled={isPending || isComingSoon}>
<Button
type="submit"
data-test="document-generate-btn"
disabled={isPending || isComingSoon}
>
{isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />

View File

@@ -259,10 +259,10 @@ async function generateMemberCards(
orientation: input.orientation,
style: s.page,
},
...batch.map((m) =>
...batch.map((memberItem) =>
React.createElement(
View,
{ key: m.id, style: s.card },
{ key: memberItem.id, style: s.card },
// Accent bar
React.createElement(View, { style: s.accentBar }),
@@ -298,7 +298,7 @@ async function generateMemberCards(
React.createElement(
Text,
{ style: s.memberNumber },
`Nr. ${m.member_number ?? ''}`,
`Nr. ${memberItem.member_number ?? ''}`,
),
),
@@ -309,7 +309,7 @@ async function generateMemberCards(
React.createElement(
Text,
{ style: s.memberName },
`${m.first_name} ${m.last_name}`,
`${memberItem.first_name} ${memberItem.last_name}`,
),
React.createElement(
View,
@@ -326,7 +326,7 @@ async function generateMemberCards(
React.createElement(
Text,
{ style: s.fieldValue },
fmtDate(m.entry_date),
fmtDate(memberItem.entry_date),
),
),
// Date of birth
@@ -341,7 +341,7 @@ async function generateMemberCards(
React.createElement(
Text,
{ style: s.fieldValue },
fmtDate(m.date_of_birth),
fmtDate(memberItem.date_of_birth),
),
),
// Address
@@ -356,13 +356,16 @@ async function generateMemberCards(
React.createElement(
Text,
{ style: s.fieldValue },
[m.street, m.house_number].filter(Boolean).join(' ') ||
'',
[memberItem.street, memberItem.house_number]
.filter(Boolean)
.join(' ') || '',
),
React.createElement(
Text,
{ style: { ...s.fieldValue, marginTop: 1 } },
[m.postal_code, m.city].filter(Boolean).join(' ') || '',
[memberItem.postal_code, memberItem.city]
.filter(Boolean)
.join(' ') || '',
),
),
),
@@ -428,12 +431,20 @@ async function generateLabels(
return { success: false, error: 'Keine aktiven Mitglieder.' };
const api = createDocumentGeneratorApi();
const records = members.map((m) => ({
line1: [m.salutation, m.title, m.first_name, m.last_name]
const records = members.map((record) => ({
line1: [
record.salutation,
record.title,
record.first_name,
record.last_name,
]
.filter(Boolean)
.join(' '),
line2: [m.street, m.house_number].filter(Boolean).join(' ') || undefined,
line3: [m.postal_code, m.city].filter(Boolean).join(' ') || undefined,
line2:
[record.street, record.house_number].filter(Boolean).join(' ') ||
undefined,
line3:
[record.postal_code, record.city].filter(Boolean).join(' ') || undefined,
}));
const html = api.generateLabelsHtml({ labelFormat: 'avery-l7163', records });
@@ -500,16 +511,16 @@ async function generateMemberReport(
excluded: 'Ausgeschlossen',
};
for (const m of members) {
for (const member of members) {
ws.addRow({
nr: m.member_number ?? '',
name: m.last_name,
vorname: m.first_name,
email: m.email ?? '',
plz: m.postal_code ?? '',
ort: m.city ?? '',
status: SL[m.status] ?? m.status,
eintritt: m.entry_date ? formatDate(m.entry_date) : '',
nr: member.member_number ?? '',
name: member.last_name,
vorname: member.first_name,
email: member.email ?? '',
plz: member.postal_code ?? '',
ort: member.city ?? '',
status: SL[member.status] ?? member.status,
eintritt: member.entry_date ? formatDate(member.entry_date) : '',
});
}

View File

@@ -46,7 +46,7 @@ export default async function DocumentTemplatesPage({ params }: PageProps) {
</p>
</div>
<Button>
<Button data-test="document-templates-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neue Vorlage
</Button>

View File

@@ -56,20 +56,21 @@ export default async function EventDetailPage({ params }: PageProps) {
if (!event) return <div>Veranstaltung nicht gefunden</div>;
const e = event as Record<string, unknown>;
const eventData = event as Record<string, unknown>;
return (
<CmsPageShell account={account} title={String(e.name)}>
<CmsPageShell account={account} title={String(eventData.name)}>
<div className="flex w-full flex-col gap-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">{String(e.name)}</h1>
<h1 className="text-2xl font-bold">{String(eventData.name)}</h1>
<Badge
variant={STATUS_VARIANT[String(e.status)] ?? 'secondary'}
variant={STATUS_VARIANT[String(eventData.status)] ?? 'secondary'}
className="mt-1"
>
{STATUS_LABEL[String(e.status)] ?? String(e.status)}
{STATUS_LABEL[String(eventData.status)] ??
String(eventData.status)}
</Badge>
</div>
<Button>
@@ -86,7 +87,7 @@ export default async function EventDetailPage({ params }: PageProps) {
<div>
<p className="text-muted-foreground text-xs">Datum</p>
<p className="font-semibold">
{formatDate(e.event_date as string)}
{formatDate(eventData.event_date as string)}
</p>
</div>
</CardContent>
@@ -97,7 +98,8 @@ export default async function EventDetailPage({ params }: PageProps) {
<div>
<p className="text-muted-foreground text-xs">Uhrzeit</p>
<p className="font-semibold">
{String(e.start_time ?? '—')} {String(e.end_time ?? '—')}
{String(eventData.start_time ?? '—')} {' '}
{String(eventData.end_time ?? '—')}
</p>
</div>
</CardContent>
@@ -107,7 +109,9 @@ export default async function EventDetailPage({ params }: PageProps) {
<MapPin className="text-primary h-5 w-5" />
<div>
<p className="text-muted-foreground text-xs">Ort</p>
<p className="font-semibold">{String(e.location ?? '—')}</p>
<p className="font-semibold">
{String(eventData.location ?? '—')}
</p>
</div>
</CardContent>
</Card>
@@ -117,7 +121,7 @@ export default async function EventDetailPage({ params }: PageProps) {
<div>
<p className="text-muted-foreground text-xs">Anmeldungen</p>
<p className="font-semibold">
{registrations.length} / {String(e.capacity ?? '∞')}
{registrations.length} / {String(eventData.capacity ?? '∞')}
</p>
</div>
</CardContent>
@@ -125,14 +129,14 @@ export default async function EventDetailPage({ params }: PageProps) {
</div>
{/* Description */}
{e.description ? (
{eventData.description ? (
<Card>
<CardHeader>
<CardTitle>Beschreibung</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm whitespace-pre-wrap">
{String(e.description)}
{String(eventData.description)}
</p>
</CardContent>
</Card>

View File

@@ -47,19 +47,21 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
const events = await api.listEvents(acct.id, { page });
// Fetch registration counts for all events on this page
const eventIds = events.data.map((e: Record<string, unknown>) =>
String(e.id),
const eventIds = events.data.map((eventItem: Record<string, unknown>) =>
String(eventItem.id),
);
const registrationCounts = await api.getRegistrationCounts(eventIds);
// Pre-compute stats before rendering
const uniqueLocationCount = new Set(
events.data.map((e: Record<string, unknown>) => e.location).filter(Boolean),
events.data
.map((eventItem: Record<string, unknown>) => eventItem.location)
.filter(Boolean),
).size;
const totalCapacity = events.data.reduce(
(sum: number, e: Record<string, unknown>) =>
sum + (Number(e.capacity) || 0),
(sum: number, eventItem: Record<string, unknown>) =>
sum + (Number(eventItem.capacity) || 0),
0,
);
@@ -74,7 +76,7 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
</div>
<Link href={`/home/${account}/events/new`}>
<Button>
<Button data-test="events-new-btn">
<Plus className="mr-2 h-4 w-4" />
{t('newEvent')}
</Button>

View File

@@ -55,7 +55,7 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
const api = createFinanceApi(client);
const invoice = await api.getInvoiceWithItems(id);
if (!invoice) return <div>Rechnung nicht gefunden</div>;
if (!invoice) return <AccountNotFound />;
const status = String(invoice.status);
const items = (invoice.items ?? []) as Array<Record<string, unknown>>;

View File

@@ -74,7 +74,7 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
api.getBatchItems(batchId),
]);
if (!batch) return <div>Einzug nicht gefunden</div>;
if (!batch) return <AccountNotFound />;
const status = String(batch.status);

View File

@@ -41,7 +41,7 @@ export default async function NewsletterDetailPage({ params }: PageProps) {
api.getRecipients(campaignId),
]);
if (!newsletter) return <div>Newsletter nicht gefunden</div>;
if (!newsletter) return <AccountNotFound />;
const status = String(newsletter.status);
const sentCount = recipients.filter(
@@ -60,6 +60,7 @@ export default async function NewsletterDetailPage({ params }: PageProps) {
<Link
href={`/home/${account}/newsletter`}
className="text-muted-foreground hover:text-foreground inline-flex items-center text-sm"
data-test="newsletter-back-link"
>
<ArrowLeft className="mr-1 h-4 w-4" />
Zurück zu Newsletter
@@ -98,7 +99,7 @@ export default async function NewsletterDetailPage({ params }: PageProps) {
{/* Actions */}
{status === 'draft' && (
<div className="mt-6">
<Button>
<Button data-test="newsletter-send-btn">
<Send className="mr-2 h-4 w-4" />
Newsletter versenden
</Button>

View File

@@ -82,7 +82,7 @@ export default async function NewsletterPage({
</div>
<Link href={`/home/${account}/newsletter/new`}>
<Button>
<Button data-test="newsletter-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neuer Newsletter
</Button>

View File

@@ -43,7 +43,7 @@ export default async function NewsletterTemplatesPage({ params }: PageProps) {
</p>
</div>
<Button>
<Button data-test="newsletter-templates-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neue Vorlage
</Button>

View File

@@ -20,7 +20,7 @@ export default async function EditPageRoute({ params }: Props) {
const api = createSiteBuilderApi(client);
const page = await api.getPage(pageId);
if (!page) return <div>Seite nicht gefunden</div>;
if (!page) return <AccountNotFound />;
return (
<SiteEditor

View File

@@ -69,7 +69,7 @@ export default async function SiteBuilderDashboard({ params }: Props) {
)}
</div>
<Link href={`/home/${account}/site-builder/new`}>
<Button>
<Button data-test="site-new-page-btn">
<Plus className="mr-2 h-4 w-4" />
Neue Seite
</Button>

View File

@@ -38,7 +38,7 @@ export default async function PostsManagerPage({ params }: Props) {
>
<div className="space-y-6">
<div className="flex justify-end">
<Button>
<Button data-test="site-new-post-btn">
<Plus className="mr-2 h-4 w-4" />
Neuer Beitrag
</Button>

View File

@@ -0,0 +1,71 @@
{
"nav": {
"bookings": "Buchungen",
"calendar": "Kalender",
"rooms": "Zimmer",
"guests": "Gäste",
"newBooking": "Neue Buchung"
},
"list": {
"title": "Buchungen",
"searchPlaceholder": "Gast oder Zimmer suchen...",
"newBooking": "Neue Buchung",
"noBookings": "Keine Buchungen vorhanden",
"createFirst": "Erstellen Sie Ihre erste Buchung.",
"activeBookings": "Aktive Buchungen",
"guest": "Gast",
"room": "Zimmer",
"checkIn": "Check-in",
"checkOut": "Check-out",
"nights": "Nächte",
"price": "Preis"
},
"detail": {
"notFound": "Buchung nicht gefunden",
"guestInfo": "Gastinformationen",
"roomInfo": "Zimmerinformationen",
"bookingDetails": "Buchungsdetails",
"extras": "Extras"
},
"form": {
"room": "Zimmer *",
"selectRoom": "— Zimmer wählen —",
"guest": "Gast",
"checkIn": "Check-in *",
"checkOut": "Check-out *",
"adults": "Erwachsene",
"children": "Kinder",
"notes": "Notizen",
"created": "Buchung erfolgreich erstellt",
"errorCreating": "Fehler beim Erstellen"
},
"status": {
"pending": "Ausstehend",
"confirmed": "Bestätigt",
"checked_in": "Eingecheckt",
"checked_out": "Ausgecheckt",
"cancelled": "Storniert",
"no_show": "Nicht erschienen"
},
"rooms": {
"title": "Zimmer",
"newRoom": "Neues Zimmer",
"noRooms": "Keine Zimmer vorhanden",
"name": "Name",
"type": "Typ",
"capacity": "Kapazität",
"price": "Preis/Nacht"
},
"guests": {
"title": "Gäste",
"newGuest": "Neuer Gast",
"noGuests": "Keine Gäste vorhanden",
"name": "Name",
"email": "E-Mail",
"phone": "Telefon",
"bookings": "Buchungen"
},
"calendar": {
"title": "Belegungskalender"
}
}

View File

@@ -0,0 +1,120 @@
{
"nav": {
"courses": "Kurse",
"newCourse": "Neuer Kurs",
"calendar": "Kalender",
"categories": "Kategorien",
"instructors": "Kursleiter",
"locations": "Standorte",
"statistics": "Statistiken"
},
"pages": {
"coursesTitle": "Kurse",
"newCourseTitle": "Neuer Kurs",
"calendarTitle": "Kurskalender",
"categoriesTitle": "Kurskategorien",
"instructorsTitle": "Kursleiter",
"locationsTitle": "Standorte",
"statisticsTitle": "Kurs-Statistiken"
},
"list": {
"searchPlaceholder": "Kurs suchen...",
"title": "Kurse ({count})",
"noCourses": "Keine Kurse vorhanden",
"createFirst": "Erstellen Sie Ihren ersten Kurs, um loszulegen.",
"courseNumber": "Kursnr.",
"courseName": "Kursname",
"startDate": "Beginn",
"endDate": "Ende",
"participants": "Teilnehmer",
"fee": "Gebühr"
},
"detail": {
"notFound": "Kurs nicht gefunden",
"participants": "Teilnehmer",
"sessions": "Termine",
"viewAllParticipants": "Alle Teilnehmer anzeigen",
"viewAttendance": "Anwesenheit anzeigen",
"noParticipants": "Noch keine Teilnehmer.",
"noSessions": "Noch keine Termine.",
"addParticipant": "Teilnehmer hinzufügen"
},
"form": {
"basicData": "Grunddaten",
"courseNumber": "Kursnummer",
"courseName": "Kursname *",
"description": "Beschreibung",
"schedule": "Zeitplan",
"startDate": "Startdatum",
"endDate": "Enddatum",
"registrationDeadline": "Anmeldeschluss",
"capacity": "Kapazität",
"maxParticipants": "Max. Teilnehmer",
"minParticipants": "Min. Teilnehmer",
"fee": "Gebühr (€)",
"reducedFee": "Ermäßigte Gebühr (€)",
"statusSection": "Status",
"courseStatus": "Kursstatus",
"created": "Kurs erfolgreich erstellt",
"updated": "Kurs aktualisiert",
"errorCreating": "Fehler beim Erstellen des Kurses",
"errorUpdating": "Fehler beim Aktualisieren"
},
"status": {
"planned": "Geplant",
"open": "Offen",
"running": "Laufend",
"completed": "Abgeschlossen",
"cancelled": "Abgesagt"
},
"enrollment": {
"enrolled": "Eingeschrieben",
"waitlisted": "Warteliste",
"cancelled": "Storniert",
"completed": "Abgeschlossen",
"enrolledAt": "Eingeschrieben am"
},
"attendance": {
"title": "Anwesenheit",
"present": "Anwesend",
"absent": "Abwesend",
"excused": "Entschuldigt",
"session": "Termin"
},
"calendar": {
"title": "Kurskalender",
"courseDay": "Kurstag",
"free": "Frei",
"today": "Heute",
"weekdays": ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"],
"months": [
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember"
]
},
"categories": {
"title": "Kategorien",
"newCategory": "Neue Kategorie",
"noCategories": "Keine Kategorien vorhanden."
},
"instructors": {
"title": "Kursleiter",
"newInstructor": "Neuer Kursleiter",
"noInstructors": "Keine Kursleiter vorhanden."
},
"locations": {
"title": "Standorte",
"newLocation": "Neuer Standort",
"noLocations": "Keine Standorte vorhanden."
}
}

View File

@@ -0,0 +1,81 @@
{
"nav": {
"documents": "Dokumente",
"generate": "Generieren",
"templates": "Vorlagen"
},
"overview": {
"title": "Dokumente",
"subtitle": "Dokumente generieren und Vorlagen verwalten",
"generate": "Erstellen",
"manageTemplates": "Vorlagen verwalten"
},
"types": {
"member_card": "Mitgliedsausweis",
"invoice": "Rechnung",
"label": "Etiketten",
"report": "Bericht",
"letter": "Brief",
"certificate": "Zertifikat"
},
"typeDescriptions": {
"member_card": "Mitgliedsausweise mit Foto und QR-Code generieren",
"invoice": "Professionelle Rechnungen für Mitglieder und Dienstleistungen",
"label": "Adressetiketten für Serienbriefe und Versand",
"report": "Statistische Auswertungen und Übersichten",
"letter": "Serienbriefe und individuelle Schreiben",
"certificate": "Teilnahmebescheinigungen und Urkunden"
},
"generate": {
"title": "Dokument generieren",
"documentType": "Dokumenttyp",
"titleLabel": "Titel / Bezeichnung",
"format": "Format",
"orientation": "Ausrichtung",
"portrait": "Hochformat",
"landscape": "Querformat",
"comingSoon": "Demnächst verfügbar",
"generating": "Wird generiert...",
"generateButton": "Generieren",
"success": "Dokument erfolgreich erstellt!",
"downloaded": "Datei heruntergeladen",
"downloadAgain": "Erneut herunterladen",
"error": "Fehler bei der Generierung",
"backToDocuments": "Zurück zu Dokumente",
"cancel": "Abbrechen",
"hint": "Hinweis",
"memberCardHint": "Es werden Mitgliedsausweise für alle aktiven Mitglieder generiert.",
"labelHint": "Es werden Adressetiketten für alle aktiven Mitglieder generiert.",
"reportHint": "Es wird ein Mitgliederbericht mit aktuellen Daten generiert."
},
"templates": {
"title": "Dokumentvorlagen",
"subtitle": "Vorlagen für Mitgliedsausweise, Rechnungen, Etiketten und mehr",
"newTemplate": "Neue Vorlage",
"noTemplates": "Keine Vorlagen vorhanden",
"createFirst": "Erstellen Sie Ihre erste Dokumentvorlage.",
"allTemplates": "Alle Vorlagen ({count})",
"name": "Name",
"type": "Typ",
"description": "Beschreibung"
},
"card": {
"title": "MITGLIEDSAUSWEIS",
"memberSince": "Mitglied seit",
"dateOfBirth": "Geb.-Datum",
"address": "Adresse",
"validUntil": "Gültig",
"issuedOn": "Ausgestellt"
},
"report": {
"title": "Mitgliederbericht",
"total": "Gesamt:"
},
"errors": {
"accountNotFound": "Konto nicht gefunden",
"unknownType": "Unbekannter Dokumenttyp",
"underDevelopment": "ist noch in Entwicklung",
"dbError": "DB-Fehler",
"noActiveMembers": "Keine aktiven Mitglieder"
}
}

View File

@@ -0,0 +1,67 @@
{
"nav": {
"events": "Veranstaltungen",
"newEvent": "Neue Veranstaltung",
"registrations": "Anmeldungen",
"holidayPasses": "Ferienpässe"
},
"list": {
"title": "Veranstaltungen",
"newEvent": "Neue Veranstaltung",
"noEvents": "Keine Veranstaltungen vorhanden",
"createFirst": "Erstellen Sie Ihre erste Veranstaltung.",
"upcoming": "Bevorstehend",
"totalRegistrations": "Anmeldungen gesamt"
},
"detail": {
"notFound": "Veranstaltung nicht gefunden",
"date": "Datum",
"time": "Uhrzeit",
"location": "Ort",
"registrations": "Anmeldungen",
"description": "Beschreibung",
"noRegistrations": "Noch keine Anmeldungen",
"parentName": "Elternteil"
},
"form": {
"basicData": "Grunddaten",
"name": "Name der Veranstaltung *",
"description": "Beschreibung",
"dateTime": "Datum & Zeit",
"eventDate": "Datum *",
"startTime": "Startzeit",
"endDate": "Enddatum",
"location": "Ort",
"contact": "Kontakt",
"contactName": "Ansprechpartner",
"contactEmail": "E-Mail",
"contactPhone": "Telefon",
"participantsAndFees": "Teilnehmer & Kosten",
"capacity": "Max. Teilnehmer",
"minAge": "Mindestalter",
"maxAge": "Höchstalter",
"fee": "Gebühr (€)",
"registrationDeadline": "Anmeldeschluss",
"status": "Status",
"created": "Veranstaltung erfolgreich erstellt",
"errorCreating": "Fehler beim Erstellen der Veranstaltung"
},
"status": {
"planned": "Geplant",
"open": "Offen",
"full": "Ausgebucht",
"running": "Laufend",
"completed": "Abgeschlossen",
"cancelled": "Abgesagt"
},
"registrationStatus": {
"pending": "Ausstehend",
"confirmed": "Bestätigt",
"waitlisted": "Warteliste",
"cancelled": "Storniert"
},
"holidayPasses": {
"title": "Ferienpässe",
"noPasses": "Keine Ferienpässe vorhanden"
}
}

View File

@@ -0,0 +1,113 @@
{
"nav": {
"overview": "Finanzen",
"invoices": "Rechnungen",
"sepa": "SEPA-Einzüge",
"payments": "Zahlungen",
"newInvoice": "Neue Rechnung",
"newBatch": "Neuer Einzug"
},
"dashboard": {
"title": "Finanzen Übersicht",
"subtitle": "Rechnungen, SEPA-Einzüge und Zahlungen verwalten",
"openInvoices": "Offene Rechnungen",
"overdueInvoices": "Überfällige Rechnungen",
"totalRevenue": "Gesamteinnahmen",
"sepaBatches": "SEPA-Einzüge"
},
"invoices": {
"title": "Rechnungen",
"newInvoice": "Neue Rechnung",
"noInvoices": "Keine Rechnungen vorhanden",
"createFirst": "Erstellen Sie Ihre erste Rechnung.",
"invoiceNumber": "Rechnungsnr.",
"recipient": "Empfänger",
"issueDate": "Rechnungsdatum",
"dueDate": "Fälligkeitsdatum",
"amount": "Betrag",
"notFound": "Rechnung nicht gefunden"
},
"invoiceForm": {
"title": "Rechnungsdaten",
"invoiceNumber": "Rechnungsnummer *",
"issueDate": "Rechnungsdatum",
"dueDate": "Fälligkeitsdatum *",
"recipient": "Empfänger",
"recipientName": "Name *",
"recipientAddress": "Adresse",
"lineItems": "Positionen",
"addLineItem": "+ Position hinzufügen",
"itemDescription": "Beschreibung *",
"quantity": "Menge",
"unitPrice": "Einzelpreis (€)",
"removeItem": "Position entfernen",
"amounts": "Beträge",
"taxRate": "MwSt.-Satz (%)",
"subtotal": "Zwischensumme (netto)",
"tax": "MwSt. ({rate}%)",
"total": "Gesamtbetrag",
"notes": "Anmerkungen",
"created": "Rechnung erfolgreich erstellt",
"errorCreating": "Fehler beim Erstellen der Rechnung"
},
"invoiceStatus": {
"draft": "Entwurf",
"sent": "Versendet",
"paid": "Bezahlt",
"overdue": "Überfällig",
"cancelled": "Storniert",
"credited": "Gutgeschrieben"
},
"sepa": {
"title": "SEPA-Einzüge",
"newBatch": "Neuer Einzug",
"noBatches": "Keine SEPA-Einzüge vorhanden",
"createFirst": "Erstellen Sie Ihren ersten SEPA-Einzug.",
"directDebit": "Lastschrift",
"creditTransfer": "Überweisung",
"executionDate": "Ausführungsdatum",
"totalAmount": "Gesamtbetrag",
"itemCount": "Positionen",
"downloadXml": "XML herunterladen",
"notFound": "Einzug nicht gefunden"
},
"sepaBatchForm": {
"title": "SEPA-Einzug erstellen",
"batchType": "Typ",
"directDebit": "Lastschrift (SEPA Core)",
"creditTransfer": "Überweisung",
"description": "Beschreibung",
"descriptionPlaceholder": "z.B. Mitgliedsbeiträge Q1 2026",
"executionDate": "Ausführungsdatum *",
"executionDateRequired": "Ausführungsdatum ist erforderlich",
"created": "SEPA-Einzug erstellt",
"errorCreating": "Fehler beim Erstellen"
},
"sepaBatchStatus": {
"draft": "Entwurf",
"ready": "Bereit",
"submitted": "Eingereicht",
"executed": "Abgeschlossen",
"failed": "Fehlgeschlagen",
"cancelled": "Abgebrochen"
},
"sepaItemStatus": {
"pending": "Ausstehend",
"success": "Verarbeitet",
"failed": "Fehlgeschlagen",
"rejected": "Abgelehnt"
},
"payments": {
"title": "Zahlungsübersicht",
"paidInvoices": "Bezahlte Rechnungen",
"openInvoices": "Offene Rechnungen",
"overdueInvoices": "Überfällige Rechnungen",
"sepaBatches": "SEPA-Einzüge"
},
"common": {
"cancel": "Abbrechen",
"creating": "Wird erstellt...",
"membershipFee": "Mitgliedsbeitrag",
"sepaDirectDebit": "SEPA Einzug"
}
}

View File

@@ -0,0 +1,80 @@
{
"nav": {
"overview": "Übersicht",
"protocols": "Protokolle",
"tasks": "Aufgaben",
"newProtocol": "Neues Protokoll"
},
"pages": {
"overviewTitle": "Sitzungsprotokolle",
"protocolsTitle": "Sitzungsprotokolle - Protokolle",
"tasksTitle": "Sitzungsprotokolle - Aufgaben"
},
"dashboard": {
"title": "Sitzungsprotokolle Übersicht",
"subtitle": "Protokolle, Tagesordnungspunkte und Aufgaben verwalten",
"totalProtocols": "Protokolle gesamt",
"publishedProtocols": "Veröffentlicht",
"draftProtocols": "Entwürfe",
"overdueTasks": "Überfällige Aufgaben",
"recentProtocols": "Neueste Protokolle",
"noRecentProtocols": "Noch keine Protokolle vorhanden."
},
"protocol": {
"title": "Titel *",
"meetingDate": "Sitzungsdatum *",
"meetingType": "Sitzungsart",
"location": "Ort",
"created": "Protokoll erfolgreich erstellt",
"updated": "Protokoll aktualisiert",
"published": "Protokoll veröffentlicht",
"errorSaving": "Fehler beim Speichern des Protokolls",
"notFound": "Protokoll nicht gefunden",
"noProtocols": "Keine Protokolle vorhanden",
"createFirst": "Erstellen Sie Ihr erstes Sitzungsprotokoll, um loszulegen."
},
"protocolList": {
"searchPlaceholder": "Protokoll suchen...",
"title": "Protokolle ({count})",
"date": "Datum",
"titleColumn": "Titel",
"type": "Sitzungsart",
"locationColumn": "Ort",
"statusColumn": "Status",
"published": "Veröffentlicht",
"draft": "Entwurf"
},
"items": {
"agendaItems": "Tagesordnungspunkte",
"noItems": "Keine Tagesordnungspunkte vorhanden.",
"addItem": "Punkt hinzufügen",
"statusUpdated": "Status aktualisiert",
"deleted": "Tagesordnungspunkt gelöscht",
"deleteConfirm": "Tagesordnungspunkt wirklich löschen?",
"errorUpdating": "Fehler beim Aktualisieren",
"errorDeleting": "Fehler beim Löschen",
"dueDate": "Fällig: {date}",
"responsible": "Verantwortlich: {name}",
"status": {
"open": "Offen",
"in_progress": "In Bearbeitung",
"done": "Erledigt",
"cancelled": "Abgebrochen"
}
},
"tasks": {
"title": "Offene Aufgaben ({count})",
"noTasks": "Keine offenen Aufgaben",
"allTasksDone": "Alle Aufgaben sind erledigt.",
"overdue": "Überfällig",
"fromProtocol": "aus Protokoll"
},
"meetingTypes": {
"regular": "Ordentliche Sitzung",
"extraordinary": "Außerordentliche Sitzung",
"board": "Vorstandssitzung",
"general_assembly": "Mitgliederversammlung",
"committee": "Ausschusssitzung",
"other": "Sonstige"
}
}

View File

@@ -0,0 +1,76 @@
{
"nav": {
"newsletters": "Newsletter",
"templates": "Vorlagen",
"newNewsletter": "Neuer Newsletter"
},
"list": {
"title": "Newsletter",
"subtitle": "Newsletter erstellen und versenden",
"newNewsletter": "Neuer Newsletter",
"noNewsletters": "Keine Newsletter vorhanden",
"createFirst": "Erstellen Sie Ihren ersten Newsletter, um loszulegen.",
"subject": "Betreff",
"noSubject": "(Kein Betreff)",
"recipients": "Empfänger",
"created": "Erstellt",
"sent": "Gesendet",
"totalSent": "Gesendet",
"totalRecipients": "Empfänger gesamt"
},
"detail": {
"title": "Newsletter Details",
"backToList": "Zurück zu Newsletter",
"sendNewsletter": "Newsletter versenden",
"recipientsSection": "Empfänger",
"sentCount": "Gesendet",
"failedCount": "Fehlgeschlagen",
"noRecipients": "Keine Empfänger hinzugefügt. Fügen Sie Empfänger aus Ihrer Mitgliederliste hinzu.",
"recipientName": "Name",
"recipientEmail": "E-Mail",
"recipientStatus": "Status",
"notFound": "Newsletter nicht gefunden"
},
"form": {
"content": "Newsletter-Inhalt",
"subject": "Betreff *",
"bodyHtml": "Inhalt (HTML) *",
"bodyHtmlPlaceholder": "<h1>Hallo!</h1><p>Ihr Newsletter-Inhalt...</p>",
"bodyText": "Nur-Text-Version",
"bodyTextHelp": "Nur-Text-Fallback für E-Mail-Clients ohne HTML-Unterstützung",
"schedule": "Zeitplan",
"scheduledDate": "Geplanter Versand (optional)",
"scheduleHelp": "Leer lassen, um den Newsletter als Entwurf zu speichern.",
"created": "Newsletter erfolgreich erstellt",
"errorCreating": "Fehler beim Erstellen des Newsletters"
},
"templates": {
"title": "Newsletter-Vorlagen",
"subtitle": "Wiederverwendbare Vorlagen für Newsletter",
"newTemplate": "Neue Vorlage",
"noTemplates": "Keine Vorlagen vorhanden",
"createFirst": "Erstellen Sie Ihre erste Newsletter-Vorlage, um sie in Kampagnen wiederzuverwenden.",
"allTemplates": "Alle Vorlagen ({count})",
"name": "Name",
"subject": "Betreff",
"variables": "Variablen"
},
"status": {
"draft": "Entwurf",
"scheduled": "Geplant",
"sending": "Wird versendet",
"sent": "Gesendet",
"failed": "Fehlgeschlagen"
},
"recipientStatus": {
"pending": "Ausstehend",
"sent": "Gesendet",
"failed": "Fehlgeschlagen",
"bounced": "Unzustellbar"
},
"common": {
"cancel": "Abbrechen",
"creating": "Wird erstellt...",
"create": "Newsletter erstellen"
}
}

View File

@@ -0,0 +1,53 @@
{
"nav": {
"pages": "Seiten",
"posts": "Beiträge",
"settings": "Einstellungen"
},
"pages": {
"title": "Seiten",
"newPage": "Neue Seite",
"noPages": "Keine Seiten vorhanden",
"createFirst": "Erstellen Sie Ihre erste Seite.",
"pageTitle": "Seitentitel *",
"slug": "URL-Pfad",
"slugPlaceholder": "Leer lassen für automatische Generierung...",
"membersOnly": "Nur für Mitglieder",
"createAndEdit": "Seite erstellen & Editor öffnen",
"pageCreated": "Seite erstellt — Editor wird geöffnet",
"errorCreating": "Fehler beim Erstellen",
"notFound": "Seite nicht gefunden",
"published": "Seite veröffentlicht",
"error": "Fehler"
},
"posts": {
"title": "Beiträge",
"newPost": "Neuer Beitrag",
"noPosts": "Keine Beiträge vorhanden",
"createFirst": "Erstellen Sie Ihren ersten Beitrag.",
"postTitle": "Titel *",
"content": "Beitragsinhalt (HTML erlaubt)...",
"excerpt": "Kurzfassung",
"postCreated": "Beitrag erstellt",
"errorCreating": "Fehler"
},
"settings": {
"title": "Einstellungen",
"saved": "Einstellungen gespeichert",
"error": "Fehler"
},
"editor": {
"publish": "Veröffentlichen",
"unpublish": "Zurückziehen"
},
"portal": {
"loginSuccess": "Erfolgreich angemeldet",
"membersArea": "Mitgliederbereich"
},
"blocks": {
"news": "Neuigkeiten",
"events": "Veranstaltungen",
"loginError": "Fehler bei der Anmeldung",
"connectionError": "Verbindungsfehler"
}
}

View File

@@ -0,0 +1,71 @@
{
"nav": {
"bookings": "Bookings",
"calendar": "Calendar",
"rooms": "Rooms",
"guests": "Guests",
"newBooking": "New Booking"
},
"list": {
"title": "Bookings",
"searchPlaceholder": "Search guest or room...",
"newBooking": "New Booking",
"noBookings": "No bookings found",
"createFirst": "Create your first booking.",
"activeBookings": "Active Bookings",
"guest": "Guest",
"room": "Room",
"checkIn": "Check-in",
"checkOut": "Check-out",
"nights": "Nights",
"price": "Price"
},
"detail": {
"notFound": "Booking not found",
"guestInfo": "Guest Information",
"roomInfo": "Room Information",
"bookingDetails": "Booking Details",
"extras": "Extras"
},
"form": {
"room": "Room *",
"selectRoom": "— Select room —",
"guest": "Guest",
"checkIn": "Check-in *",
"checkOut": "Check-out *",
"adults": "Adults",
"children": "Children",
"notes": "Notes",
"created": "Booking created successfully",
"errorCreating": "Error creating booking"
},
"status": {
"pending": "Pending",
"confirmed": "Confirmed",
"checked_in": "Checked In",
"checked_out": "Checked Out",
"cancelled": "Cancelled",
"no_show": "No Show"
},
"rooms": {
"title": "Rooms",
"newRoom": "New Room",
"noRooms": "No rooms found",
"name": "Name",
"type": "Type",
"capacity": "Capacity",
"price": "Price/Night"
},
"guests": {
"title": "Guests",
"newGuest": "New Guest",
"noGuests": "No guests found",
"name": "Name",
"email": "Email",
"phone": "Phone",
"bookings": "Bookings"
},
"calendar": {
"title": "Availability Calendar"
}
}

View File

@@ -0,0 +1,120 @@
{
"nav": {
"courses": "Courses",
"newCourse": "New Course",
"calendar": "Calendar",
"categories": "Categories",
"instructors": "Instructors",
"locations": "Locations",
"statistics": "Statistics"
},
"pages": {
"coursesTitle": "Courses",
"newCourseTitle": "New Course",
"calendarTitle": "Course Calendar",
"categoriesTitle": "Course Categories",
"instructorsTitle": "Instructors",
"locationsTitle": "Locations",
"statisticsTitle": "Course Statistics"
},
"list": {
"searchPlaceholder": "Search courses...",
"title": "Courses ({count})",
"noCourses": "No courses found",
"createFirst": "Create your first course to get started.",
"courseNumber": "Course No.",
"courseName": "Course Name",
"startDate": "Start",
"endDate": "End",
"participants": "Participants",
"fee": "Fee"
},
"detail": {
"notFound": "Course not found",
"participants": "Participants",
"sessions": "Sessions",
"viewAllParticipants": "View all participants",
"viewAttendance": "View attendance",
"noParticipants": "No participants yet.",
"noSessions": "No sessions yet.",
"addParticipant": "Add Participant"
},
"form": {
"basicData": "Basic Data",
"courseNumber": "Course Number",
"courseName": "Course Name *",
"description": "Description",
"schedule": "Schedule",
"startDate": "Start Date",
"endDate": "End Date",
"registrationDeadline": "Registration Deadline",
"capacity": "Capacity",
"maxParticipants": "Max Participants",
"minParticipants": "Min Participants",
"fee": "Fee (€)",
"reducedFee": "Reduced Fee (€)",
"statusSection": "Status",
"courseStatus": "Course Status",
"created": "Course created successfully",
"updated": "Course updated",
"errorCreating": "Error creating course",
"errorUpdating": "Error updating course"
},
"status": {
"planned": "Planned",
"open": "Open",
"running": "Running",
"completed": "Completed",
"cancelled": "Cancelled"
},
"enrollment": {
"enrolled": "Enrolled",
"waitlisted": "Waitlisted",
"cancelled": "Cancelled",
"completed": "Completed",
"enrolledAt": "Enrolled on"
},
"attendance": {
"title": "Attendance",
"present": "Present",
"absent": "Absent",
"excused": "Excused",
"session": "Session"
},
"calendar": {
"title": "Course Calendar",
"courseDay": "Course Day",
"free": "Free",
"today": "Today",
"weekdays": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
"months": [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
]
},
"categories": {
"title": "Categories",
"newCategory": "New Category",
"noCategories": "No categories found."
},
"instructors": {
"title": "Instructors",
"newInstructor": "New Instructor",
"noInstructors": "No instructors found."
},
"locations": {
"title": "Locations",
"newLocation": "New Location",
"noLocations": "No locations found."
}
}

View File

@@ -0,0 +1,81 @@
{
"nav": {
"documents": "Documents",
"generate": "Generate",
"templates": "Templates"
},
"overview": {
"title": "Documents",
"subtitle": "Generate documents and manage templates",
"generate": "Generate",
"manageTemplates": "Manage Templates"
},
"types": {
"member_card": "Member Card",
"invoice": "Invoice",
"label": "Labels",
"report": "Report",
"letter": "Letter",
"certificate": "Certificate"
},
"typeDescriptions": {
"member_card": "Generate member cards with photo and QR code",
"invoice": "Professional invoices for members and services",
"label": "Address labels for mailings and shipping",
"report": "Statistical reports and summaries",
"letter": "Form letters and individual correspondence",
"certificate": "Participation certificates and awards"
},
"generate": {
"title": "Generate Document",
"documentType": "Document Type",
"titleLabel": "Title / Label",
"format": "Format",
"orientation": "Orientation",
"portrait": "Portrait",
"landscape": "Landscape",
"comingSoon": "Coming Soon",
"generating": "Generating...",
"generateButton": "Generate",
"success": "Document created successfully!",
"downloaded": "File downloaded",
"downloadAgain": "Download Again",
"error": "Error generating document",
"backToDocuments": "Back to Documents",
"cancel": "Cancel",
"hint": "Note",
"memberCardHint": "Member cards will be generated for all active members.",
"labelHint": "Address labels will be generated for all active members.",
"reportHint": "A member report with current data will be generated."
},
"templates": {
"title": "Document Templates",
"subtitle": "Templates for member cards, invoices, labels, and more",
"newTemplate": "New Template",
"noTemplates": "No templates found",
"createFirst": "Create your first document template.",
"allTemplates": "All Templates ({count})",
"name": "Name",
"type": "Type",
"description": "Description"
},
"card": {
"title": "MEMBER CARD",
"memberSince": "Member since",
"dateOfBirth": "Date of Birth",
"address": "Address",
"validUntil": "Valid until",
"issuedOn": "Issued on"
},
"report": {
"title": "Member Report",
"total": "Total:"
},
"errors": {
"accountNotFound": "Account not found",
"unknownType": "Unknown document type",
"underDevelopment": "is still under development",
"dbError": "Database error",
"noActiveMembers": "No active members"
}
}

View File

@@ -0,0 +1,67 @@
{
"nav": {
"events": "Events",
"newEvent": "New Event",
"registrations": "Registrations",
"holidayPasses": "Holiday Passes"
},
"list": {
"title": "Events",
"newEvent": "New Event",
"noEvents": "No events found",
"createFirst": "Create your first event.",
"upcoming": "Upcoming",
"totalRegistrations": "Total Registrations"
},
"detail": {
"notFound": "Event not found",
"date": "Date",
"time": "Time",
"location": "Location",
"registrations": "Registrations",
"description": "Description",
"noRegistrations": "No registrations yet",
"parentName": "Parent"
},
"form": {
"basicData": "Basic Data",
"name": "Event Name *",
"description": "Description",
"dateTime": "Date & Time",
"eventDate": "Date *",
"startTime": "Start Time",
"endDate": "End Date",
"location": "Location",
"contact": "Contact",
"contactName": "Contact Person",
"contactEmail": "Email",
"contactPhone": "Phone",
"participantsAndFees": "Participants & Fees",
"capacity": "Max Participants",
"minAge": "Minimum Age",
"maxAge": "Maximum Age",
"fee": "Fee (€)",
"registrationDeadline": "Registration Deadline",
"status": "Status",
"created": "Event created successfully",
"errorCreating": "Error creating event"
},
"status": {
"planned": "Planned",
"open": "Open",
"full": "Full",
"running": "Running",
"completed": "Completed",
"cancelled": "Cancelled"
},
"registrationStatus": {
"pending": "Pending",
"confirmed": "Confirmed",
"waitlisted": "Waitlisted",
"cancelled": "Cancelled"
},
"holidayPasses": {
"title": "Holiday Passes",
"noPasses": "No holiday passes found"
}
}

View File

@@ -0,0 +1,113 @@
{
"nav": {
"overview": "Finance",
"invoices": "Invoices",
"sepa": "SEPA Batches",
"payments": "Payments",
"newInvoice": "New Invoice",
"newBatch": "New Batch"
},
"dashboard": {
"title": "Finance Overview",
"subtitle": "Manage invoices, SEPA batches, and payments",
"openInvoices": "Open Invoices",
"overdueInvoices": "Overdue Invoices",
"totalRevenue": "Total Revenue",
"sepaBatches": "SEPA Batches"
},
"invoices": {
"title": "Invoices",
"newInvoice": "New Invoice",
"noInvoices": "No invoices found",
"createFirst": "Create your first invoice.",
"invoiceNumber": "Invoice No.",
"recipient": "Recipient",
"issueDate": "Issue Date",
"dueDate": "Due Date",
"amount": "Amount",
"notFound": "Invoice not found"
},
"invoiceForm": {
"title": "Invoice Details",
"invoiceNumber": "Invoice Number *",
"issueDate": "Issue Date",
"dueDate": "Due Date *",
"recipient": "Recipient",
"recipientName": "Name *",
"recipientAddress": "Address",
"lineItems": "Line Items",
"addLineItem": "+ Add Line Item",
"itemDescription": "Description *",
"quantity": "Quantity",
"unitPrice": "Unit Price (€)",
"removeItem": "Remove Item",
"amounts": "Amounts",
"taxRate": "Tax Rate (%)",
"subtotal": "Subtotal (net)",
"tax": "Tax ({rate}%)",
"total": "Total Amount",
"notes": "Notes",
"created": "Invoice created successfully",
"errorCreating": "Error creating invoice"
},
"invoiceStatus": {
"draft": "Draft",
"sent": "Sent",
"paid": "Paid",
"overdue": "Overdue",
"cancelled": "Cancelled",
"credited": "Credited"
},
"sepa": {
"title": "SEPA Batches",
"newBatch": "New Batch",
"noBatches": "No SEPA batches found",
"createFirst": "Create your first SEPA batch.",
"directDebit": "Direct Debit",
"creditTransfer": "Credit Transfer",
"executionDate": "Execution Date",
"totalAmount": "Total Amount",
"itemCount": "Items",
"downloadXml": "Download XML",
"notFound": "Batch not found"
},
"sepaBatchForm": {
"title": "Create SEPA Batch",
"batchType": "Type",
"directDebit": "Direct Debit (SEPA Core)",
"creditTransfer": "Credit Transfer",
"description": "Description",
"descriptionPlaceholder": "e.g. Membership fees Q1 2026",
"executionDate": "Execution Date *",
"executionDateRequired": "Execution date is required",
"created": "SEPA batch created",
"errorCreating": "Error creating batch"
},
"sepaBatchStatus": {
"draft": "Draft",
"ready": "Ready",
"submitted": "Submitted",
"executed": "Executed",
"failed": "Failed",
"cancelled": "Cancelled"
},
"sepaItemStatus": {
"pending": "Pending",
"success": "Processed",
"failed": "Failed",
"rejected": "Rejected"
},
"payments": {
"title": "Payment Overview",
"paidInvoices": "Paid Invoices",
"openInvoices": "Open Invoices",
"overdueInvoices": "Overdue Invoices",
"sepaBatches": "SEPA Batches"
},
"common": {
"cancel": "Cancel",
"creating": "Creating...",
"membershipFee": "Membership Fee",
"sepaDirectDebit": "SEPA Direct Debit"
}
}

View File

@@ -0,0 +1,80 @@
{
"nav": {
"overview": "Overview",
"protocols": "Protocols",
"tasks": "Tasks",
"newProtocol": "New Protocol"
},
"pages": {
"overviewTitle": "Meeting Protocols",
"protocolsTitle": "Meeting Protocols - Protocols",
"tasksTitle": "Meeting Protocols - Tasks"
},
"dashboard": {
"title": "Meeting Protocols Overview",
"subtitle": "Manage protocols, agenda items, and tasks",
"totalProtocols": "Total Protocols",
"publishedProtocols": "Published",
"draftProtocols": "Drafts",
"overdueTasks": "Overdue Tasks",
"recentProtocols": "Recent Protocols",
"noRecentProtocols": "No protocols yet."
},
"protocol": {
"title": "Title *",
"meetingDate": "Meeting Date *",
"meetingType": "Meeting Type",
"location": "Location",
"created": "Protocol created successfully",
"updated": "Protocol updated",
"published": "Protocol published",
"errorSaving": "Error saving protocol",
"notFound": "Protocol not found",
"noProtocols": "No protocols found",
"createFirst": "Create your first meeting protocol to get started."
},
"protocolList": {
"searchPlaceholder": "Search protocols...",
"title": "Protocols ({count})",
"date": "Date",
"titleColumn": "Title",
"type": "Meeting Type",
"locationColumn": "Location",
"statusColumn": "Status",
"published": "Published",
"draft": "Draft"
},
"items": {
"agendaItems": "Agenda Items",
"noItems": "No agenda items found.",
"addItem": "Add Item",
"statusUpdated": "Status updated",
"deleted": "Agenda item deleted",
"deleteConfirm": "Delete this agenda item?",
"errorUpdating": "Error updating",
"errorDeleting": "Error deleting",
"dueDate": "Due: {date}",
"responsible": "Responsible: {name}",
"status": {
"open": "Open",
"in_progress": "In Progress",
"done": "Done",
"cancelled": "Cancelled"
}
},
"tasks": {
"title": "Open Tasks ({count})",
"noTasks": "No open tasks",
"allTasksDone": "All tasks are completed.",
"overdue": "Overdue",
"fromProtocol": "from protocol"
},
"meetingTypes": {
"regular": "Regular Meeting",
"extraordinary": "Extraordinary Meeting",
"board": "Board Meeting",
"general_assembly": "General Assembly",
"committee": "Committee Meeting",
"other": "Other"
}
}

View File

@@ -0,0 +1,76 @@
{
"nav": {
"newsletters": "Newsletters",
"templates": "Templates",
"newNewsletter": "New Newsletter"
},
"list": {
"title": "Newsletters",
"subtitle": "Create and send newsletters",
"newNewsletter": "New Newsletter",
"noNewsletters": "No newsletters found",
"createFirst": "Create your first newsletter to get started.",
"subject": "Subject",
"noSubject": "(No Subject)",
"recipients": "Recipients",
"created": "Created",
"sent": "Sent",
"totalSent": "Sent",
"totalRecipients": "Total Recipients"
},
"detail": {
"title": "Newsletter Details",
"backToList": "Back to Newsletters",
"sendNewsletter": "Send Newsletter",
"recipientsSection": "Recipients",
"sentCount": "Sent",
"failedCount": "Failed",
"noRecipients": "No recipients added. Add recipients from your member list.",
"recipientName": "Name",
"recipientEmail": "Email",
"recipientStatus": "Status",
"notFound": "Newsletter not found"
},
"form": {
"content": "Newsletter Content",
"subject": "Subject *",
"bodyHtml": "Content (HTML) *",
"bodyHtmlPlaceholder": "<h1>Hello!</h1><p>Your newsletter content...</p>",
"bodyText": "Plain Text Version",
"bodyTextHelp": "Plain text fallback for email clients without HTML support",
"schedule": "Schedule",
"scheduledDate": "Scheduled Send (optional)",
"scheduleHelp": "Leave empty to save the newsletter as a draft.",
"created": "Newsletter created successfully",
"errorCreating": "Error creating newsletter"
},
"templates": {
"title": "Newsletter Templates",
"subtitle": "Reusable templates for newsletters",
"newTemplate": "New Template",
"noTemplates": "No templates found",
"createFirst": "Create your first newsletter template for reuse in campaigns.",
"allTemplates": "All Templates ({count})",
"name": "Name",
"subject": "Subject",
"variables": "Variables"
},
"status": {
"draft": "Draft",
"scheduled": "Scheduled",
"sending": "Sending",
"sent": "Sent",
"failed": "Failed"
},
"recipientStatus": {
"pending": "Pending",
"sent": "Sent",
"failed": "Failed",
"bounced": "Bounced"
},
"common": {
"cancel": "Cancel",
"creating": "Creating...",
"create": "Create Newsletter"
}
}

View File

@@ -0,0 +1,53 @@
{
"nav": {
"pages": "Pages",
"posts": "Posts",
"settings": "Settings"
},
"pages": {
"title": "Pages",
"newPage": "New Page",
"noPages": "No pages found",
"createFirst": "Create your first page.",
"pageTitle": "Page Title *",
"slug": "URL Path",
"slugPlaceholder": "Leave empty for auto-generation...",
"membersOnly": "Members Only",
"createAndEdit": "Create Page & Open Editor",
"pageCreated": "Page created — opening editor",
"errorCreating": "Error creating page",
"notFound": "Page not found",
"published": "Page published",
"error": "Error"
},
"posts": {
"title": "Posts",
"newPost": "New Post",
"noPosts": "No posts found",
"createFirst": "Create your first post.",
"postTitle": "Title *",
"content": "Post content (HTML allowed)...",
"excerpt": "Excerpt",
"postCreated": "Post created",
"errorCreating": "Error"
},
"settings": {
"title": "Settings",
"saved": "Settings saved",
"error": "Error"
},
"editor": {
"publish": "Publish",
"unpublish": "Unpublish"
},
"portal": {
"loginSuccess": "Logged in successfully",
"membersArea": "Members Area"
},
"blocks": {
"news": "News",
"events": "Events",
"loginError": "Login error",
"connectionError": "Connection error"
}
}

View File

@@ -23,6 +23,14 @@ const namespaces = [
'verband',
'members',
'fischerei',
'meetings',
'courses',
'finance',
'newsletter',
'siteBuilder',
'events',
'documents',
'bookings',
] as const;
const isDevelopment = process.env.NODE_ENV === 'development';