Merge pull request #593 from isaiastavares/fix/i18n-translations

fix(i18n): comprehensive translation audit and fixes across all 14 languages
This commit is contained in:
Maurice
2026-04-12 23:51:22 +02:00
committed by GitHub
38 changed files with 714 additions and 151 deletions
+2 -2
View File
@@ -100,7 +100,7 @@ describe('FilesPage', () => {
expect(screen.getByTestId('file-manager')).toBeInTheDocument();
});
expect(screen.getByText(/2 Dateien/)).toBeInTheDocument();
expect(screen.getByText(/2 files for/i)).toBeInTheDocument();
});
});
@@ -205,7 +205,7 @@ describe('FilesPage', () => {
expect(screen.getByTestId('file-manager')).toBeInTheDocument();
});
expect(screen.getByRole('heading', { name: /Dateien & Dokumente/i })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /Files & Documents/i })).toBeInTheDocument();
});
});
});
+2 -2
View File
@@ -78,8 +78,8 @@ export default function FilesPage(): React.ReactElement {
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Dateien & Dokumente</h1>
<p className="text-gray-500 text-sm">{files.length} Dateien für {trip?.name}</p>
<h1 className="text-2xl font-bold text-gray-900">{t('files.pageTitle')}</h1>
<p className="text-gray-500 text-sm">{t('files.subtitle', { count: files.length, trip: trip?.name })}</p>
</div>
</div>
+31 -31
View File
@@ -563,9 +563,9 @@ export default function JourneyDetailPage() {
setDeleteTarget(null)
loadJourney(Number(id))
}}
title="Delete Entry"
message={`Delete "${deleteTarget?.title || 'this entry'}"? This cannot be undone.`}
confirmLabel="Delete"
title={t('journey.entries.deleteTitle')}
message={t('journey.deleteConfirmMessage', { title: deleteTarget?.title || 'this entry' })}
confirmLabel={t('common.delete')}
danger
/>
@@ -584,9 +584,9 @@ export default function JourneyDetailPage() {
toast.error(t('journey.trips.unlinkFailed'))
}
}}
title="Unlink Trip"
message={`Unlink "${unlinkTrip?.title}"? All synced entries and photos from this trip will be permanently deleted. This cannot be undone.`}
confirmLabel="Unlink"
title={t('journey.trips.unlinkTrip')}
message={t('journey.trips.unlinkMessage', { title: unlinkTrip?.title })}
confirmLabel={t('journey.trips.unlink')}
danger
/>
@@ -811,7 +811,7 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres
for (const f of files) formData.append('photos', f)
try {
await journeyApi.uploadPhotos(entryId, formData)
toast.success(`${files.length} photos uploaded`)
toast.success(t('journey.photosUploaded', { count: files.length }))
onRefresh()
} catch {
toast.error(t('journey.settings.coverFailed'))
@@ -938,7 +938,7 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres
} catch {}
}
if (added > 0) {
toast.success(`${added} photos added`)
toast.success(t('journey.photosAdded', { count: added }))
onRefresh()
}
setShowPicker(false)
@@ -1179,8 +1179,8 @@ function EntryCard({ entry, onEdit, onDelete, onPhotoClick }: {
<>
<div className="fixed inset-0 z-[99]" onClick={() => setMenuOpen(false)} />
<div className="fixed z-[100] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-lg py-1 min-w-[120px]" style={{ top: (menuBtnRef.current?.getBoundingClientRect().bottom || 0) + 4, right: window.innerWidth - (menuBtnRef.current?.getBoundingClientRect().right || 0) }}>
<button onClick={() => { setMenuOpen(false); onEdit() }} className="w-full text-left px-3 py-1.5 text-[12px] text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700 flex items-center gap-2"><Pencil size={12} /> Edit</button>
<button onClick={() => { setMenuOpen(false); onDelete() }} className="w-full text-left px-3 py-1.5 text-[12px] text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2"><Trash2 size={12} /> Delete</button>
<button onClick={() => { setMenuOpen(false); onEdit() }} className="w-full text-left px-3 py-1.5 text-[12px] text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700 flex items-center gap-2"><Pencil size={12} /> {t('common.edit')}</button>
<button onClick={() => { setMenuOpen(false); onDelete() }} className="w-full text-left px-3 py-1.5 text-[12px] text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2"><Trash2 size={12} /> {t('common.delete')}</button>
</div>
</>
)}
@@ -2177,7 +2177,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
<button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">{t('common.cancel')}</button>
<button onClick={handleSave} disabled={saving} className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-50">
{saving ? 'Saving...' : 'Save'}
{saving ? t('common.saving') : t('common.save')}
</button>
</div>
</div>
@@ -2546,7 +2546,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
await updateJourney(journey.id, { title, subtitle: subtitle || null })
onSaved()
} catch {
toast.error('Failed to save')
toast.error(t('journey.settings.saveFailed'))
} finally {
setSaving(false)
}
@@ -2559,10 +2559,10 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
formData.append('cover', file)
try {
await journeyApi.uploadCover(journey.id, formData)
toast.success('Cover updated')
toast.success(t('journey.settings.coverUpdated'))
onSaved()
} catch {
toast.error('Upload failed')
toast.error(t('journey.settings.coverFailed'))
}
}
@@ -2573,7 +2573,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
await deleteJourney(journey.id)
navigate('/journey')
} catch {
toast.error('Failed to delete')
toast.error(t('journey.settings.failedToDelete'))
}
}
@@ -2633,14 +2633,14 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
{/* Synced Trips */}
<div>
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">Synced Trips</label>
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.detail.syncedTrips')}</label>
<div className="flex flex-col gap-1.5">
{journey.trips.map((trip: any) => (
<div key={trip.trip_id} className="flex items-center gap-2.5 p-2 rounded-lg bg-zinc-50 dark:bg-zinc-800">
<div className="w-8 h-8 rounded-md flex-shrink-0" style={{ background: pickGradient(trip.trip_id) }} />
<div className="flex-1 min-w-0">
<div className="text-[12px] font-medium text-zinc-900 dark:text-white">{trip.title}</div>
<div className="text-[10px] text-zinc-500">{trip.place_count || 0} places</div>
<div className="text-[10px] text-zinc-500">{trip.place_count || 0} {t('journey.synced.places')}</div>
</div>
<button
onClick={() => setUnlinkTarget({ trip_id: trip.trip_id, title: trip.title })}
@@ -2651,19 +2651,19 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
</button>
</div>
))}
{journey.trips.length === 0 && <p className="text-[11px] text-zinc-400">No trips linked</p>}
{journey.trips.length === 0 && <p className="text-[11px] text-zinc-400">{t('journey.trips.noTripsLinkedSettings')}</p>}
<button
onClick={() => setShowAddTrip(true)}
className="w-full mt-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-600 text-[12px] font-medium text-zinc-500 hover:border-zinc-400 hover:text-zinc-700 dark:hover:border-zinc-500 dark:hover:text-zinc-300 transition-colors"
>
<Plus size={14} /> Add Trip
<Plus size={14} /> {t('journey.trips.addTrip')}
</button>
</div>
</div>
{/* Contributors */}
<div>
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">Contributors</label>
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.detail.contributors')}</label>
<div className="flex flex-col gap-2">
{journey.contributors.map((c: any) => (
<div key={c.user_id} className="flex items-center gap-2.5">
@@ -2678,7 +2678,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
onClick={onOpenInvite}
className="w-full mt-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-600 text-[12px] font-medium text-zinc-500 hover:border-zinc-400 hover:text-zinc-700 dark:hover:border-zinc-500 dark:hover:text-zinc-300 transition-colors"
>
<UserPlus size={14} /> Invite Contributor
<UserPlus size={14} /> {t('journey.contributors.invite')}
</button>
</div>
</div>
@@ -2697,11 +2697,11 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
className="flex items-center gap-1.5 text-[12px] font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg px-2.5 py-2 mr-auto"
>
<Trash2 size={13} />
Delete
{t('journey.settings.delete')}
</button>
<button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">{t('common.cancel')}</button>
<button onClick={handleSave} disabled={saving || !title.trim()} className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-40">
{saving ? 'Saving...' : 'Save'}
{saving ? t('common.saving') : t('common.save')}
</button>
</div>
</div>
@@ -2714,16 +2714,16 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
if (!unlinkTarget) return
try {
await journeyApi.removeTrip(journey.id, unlinkTarget.trip_id)
toast.success('Trip unlinked')
toast.success(t('journey.trips.tripUnlinked'))
setUnlinkTarget(null)
onSaved()
} catch {
toast.error('Failed to unlink trip')
toast.error(t('journey.trips.unlinkFailed'))
}
}}
title="Unlink Trip"
message={`Unlink "${unlinkTarget?.title}"? All synced entries and photos from this trip will be permanently deleted. This cannot be undone.`}
confirmLabel="Unlink"
title={t('journey.trips.unlinkTrip')}
message={t('journey.trips.unlinkMessage', { title: unlinkTarget?.title })}
confirmLabel={t('journey.trips.unlink')}
danger
/>
@@ -2741,9 +2741,9 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
isOpen={showDeleteConfirm}
onClose={() => setShowDeleteConfirm(false)}
onConfirm={handleDelete}
title="Delete Journey"
message={`Delete "${journey.title}"? All entries and photos will be lost.`}
confirmLabel="Delete"
title={t('journey.settings.deleteJourney')}
message={t('journey.settings.deleteMessage', { title: journey.title })}
confirmLabel={t('common.delete')}
danger
/>
</div>
+5 -5
View File
@@ -65,7 +65,7 @@ export default function LoginPage(): React.ReactElement {
authApi.validateInvite(invite).then(() => {
setInviteValid(true)
}).catch(() => {
setError('Invalid or expired invite link')
setError(t('login.invalidInviteLink'))
})
window.history.replaceState({}, '', window.location.pathname)
}
@@ -82,12 +82,12 @@ export default function LoginPage(): React.ReactElement {
await loadUser()
navigate('/dashboard', { replace: true })
} else {
setError(data.error || 'OIDC login failed')
setError(data.error || t('login.oidcFailed'))
}
})
.catch(() => {
window.history.replaceState({}, '', '/login')
setError('OIDC login failed')
setError(t('login.oidcFailed'))
})
.finally(() => setIsLoading(false))
return
@@ -172,8 +172,8 @@ export default function LoginPage(): React.ReactElement {
return
}
if (mode === 'register') {
if (!username.trim()) { setError('Username is required'); setIsLoading(false); return }
if (password.length < 8) { setError('Password must be at least 8 characters'); setIsLoading(false); return }
if (!username.trim()) { setError(t('login.usernameRequired')); setIsLoading(false); return }
if (password.length < 8) { setError(t('login.passwordMinLength')); setIsLoading(false); return }
await register(username, email, password, inviteToken || undefined)
} else {
const result = await login(email, password)
+2 -2
View File
@@ -118,7 +118,7 @@ describe('PhotosPage', () => {
expect(screen.getByTestId('photo-gallery')).toBeInTheDocument();
});
expect(screen.getByText(/1 Fotos/)).toBeInTheDocument();
expect(screen.getByText(/1 photos for/i)).toBeInTheDocument();
});
});
@@ -224,7 +224,7 @@ describe('PhotosPage', () => {
expect(screen.getByTestId('photo-gallery')).toBeInTheDocument();
});
expect(screen.getByRole('heading', { name: /fotos/i })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /photos/i })).toBeInTheDocument();
});
});
});
+2 -2
View File
@@ -89,8 +89,8 @@ export default function PhotosPage(): React.ReactElement {
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Fotos</h1>
<p className="text-gray-500 text-sm">{photos.length} Fotos für {trip?.name}</p>
<h1 className="text-2xl font-bold text-gray-900">{t('photos.title')}</h1>
<p className="text-gray-500 text-sm">{t('photos.subtitle', { count: photos.length, trip: trip?.name })}</p>
</div>
</div>
+8 -8
View File
@@ -366,7 +366,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
}
})
}
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) }
}, [deletePlaceId, tripId, toast, selectedPlaceId, pushUndo])
const handleAssignToDay = useCallback(async (placeId, dayId, position) => {
@@ -383,7 +383,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
await tripActions.removeAssignment(tripId, capturedTarget, capturedAssignmentId)
})
}
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) }
}, [selectedDayId, tripId, toast, updateRouteForDay, pushUndo])
const handleRemoveAssignment = useCallback(async (dayId, assignmentId) => {
@@ -401,7 +401,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
})
}
}
catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) }
}, [tripId, toast, updateRouteForDay, pushUndo])
const handleReorder = useCallback((dayId, orderedIds) => {
@@ -430,7 +430,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
const handleUpdateDayTitle = useCallback(async (dayId, title) => {
try { await tripActions.updateDayTitle(tripId, dayId, title) }
catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) }
}, [tripId, toast])
const handleSaveReservation = async (data) => {
@@ -453,7 +453,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
}
return r
}
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) }
}
const handleDeleteReservation = async (id) => {
@@ -463,7 +463,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
// Refresh accommodations in case a hotel booking was deleted
accommodationsApi.list(tripId).then(d => setTripAccommodations(d.accommodations || [])).catch(() => {})
}
catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) }
}
const selectedPlace = selectedPlaceId ? places.find(p => p.id === selectedPlaceId) : null
@@ -818,7 +818,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
}))
} catch {}
}}
onUpdatePlace={async (placeId, data) => { try { await tripActions.updatePlace(tripId, placeId, data) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') } }}
onUpdatePlace={async (placeId, data) => { try { await tripActions.updatePlace(tripId, placeId, data) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) } }}
leftWidth={(isMobile || window.innerWidth < 900) ? 0 : (leftCollapsed ? 0 : leftWidth)}
rightWidth={(isMobile || window.innerWidth < 900) ? 0 : (rightCollapsed ? 0 : rightWidth)}
/>
@@ -867,7 +867,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
}))
} catch {}
}}
onUpdatePlace={async (placeId, data) => { try { await tripActions.updatePlace(tripId, placeId, data) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') } }}
onUpdatePlace={async (placeId, data) => { try { await tripActions.updatePlace(tripId, placeId, data) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) } }}
leftWidth={0}
rightWidth={0}
/>