Complete translation implementation for admin interface

- Fixed all hardcoded strings in admin components to use t() function
- Updated BrandingPage.tsx to use translations for watermark settings
- Updated EventsListPage.tsx to use translations for status labels
- Added missing translation keys to both en.json and de.json
- Fixed translations for:
  - System settings (general, storage, categories tabs)
  - Branding page (watermark settings, positions, opacity)
  - Email configuration and templates
  - Event list view (status labels, filters, actions)
  - Event detail view (all sections properly translated)
- Added comprehensive German translations for all new keys
- Ensured consistent translation usage across all admin pages

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

Co-Authored-By: Claude <[email protected]>
This commit is contained in:
2025-07-08 17:41:02 +02:00
co-authored by Claude
parent d594d00227
commit 1bc9b547c7
11 changed files with 491 additions and 356 deletions
+43 -41
View File
@@ -19,8 +19,10 @@ import { BulkArchiveModal } from '../../components/admin';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { eventsService } from '../../services/events.service';
import type { Event } from '../../types';
import { useTranslation } from 'react-i18next';
export const EventsListPage: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const [searchParams, setSearchParams] = useSearchParams();
@@ -46,10 +48,10 @@ export const EventsListPage: React.FC = () => {
mutationFn: eventsService.archiveEvent,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-events'] });
toast.success('Event archived successfully');
toast.success(t('toast.eventArchived'));
},
onError: () => {
toast.error('Failed to archive event');
toast.error(t('toast.saveError'));
},
});
@@ -58,10 +60,10 @@ export const EventsListPage: React.FC = () => {
mutationFn: eventsService.deleteEvent,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-events'] });
toast.success('Event deleted successfully');
toast.success(t('toast.deleteSuccess'));
},
onError: () => {
toast.error('Failed to delete event');
toast.error(t('toast.deleteError'));
},
});
@@ -74,13 +76,13 @@ export const EventsListPage: React.FC = () => {
setShowBulkArchiveModal(false);
if (data.results.failed.length === 0) {
toast.success(`Successfully archived ${data.results.successful.length} events`);
toast.success(t('events.bulkArchiveSuccess', { count: data.results.successful.length }));
} else {
toast.warning(`Archived ${data.results.successful.length} events, ${data.results.failed.length} failed`);
toast.warning(t('events.bulkArchivePartial', { success: data.results.successful.length, failed: data.results.failed.length }));
}
},
onError: () => {
toast.error('Failed to archive events');
toast.error(t('toast.saveError'));
},
});
@@ -140,14 +142,14 @@ export const EventsListPage: React.FC = () => {
};
const getEventStatus = (event: Event) => {
if (event.is_archived) return { label: 'Archived', color: 'text-neutral-500 bg-neutral-100' };
if (!event.is_active) return { label: 'Inactive', color: 'text-red-600 bg-red-100' };
if (event.is_archived) return { label: t('events.archived'), color: 'text-neutral-500 bg-neutral-100' };
if (!event.is_active) return { label: t('events.inactive'), color: 'text-red-600 bg-red-100' };
const days = event.expires_at ? differenceInDays(parseISO(event.expires_at), new Date()) : 0;
if (days <= 0) return { label: 'Expired', color: 'text-red-600 bg-red-100' };
if (days <= 7) return { label: `${days}d left`, color: 'text-orange-600 bg-orange-100' };
if (days <= 0) return { label: t('events.expired'), color: 'text-red-600 bg-red-100' };
if (days <= 7) return { label: t('events.daysLeft', { count: days }), color: 'text-orange-600 bg-orange-100' };
return { label: 'Active', color: 'text-green-600 bg-green-100' };
return { label: t('events.active'), color: 'text-green-600 bg-green-100' };
};
if (isLoading) {
@@ -155,8 +157,8 @@ export const EventsListPage: React.FC = () => {
<div>
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold text-neutral-900">Events</h1>
<p className="text-neutral-600 mt-1">Manage your photo galleries and archives</p>
<h1 className="text-2xl font-bold text-neutral-900">{t('events.title')}</h1>
<p className="text-neutral-600 mt-1">{t('events.subtitle')}</p>
</div>
</div>
<SkeletonTable rows={5} />
@@ -167,9 +169,9 @@ export const EventsListPage: React.FC = () => {
if (error) {
return (
<div className="text-center py-12">
<p className="text-red-600">Failed to load events</p>
<p className="text-red-600">{t('events.failedToLoadEvents')}</p>
<Button onClick={() => window.location.reload()} className="mt-4">
Try Again
{t('events.tryAgain')}
</Button>
</div>
);
@@ -181,15 +183,15 @@ export const EventsListPage: React.FC = () => {
{/* Page Header */}
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold text-neutral-900">Events</h1>
<p className="text-neutral-600 mt-1">Manage your photo galleries and events</p>
<h1 className="text-2xl font-bold text-neutral-900">{t('events.title')}</h1>
<p className="text-neutral-600 mt-1">{t('events.subtitle')}</p>
</div>
<Button
variant="primary"
leftIcon={<Plus className="w-5 h-5" />}
onClick={() => navigate('/admin/events/new')}
>
Create Event
{t('events.createEvent')}
</Button>
</div>
@@ -200,7 +202,7 @@ export const EventsListPage: React.FC = () => {
<div className="flex-1">
<Input
type="text"
placeholder="Search events..."
placeholder={t('events.searchEventsPlaceholder')}
leftIcon={<Search className="w-5 h-5 text-neutral-400" />}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
@@ -217,14 +219,14 @@ export const EventsListPage: React.FC = () => {
setSearchParams(searchParams);
}}
>
All ({data?.events.length || 0})
{t('events.all')} ({data?.events.length || 0})
</Button>
<Button
variant={statusFilter === 'active' ? 'primary' : 'outline'}
size="md"
onClick={() => setSearchParams({ filter: 'active' })}
>
Active
{t('events.active')}
</Button>
<Button
variant={isExpiringFilter ? 'primary' : 'outline'}
@@ -232,7 +234,7 @@ export const EventsListPage: React.FC = () => {
onClick={() => setSearchParams({ filter: 'expiring' })}
leftIcon={<AlertTriangle className="w-4 h-4" />}
>
Expiring
{t('events.expiring')}
</Button>
<Button
variant={statusFilter === 'archived' ? 'primary' : 'outline'}
@@ -240,7 +242,7 @@ export const EventsListPage: React.FC = () => {
onClick={() => setSearchParams({ filter: 'archived' })}
leftIcon={<Archive className="w-4 h-4" />}
>
Archived
{t('events.archived')}
</Button>
</div>
</div>
@@ -249,18 +251,18 @@ export const EventsListPage: React.FC = () => {
{selectedEvents.length > 0 && (
<div className="mt-4 p-3 bg-primary-50 rounded-lg flex items-center justify-between">
<span className="text-sm text-primary-900">
{selectedEvents.length} event{selectedEvents.length > 1 ? 's' : ''} selected
{t('events.eventsSelected', { count: selectedEvents.length })}
</span>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => setSelectedEvents([])}>
Clear
{t('events.clear')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setShowBulkArchiveModal(true)}
>
Archive Selected
{t('events.archiveSelected')}
</Button>
</div>
</div>
@@ -282,22 +284,22 @@ export const EventsListPage: React.FC = () => {
/>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 uppercase tracking-wider">
Event
{t('events.event')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 uppercase tracking-wider">
Type
{t('events.type')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 uppercase tracking-wider">
Date
{t('events.date')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 uppercase tracking-wider">
Status
{t('events.status')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 uppercase tracking-wider">
Expires
{t('events.expires')}
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-neutral-500 uppercase tracking-wider">
Actions
{t('events.actions')}
</th>
</tr>
</thead>
@@ -305,7 +307,7 @@ export const EventsListPage: React.FC = () => {
{filteredEvents.length === 0 ? (
<tr>
<td colSpan={7} className="px-6 py-12 text-center text-neutral-500">
No events found
{t('events.noEventsFound')}
</td>
</tr>
) : (
@@ -362,7 +364,7 @@ export const EventsListPage: React.FC = () => {
className="w-full text-left px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 flex items-center gap-2"
>
<Edit className="w-4 h-4" />
View Details
{t('events.viewDetails')}
</button>
{event.share_link ? (
<a
@@ -373,7 +375,7 @@ export const EventsListPage: React.FC = () => {
onClick={() => setActiveDropdown(null)}
>
<ExternalLink className="w-4 h-4" />
View Gallery
{t('events.viewGallery')}
</a>
) : null}
{!event.is_archived ? (
@@ -385,24 +387,24 @@ export const EventsListPage: React.FC = () => {
className="w-full text-left px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 flex items-center gap-2"
>
<Archive className="w-4 h-4" />
Archive Event
{t('events.archiveEventAction')}
</button>
) : null}
{event.is_archived ? (
<button
onClick={() => {
toast.info('Download archive coming soon');
toast.info(t('events.downloadArchiveSoon'));
setActiveDropdown(null);
}}
className="w-full text-left px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 flex items-center gap-2"
>
<Download className="w-4 h-4" />
Download Archive
{t('events.downloadArchiveAction')}
</button>
) : null}
<button
onClick={() => {
if (confirm('Are you sure you want to delete this event?')) {
if (confirm(t('events.deleteEventConfirm'))) {
deleteMutation.mutate(event.id);
setActiveDropdown(null);
}
@@ -410,7 +412,7 @@ export const EventsListPage: React.FC = () => {
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
>
<Trash2 className="w-4 h-4" />
Delete Event
{t('events.deleteEvent')}
</button>
</div>
</div>