fix(i18n): comprehensive translation audit and fixes across all 14 languages

- Fix critical bug: Photos and Files pages had German text hardcoded in JSX,
  now use t() keys visible correctly in all languages
- Add 16 new translation keys (photos/files UI, login validation, common errors,
  rate limit message) across all 14 language files
- Add missing keys in packing, memories, and budget sections for br, de, it, es,
  fr, nl, pl, cs, hu, ru, zh, zh-TW, ar
- Add 152+ missing keys for zh-TW (entire sections were absent)
- Change Vacay addon name to 'Férias' in pt-BR only
- Add client-side HTTP 429 interceptor that shows translated rate limit message
- Replace hardcoded English fallbacks in TripPlannerPage, DayPlanSidebar,
  DisplaySettingsTab, MapSettingsTab, AccountTab, and TodoListPanel with t()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Isaias Tavares
2026-04-12 09:35:22 -03:00
parent 7abfb4deba
commit 9c42a01391
27 changed files with 357 additions and 56 deletions
+40 -1
View File
@@ -1,5 +1,34 @@
import axios, { AxiosInstance } from 'axios'
import { getSocketId } from './websocket'
import en from '../i18n/translations/en'
import br from '../i18n/translations/br'
import de from '../i18n/translations/de'
import es from '../i18n/translations/es'
import fr from '../i18n/translations/fr'
import it from '../i18n/translations/it'
import nl from '../i18n/translations/nl'
import pl from '../i18n/translations/pl'
import cs from '../i18n/translations/cs'
import hu from '../i18n/translations/hu'
import ru from '../i18n/translations/ru'
import zh from '../i18n/translations/zh'
import zhTw from '../i18n/translations/zhTw'
import ar from '../i18n/translations/ar'
const rateLimitTranslations: Record<string, Record<string, string | unknown>> = {
en, br, de, es, fr, it, nl, pl, cs, hu, ru, zh, 'zh-TW': zhTw, ar,
}
function translateRateLimit(): string {
const fallback = 'Too many attempts. Please try again later.'
try {
const lang = localStorage.getItem('app_language') || 'en'
const table = rateLimitTranslations[lang] || rateLimitTranslations.en
return (table['common.tooManyAttempts'] as string) || (rateLimitTranslations.en['common.tooManyAttempts'] as string) || fallback
} catch {
return fallback
}
}
export const apiClient: AxiosInstance = axios.create({
baseURL: '/api',
@@ -21,7 +50,7 @@ apiClient.interceptors.request.use(
(error) => Promise.reject(error)
)
// Response interceptor - handle 401
// Response interceptor - handle 401, 403 MFA, 429 rate limit
apiClient.interceptors.response.use(
(response) => response,
(error) => {
@@ -38,6 +67,16 @@ apiClient.interceptors.response.use(
) {
window.location.href = '/settings?mfa=required'
}
if (error.response?.status === 429) {
const translated = translateRateLimit()
const data = error.response.data as { error?: string } | undefined
if (data && typeof data === 'object') {
data.error = translated
} else {
error.response.data = { error: translated }
}
error.message = translated
}
return Promise.reject(error)
}
)
+1 -1
View File
@@ -778,7 +778,7 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate,
title={previewFile.original_name}
>
<p style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>
<button onClick={async () => { const u = await getAuthUrl(previewFile.url, 'download'); window.open(u, '_blank', 'noopener noreferrer') }} style={{ color: 'var(--text-primary)', textDecoration: 'underline', background: 'none', border: 'none', cursor: 'pointer', font: 'inherit' }}>PDF herunterladen</button>
<button onClick={async () => { const u = await getAuthUrl(previewFile.url, 'download'); window.open(u, '_blank', 'noopener noreferrer') }} style={{ color: 'var(--text-primary)', textDecoration: 'underline', background: 'none', border: 'none', cursor: 'pointer', font: 'inherit' }}>{t('files.downloadPdf')}</button>
</p>
</object>
</div>
@@ -149,7 +149,7 @@ export function PhotoLightbox({ photos, initialIndex, onClose, onUpdate, onDelet
value={caption}
onChange={e => setCaption(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSaveCaption()}
placeholder="Beschriftung hinzufügen..."
placeholder={t('photos.addCaption')}
className="flex-1 bg-white/10 text-white border border-white/20 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:border-white/40"
autoFocus
/>
@@ -173,7 +173,7 @@ export function PhotoLightbox({ photos, initialIndex, onClose, onUpdate, onDelet
className="text-white text-sm flex-1 cursor-pointer hover:text-white/80"
onClick={() => setEditCaption(true)}
>
{photo.caption || <span className="text-white/40 italic">Beschriftung hinzufügen...</span>}
{photo.caption || <span className="text-white/40 italic">{t('photos.addCaption')}</span>}
</p>
<button
onClick={() => setEditCaption(true)}
+4 -4
View File
@@ -85,10 +85,10 @@ export function PhotoUpload({ tripId, days, places, onUpload, onClose }: PhotoUp
<input {...getInputProps()} />
<Upload className={`w-10 h-10 mx-auto mb-3 ${isDragActive ? 'text-slate-900' : 'text-gray-400'}`} />
{isDragActive ? (
<p className="text-slate-700 font-medium">Fotos hier ablegen...</p>
<p className="text-slate-700 font-medium">{t('photos.dropHere')}</p>
) : (
<>
<p className="text-gray-600 font-medium">Fotos hier ablegen</p>
<p className="text-gray-600 font-medium">{t('photos.dropHereActive')}</p>
<p className="text-gray-400 text-sm mt-1">{t('photos.clickToSelect')}</p>
<p className="text-gray-400 text-xs mt-2">JPG, PNG, WebP · max. 10 MB · bis zu 30 Fotos</p>
</>
@@ -152,12 +152,12 @@ export function PhotoUpload({ tripId, days, places, onUpload, onClose }: PhotoUp
</select>
</div>
<div className="col-span-2">
<label className="block text-xs font-medium text-gray-700 mb-1">Beschriftung (für alle)</label>
<label className="block text-xs font-medium text-gray-700 mb-1">{t('photos.captionForAll')}</label>
<input
type="text"
value={caption}
onChange={e => setCaption(e.target.value)}
placeholder="Optionale Beschriftung..."
placeholder={t('photos.captionPlaceholder')}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-slate-900"
/>
</div>
@@ -616,7 +616,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
await tripActions.reorderAssignments(tripId, capturedDayId, capturedPrevIds)
})
}
} 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 handleMergedDrop = async (dayId, fromType, fromId, toType, toId, insertAfter = false) => {
@@ -703,7 +703,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
tripActions.setAssignments(currentAssignments)
}
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Unknown error')
toast.error(err instanceof Error ? err.message : t('common.unknownError'))
return
}
@@ -852,9 +852,9 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
await tripActions.moveAssignment(tripId, Number(assignmentId), dayId, capturedFromDayId, capturedOrderIndex)
})
})
.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')))
} else if (noteId && fromDayId !== dayId) {
tripActions.moveDayNote(tripId, fromDayId, dayId, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveDayNote(tripId, fromDayId, dayId, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
}
setDraggingId(null)
setDropTargetKey(null)
@@ -959,7 +959,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
a.download = `${trip?.title || 'trip'}.ics`
a.click()
URL.revokeObjectURL(url)
} catch { toast.error('ICS export failed') }
} catch { toast.error(t('planner.icsExportFailed')) }
}}
onMouseEnter={() => setIcsHover(true)}
onMouseLeave={() => setIcsHover(false)}
@@ -1186,11 +1186,11 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
if (placeId) {
onAssignToDay?.(parseInt(placeId), day.id)
} else if (assignmentId && fromDayId !== day.id) {
tripActions.moveAssignment(tripId, Number(assignmentId), fromDayId, day.id).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveAssignment(tripId, Number(assignmentId), fromDayId, day.id).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
} else if (assignmentId) {
handleMergedDrop(day.id, 'place', Number(assignmentId), 'transport', transportId, isAfter)
} else if (noteId && fromDayId !== day.id) {
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
} else if (noteId) {
handleMergedDrop(day.id, 'note', Number(noteId), 'transport', transportId, isAfter)
}
@@ -1204,11 +1204,11 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
setDropTargetKey(null); window.__dragData = null; return
}
if (assignmentId && fromDayId !== day.id) {
tripActions.moveAssignment(tripId, Number(assignmentId), fromDayId, day.id).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveAssignment(tripId, Number(assignmentId), fromDayId, day.id).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
setDraggingId(null); setDropTargetKey(null); dragDataRef.current = null; return
}
if (noteId && fromDayId !== day.id) {
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
setDraggingId(null); setDropTargetKey(null); dragDataRef.current = null; return
}
const m = getMergedItems(day.id)
@@ -1304,7 +1304,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
setDropTargetKey(null); window.__dragData = null
} else if (fromAssignmentId && fromDayId !== day.id) {
const toIdx = getDayAssignments(day.id).findIndex(a => a.id === assignment.id)
tripActions.moveAssignment(tripId, Number(fromAssignmentId), fromDayId, day.id, toIdx).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveAssignment(tripId, Number(fromAssignmentId), fromDayId, day.id, toIdx).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
setDraggingId(null); setDropTargetKey(null); dragDataRef.current = null
} else if (fromAssignmentId) {
handleMergedDrop(day.id, 'place', Number(fromAssignmentId), 'place', assignment.id)
@@ -1312,7 +1312,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
const tm = getMergedItems(day.id)
const toIdx = tm.findIndex(i => i.type === 'place' && i.data.id === assignment.id)
const so = toIdx <= 0 ? (tm[0]?.sortKey ?? 0) - 1 : (tm[toIdx - 1].sortKey + tm[toIdx].sortKey) / 2
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId), so).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId), so).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
setDraggingId(null); setDropTargetKey(null); dragDataRef.current = null
} else if (noteId) {
handleMergedDrop(day.id, 'note', Number(noteId), 'place', assignment.id)
@@ -1508,11 +1508,11 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
if (placeId) {
onAssignToDay?.(parseInt(placeId), day.id)
} else if (fromAssignmentId && fromDayId !== day.id) {
tripActions.moveAssignment(tripId, Number(fromAssignmentId), fromDayId, day.id).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveAssignment(tripId, Number(fromAssignmentId), fromDayId, day.id).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
} else if (fromAssignmentId) {
handleMergedDrop(day.id, 'place', Number(fromAssignmentId), 'transport', res.id, insertAfter)
} else if (noteId && fromDayId !== day.id) {
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
} else if (noteId) {
handleMergedDrop(day.id, 'note', Number(noteId), 'transport', res.id, insertAfter)
}
@@ -1596,7 +1596,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
const tm = getMergedItems(day.id)
const toIdx = tm.findIndex(i => i.type === 'note' && i.data.id === note.id)
const so = toIdx <= 0 ? (tm[0]?.sortKey ?? 0) - 1 : (tm[toIdx - 1].sortKey + tm[toIdx].sortKey) / 2
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(fromNoteId), so).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(fromNoteId), so).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
setDraggingId(null); setDropTargetKey(null)
} else if (fromNoteId && fromNoteId !== String(note.id)) {
handleMergedDrop(day.id, 'note', Number(fromNoteId), 'note', note.id)
@@ -1604,7 +1604,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
const tm = getMergedItems(day.id)
const noteIdx = tm.findIndex(i => i.type === 'note' && i.data.id === note.id)
const toIdx = tm.slice(0, noteIdx).filter(i => i.type === 'place').length
tripActions.moveAssignment(tripId, Number(fromAssignmentId), fromDayId, day.id, toIdx).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveAssignment(tripId, Number(fromAssignmentId), fromDayId, day.id, toIdx).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
setDraggingId(null); setDropTargetKey(null)
} else if (fromAssignmentId) {
handleMergedDrop(day.id, 'place', Number(fromAssignmentId), 'note', note.id)
@@ -1669,11 +1669,11 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
}
if (!assignmentId && !noteId) { dragDataRef.current = null; window.__dragData = null; return }
if (assignmentId && fromDayId !== day.id) {
tripActions.moveAssignment(tripId, Number(assignmentId), fromDayId, day.id).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveAssignment(tripId, Number(assignmentId), fromDayId, day.id).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
setDraggingId(null); setDropTargetKey(null); dragDataRef.current = null; return
}
if (noteId && fromDayId !== day.id) {
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : 'Unknown error'))
tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError')))
setDraggingId(null); setDropTargetKey(null); dragDataRef.current = null; return
}
const m = getMergedItems(day.id)
@@ -142,7 +142,7 @@ export default function AccountTab(): React.ReactElement {
await updateProfile({ username, email })
toast.success(t('settings.toast.profileSaved'))
} catch (err: unknown) {
toast.error(err instanceof Error ? err.message : 'Error')
toast.error(err instanceof Error ? err.message : t('common.error'))
} finally {
setSaving(false)
}
@@ -34,7 +34,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
onClick={async () => {
try {
await updateSetting('dark_mode', opt.value)
} catch (e: unknown) { toast.error(e instanceof Error ? e.message : 'Error') }
} catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
}}
style={{
display: 'flex', alignItems: 'center', gap: 6,
@@ -63,7 +63,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
key={opt.value}
onClick={async () => {
try { await updateSetting('language', opt.value) }
catch (e: unknown) { toast.error(e instanceof Error ? e.message : 'Error') }
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
}}
style={{
display: 'flex', alignItems: 'center', gap: 8,
@@ -94,7 +94,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
onClick={async () => {
setTempUnit(opt.value)
try { await updateSetting('temperature_unit', opt.value) }
catch (e: unknown) { toast.error(e instanceof Error ? e.message : 'Error') }
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
}}
style={{
display: 'flex', alignItems: 'center', gap: 8,
@@ -124,7 +124,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
key={opt.value}
onClick={async () => {
try { await updateSetting('time_format', opt.value) }
catch (e: unknown) { toast.error(e instanceof Error ? e.message : 'Error') }
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
}}
style={{
display: 'flex', alignItems: 'center', gap: 8,
@@ -154,7 +154,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
key={String(opt.value)}
onClick={async () => {
try { await updateSetting('route_calculation', opt.value) }
catch (e: unknown) { toast.error(e instanceof Error ? e.message : 'Error') }
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
}}
style={{
display: 'flex', alignItems: 'center', gap: 8,
@@ -184,7 +184,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
key={String(opt.value)}
onClick={async () => {
try { await updateSetting('blur_booking_codes', opt.value) }
catch (e: unknown) { toast.error(e instanceof Error ? e.message : 'Error') }
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
}}
style={{
display: 'flex', alignItems: 'center', gap: 8,
@@ -74,7 +74,7 @@ export default function MapSettingsTab(): React.ReactElement {
})
toast.success(t('settings.toast.mapSaved'))
} catch (err: unknown) {
toast.error(err instanceof Error ? err.message : 'Error')
toast.error(err instanceof Error ? err.message : t('common.error'))
} finally {
setSaving(false)
}
+4 -4
View File
@@ -105,7 +105,7 @@ export default function TodoListPanel({ tripId, items }: { tripId: number; items
if (!name || categories.includes(name)) { setAddingCategory(false); setNewCategoryName(''); return }
addTodoItem(tripId, { name: t('todo.newItem'), category: name } as any)
.then(() => { setAddingCategory(false); setNewCategoryName(''); setFilter(name) })
.catch(err => toast.error(err instanceof Error ? err.message : 'Error'))
.catch(err => toast.error(err instanceof Error ? err.message : t('common.error')))
}
// Get category count (non-done items)
@@ -479,7 +479,7 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
due_date: dueDate || null, category: category || null,
assigned_user_id: assignedUserId, priority,
} as any)
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Error') }
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) }
setSaving(false)
}
@@ -487,7 +487,7 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
try {
await deleteTodoItem(tripId, item.id)
onClose()
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Error') }
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) }
}
const labelStyle: React.CSSProperties = { fontSize: 12, fontWeight: 500, color: 'var(--text-secondary)', marginBottom: 4, display: 'block' }
@@ -663,7 +663,7 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated,
assigned_user_id: assignedUserId,
} as any)
if (item?.id) onCreated(item.id)
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Error') }
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) }
setSaving(false)
}
+18
View File
@@ -12,6 +12,8 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'common.loading': 'جارٍ التحميل...',
'common.import': 'استيراد',
'common.error': 'خطأ',
'common.unknownError': 'خطأ غير معروف',
'common.tooManyAttempts': 'محاولات كثيرة جدًا. يرجى المحاولة لاحقًا.',
'common.back': 'رجوع',
'common.all': 'الكل',
'common.close': 'إغلاق',
@@ -416,6 +418,10 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'login.mfaHint': 'افتح Google Authenticator أو Authy أو أي تطبيق TOTP آخر.',
'login.mfaBack': '← العودة لتسجيل الدخول',
'login.mfaVerify': 'تحقق',
'login.invalidInviteLink': 'رابط الدعوة غير صالح أو منتهي الصلاحية',
'login.oidcFailed': 'فشل تسجيل الدخول عبر OIDC',
'login.usernameRequired': 'اسم المستخدم مطلوب',
'login.passwordMinLength': 'يجب أن تكون كلمة المرور 8 أحرف على الأقل',
// Register
'register.passwordMismatch': 'كلمتا المرور غير متطابقتين',
@@ -1084,9 +1090,13 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'budget.settlement': 'التسوية',
'budget.settlementInfo': 'انقر على صورة العضو في بند الميزانية لتحديده باللون الأخضر — وهذا يعني أنه دفع. ثم تُظهر التسوية من يدين لمن وبكم.',
'budget.netBalances': 'الأرصدة الصافية',
'budget.linkedToReservation': 'مرتبط بحجز — قم بتحرير الاسم هناك',
// Files
'files.title': 'الملفات',
'files.pageTitle': 'الملفات والمستندات',
'files.subtitle': '{count} ملف لـ {trip}',
'files.downloadPdf': 'تنزيل PDF',
'files.count': '{count} ملفات',
'files.countSingular': 'ملف واحد',
'files.uploaded': 'تم رفع {count}',
@@ -1333,6 +1343,13 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'backup.keep.forever': 'الاحتفاظ للأبد',
// Photos
'photos.title': 'صور',
'photos.subtitle': '{count} صورة لـ {trip}',
'photos.dropHere': 'أسقط الصور هنا...',
'photos.dropHereActive': 'أسقط الصور هنا',
'photos.captionForAll': 'تعليق (للجميع)',
'photos.captionPlaceholder': 'تعليق اختياري...',
'photos.addCaption': 'إضافة تعليق...',
'photos.allDays': 'كل الأيام',
'photos.noPhotos': 'لا توجد صور بعد',
'photos.uploadHint': 'ارفع صور رحلتك',
@@ -1366,6 +1383,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'planner.routeCalculated': 'تم حساب المسار',
'planner.routeCalcFailed': 'تعذر حساب المسار',
'planner.routeError': 'خطأ أثناء حساب المسار',
'planner.icsExportFailed': 'فشل تصدير ICS',
'planner.routeOptimized': 'تم تحسين المسار',
'planner.reservationUpdated': 'تم تحديث الحجز',
'planner.reservationAdded': 'تمت إضافة الحجز',
+22 -1
View File
@@ -8,6 +8,8 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'common.loading': 'Carregando...',
'common.import': 'Importar',
'common.error': 'Erro',
'common.unknownError': 'Erro desconhecido',
'common.tooManyAttempts': 'Muitas tentativas. Tente novamente mais tarde.',
'common.back': 'Voltar',
'common.all': 'Todos',
'common.close': 'Fechar',
@@ -411,6 +413,10 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'login.mfaHint': 'Abra o Google Authenticator, Authy ou outro app TOTP.',
'login.mfaBack': '← Voltar ao login',
'login.mfaVerify': 'Verificar',
'login.invalidInviteLink': 'Link de convite inválido ou expirado',
'login.oidcFailed': 'Falha no login OIDC',
'login.usernameRequired': 'Nome de usuário é obrigatório',
'login.passwordMinLength': 'A senha deve ter pelo menos 8 caracteres',
// Register
'register.passwordMismatch': 'As senhas não coincidem',
@@ -1053,9 +1059,13 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'budget.settlement': 'Acerto',
'budget.settlementInfo': 'Clique no avatar de um membro em um item do orçamento para marcá-lo em verde — significa que ele pagou. O acerto mostra quem deve quanto a quem.',
'budget.netBalances': 'Saldos líquidos',
'budget.linkedToReservation': 'Vinculado a uma reserva — edite o nome lá',
// Files
'files.title': 'Arquivos',
'files.pageTitle': 'Arquivos e documentos',
'files.subtitle': '{count} arquivos para {trip}',
'files.downloadPdf': 'Baixar PDF',
'files.count': '{count} arquivos',
'files.countSingular': '1 arquivo',
'files.uploaded': '{count} enviado(s)',
@@ -1124,6 +1134,10 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'packing.allPacked': 'Tudo na mala!',
'packing.addPlaceholder': 'Adicionar item...',
'packing.categoryPlaceholder': 'Categoria...',
'packing.assignUser': 'Atribuir usuário',
'packing.saveAsTemplate': 'Salvar como modelo',
'packing.templateName': 'Nome do modelo',
'packing.templateSaved': 'Lista de bagagem salva como modelo',
'packing.filterAll': 'Todos',
'packing.filterOpen': 'Abertos',
'packing.filterDone': 'Prontos',
@@ -1134,7 +1148,6 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'packing.menuCheckAll': 'Marcar todos',
'packing.menuUncheckAll': 'Desmarcar todos',
'packing.menuDeleteCat': 'Excluir categoria',
'packing.assignUser': 'Atribuir usuário',
'packing.noMembers': 'Nenhum membro na viagem',
'packing.addItem': 'Adicionar item',
'packing.addItemPlaceholder': 'Nome do item...',
@@ -1302,6 +1315,13 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'backup.keep.forever': 'Manter para sempre',
// Photos
'photos.title': 'Fotos',
'photos.subtitle': '{count} fotos para {trip}',
'photos.dropHere': 'Arraste fotos aqui...',
'photos.dropHereActive': 'Arraste fotos aqui',
'photos.captionForAll': 'Legenda (para todos)',
'photos.captionPlaceholder': 'Legenda opcional...',
'photos.addCaption': 'Adicionar legenda...',
'photos.allDays': 'Todos os dias',
'photos.noPhotos': 'Nenhuma foto ainda',
'photos.uploadHint': 'Envie suas fotos de viagem',
@@ -1335,6 +1355,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'planner.routeCalculated': 'Rota calculada',
'planner.routeCalcFailed': 'Não foi possível calcular a rota',
'planner.routeError': 'Erro ao calcular a rota',
'planner.icsExportFailed': 'Falha ao exportar ICS',
'planner.routeOptimized': 'Rota otimizada',
'planner.reservationUpdated': 'Reserva atualizada',
'planner.reservationAdded': 'Reserva adicionada',
+18
View File
@@ -8,6 +8,8 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'common.loading': 'Načítání...',
'common.import': 'Importovat',
'common.error': 'Chyba',
'common.unknownError': 'Neznámá chyba',
'common.tooManyAttempts': 'Příliš mnoho pokusů. Zkuste to prosím znovu.',
'common.back': 'Zpět',
'common.all': 'Vše',
'common.close': 'Zavřít',
@@ -411,6 +413,10 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'login.mfaHint': 'Otevřete Google Authenticator, Authy nebo jinou TOTP aplikaci.',
'login.mfaBack': '← Zpět k přihlášení',
'login.mfaVerify': 'Ověřit',
'login.invalidInviteLink': 'Neplatný nebo vypršelý odkaz s pozvánkou',
'login.oidcFailed': 'Přihlášení přes OIDC se nezdařilo',
'login.usernameRequired': 'Uživatelské jméno je povinné',
'login.passwordMinLength': 'Heslo musí mít alespoň 8 znaků',
// Registrace (Register)
'register.passwordMismatch': 'Hesla se neshodují',
@@ -1082,9 +1088,13 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'budget.settlement': 'Vyúčtování',
'budget.settlementInfo': 'Klikněte na avatar člena u rozpočtové položky pro zelené označení to znamená, že zaplatil. Vyúčtování pak ukazuje, kdo komu a kolik dluží.',
'budget.netBalances': 'Čisté zůstatky',
'budget.linkedToReservation': 'Propojeno s rezervací — upravte název tam',
// Soubory (Files)
'files.title': 'Soubory',
'files.pageTitle': 'Soubory a dokumenty',
'files.subtitle': '{count} souborů pro {trip}',
'files.downloadPdf': 'Stáhnout PDF',
'files.count': '{count} souborů',
'files.countSingular': '1 soubor',
'files.uploaded': '{count} nahráno',
@@ -1331,6 +1341,13 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'backup.keep.forever': 'Uchovávat navždy',
// Fotky
'photos.title': 'Fotografie',
'photos.subtitle': '{count} fotek pro {trip}',
'photos.dropHere': 'Přetáhněte fotografie sem...',
'photos.dropHereActive': 'Přetáhněte fotografie sem',
'photos.captionForAll': 'Popisek (pro všechny)',
'photos.captionPlaceholder': 'Volitelný popisek...',
'photos.addCaption': 'Přidat popisek...',
'photos.allDays': 'Všechny dny',
'photos.noPhotos': 'Zatím žádné fotky',
'photos.uploadHint': 'Nahrajte své cestovní fotky',
@@ -1364,6 +1381,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'planner.routeCalculated': 'Trasa vypočtena',
'planner.routeCalcFailed': 'Trasu se nepodařilo vypočítat',
'planner.routeError': 'Chyba při výpočtu trasy',
'planner.icsExportFailed': 'Export ICS se nezdařil',
'planner.routeOptimized': 'Trasa optimalizována',
'planner.reservationUpdated': 'Rezervace aktualizována',
'planner.reservationAdded': 'Rezervace přidána',
+17
View File
@@ -8,6 +8,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'common.loading': 'Laden...',
'common.import': 'Importieren',
'common.error': 'Fehler',
'common.unknownError': 'Unbekannter Fehler',
'common.tooManyAttempts': 'Zu viele Versuche. Bitte versuchen Sie es später erneut.',
'common.back': 'Zurück',
'common.all': 'Alle',
'common.close': 'Schließen',
@@ -414,6 +416,10 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'login.mfaHint': 'Google Authenticator, Authy oder eine andere TOTP-App öffnen.',
'login.mfaBack': '← Zurück zur Anmeldung',
'login.mfaVerify': 'Bestätigen',
'login.invalidInviteLink': 'Ungültiger oder abgelaufener Einladungslink',
'login.oidcFailed': 'OIDC-Anmeldung fehlgeschlagen',
'login.usernameRequired': 'Benutzername ist erforderlich',
'login.passwordMinLength': 'Das Passwort muss mindestens 8 Zeichen lang sein',
// Register
'register.passwordMismatch': 'Passwörter stimmen nicht überein',
@@ -1087,6 +1093,9 @@ const de: Record<string, string | { name: string; category: string }[]> = {
// Files
'files.title': 'Dateien',
'files.pageTitle': 'Dateien & Dokumente',
'files.subtitle': '{count} Dateien für {trip}',
'files.downloadPdf': 'PDF herunterladen',
'files.count': '{count} Dateien',
'files.countSingular': '1 Datei',
'files.uploaded': '{count} hochgeladen',
@@ -1333,6 +1342,13 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'backup.keep.forever': 'Immer behalten',
// Photos
'photos.title': 'Fotos',
'photos.subtitle': '{count} Fotos für {trip}',
'photos.dropHere': 'Fotos hier ablegen...',
'photos.dropHereActive': 'Fotos hier ablegen',
'photos.captionForAll': 'Beschriftung (für alle)',
'photos.captionPlaceholder': 'Optionale Beschriftung...',
'photos.addCaption': 'Beschriftung hinzufügen...',
'photos.allDays': 'Alle Tage',
'photos.noPhotos': 'Noch keine Fotos',
'photos.uploadHint': 'Lade deine Reisefotos hoch',
@@ -1366,6 +1382,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'planner.routeCalculated': 'Route berechnet',
'planner.routeCalcFailed': 'Route konnte nicht berechnet werden',
'planner.routeError': 'Fehler bei der Routenberechnung',
'planner.icsExportFailed': 'ICS-Export fehlgeschlagen',
'planner.routeOptimized': 'Route optimiert',
'planner.reservationUpdated': 'Reservierung aktualisiert',
'planner.reservationAdded': 'Reservierung hinzugefügt',
+17 -1
View File
@@ -8,6 +8,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'common.loading': 'Loading...',
'common.import': 'Import',
'common.error': 'Error',
'common.unknownError': 'Unknown error',
'common.tooManyAttempts': 'Too many attempts. Please try again later.',
'common.back': 'Back',
'common.all': 'All',
'common.close': 'Close',
@@ -438,6 +440,10 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'login.mfaHint': 'Open Google Authenticator, Authy, or another TOTP app.',
'login.mfaBack': '← Back to sign in',
'login.mfaVerify': 'Verify',
'login.invalidInviteLink': 'Invalid or expired invite link',
'login.oidcFailed': 'OIDC login failed',
'login.usernameRequired': 'Username is required',
'login.passwordMinLength': 'Password must be at least 8 characters',
// Register
'register.passwordMismatch': 'Passwords do not match',
@@ -1109,6 +1115,9 @@ const en: Record<string, string | { name: string; category: string }[]> = {
// Files
'files.title': 'Files',
'files.pageTitle': 'Files & Documents',
'files.subtitle': '{count} files for {trip}',
'files.downloadPdf': 'Download PDF',
'files.count': '{count} files',
'files.countSingular': '1 file',
'files.uploaded': '{count} uploaded',
@@ -1200,7 +1209,6 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'packing.saveAsTemplate': 'Save as template',
'packing.templateName': 'Template name',
'packing.templateSaved': 'Packing list saved as template',
'packing.assignUser': 'Assign user',
'packing.bags': 'Bags',
'packing.noBag': 'Unassigned',
'packing.totalWeight': 'Total weight',
@@ -1356,6 +1364,13 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'backup.keep.forever': 'Keep forever',
// Photos
'photos.title': 'Photos',
'photos.subtitle': '{count} photos for {trip}',
'photos.dropHere': 'Drop photos here...',
'photos.dropHereActive': 'Drop photos here',
'photos.captionForAll': 'Caption (for all)',
'photos.captionPlaceholder': 'Optional caption...',
'photos.addCaption': 'Add caption...',
'photos.allDays': 'All Days',
'photos.noPhotos': 'No photos yet',
'photos.uploadHint': 'Upload your travel photos',
@@ -1389,6 +1404,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'planner.routeCalculated': 'Route calculated',
'planner.routeCalcFailed': 'Route could not be calculated',
'planner.routeError': 'Error calculating route',
'planner.icsExportFailed': 'ICS export failed',
'planner.routeOptimized': 'Route optimized',
'planner.reservationUpdated': 'Reservation updated',
'planner.reservationAdded': 'Reservation added',
+21
View File
@@ -8,6 +8,8 @@ const es: Record<string, string> = {
'common.loading': 'Cargando...',
'common.import': 'Importar',
'common.error': 'Error',
'common.unknownError': 'Error desconocido',
'common.tooManyAttempts': 'Demasiados intentos. Inténtelo de nuevo más tarde.',
'common.back': 'Atrás',
'common.all': 'Todo',
'common.close': 'Cerrar',
@@ -403,6 +405,10 @@ const es: Record<string, string> = {
'login.mfaHint': 'Abre Google Authenticator, Authy u otra app TOTP.',
'login.mfaBack': '← Volver al inicio de sesión',
'login.mfaVerify': 'Verificar',
'login.invalidInviteLink': 'Enlace de invitación inválido o expirado',
'login.oidcFailed': 'Error de inicio de sesión OIDC',
'login.usernameRequired': 'El nombre de usuario es obligatorio',
'login.passwordMinLength': 'La contraseña debe tener al menos 8 caracteres',
'login.oidc.tokenFailed': 'La autenticación falló.',
'login.oidc.invalidState': 'Sesión no válida. Inténtalo de nuevo.',
'login.demoFailed': 'Falló el acceso a la demo',
@@ -1040,9 +1046,13 @@ const es: Record<string, string> = {
'budget.settlement': 'Liquidación',
'budget.settlementInfo': 'Haz clic en el avatar de un miembro en una partida del presupuesto para marcarlo en verde — esto significa que ha pagado. La liquidación muestra quién debe cuánto a quién.',
'budget.netBalances': 'Saldos netos',
'budget.linkedToReservation': 'Vinculado a una reserva — edita el nombre allí',
// Files
'files.title': 'Archivos',
'files.pageTitle': 'Archivos y documentos',
'files.subtitle': '{count} archivos para {trip}',
'files.downloadPdf': 'Descargar PDF',
'files.count': '{count} archivos',
'files.countSingular': '1 archivo',
'files.uploaded': '{count} archivos subidos',
@@ -1111,6 +1121,9 @@ const es: Record<string, string> = {
'packing.templateName': 'Nombre de la plantilla',
'packing.templateSaved': 'Lista de equipaje guardada como plantilla',
'packing.assignUser': 'Asignar usuario',
'packing.saveAsTemplate': 'Guardar como plantilla',
'packing.templateName': 'Nombre de la plantilla',
'packing.templateSaved': 'Lista de equipaje guardada como plantilla',
'packing.noMembers': 'Sin miembros',
'packing.bags': 'Equipaje',
'packing.noBag': 'Sin asignar',
@@ -1267,6 +1280,13 @@ const es: Record<string, string> = {
'backup.keep.forever': 'Conservar para siempre',
// Photos
'photos.title': 'Fotos',
'photos.subtitle': '{count} fotos para {trip}',
'photos.dropHere': 'Suelta fotos aquí...',
'photos.dropHereActive': 'Suelta fotos aquí',
'photos.captionForAll': 'Leyenda (para todos)',
'photos.captionPlaceholder': 'Leyenda opcional...',
'photos.addCaption': 'Añadir leyenda...',
'photos.allDays': 'Todos los días',
'photos.noPhotos': 'Aún no hay fotos',
'photos.uploadHint': 'Sube y organiza las fotos compartidas de este viaje',
@@ -1316,6 +1336,7 @@ const es: Record<string, string> = {
'planner.routeCalculated': 'Ruta calculada',
'planner.routeCalcFailed': 'No se pudo calcular la ruta',
'planner.routeError': 'Error al calcular la ruta',
'planner.icsExportFailed': 'Error al exportar ICS',
'planner.routeOptimized': 'Ruta optimizada',
'planner.reservationUpdated': 'Reserva actualizada',
'planner.reservationAdded': 'Reserva añadida',
+21
View File
@@ -8,6 +8,8 @@ const fr: Record<string, string> = {
'common.loading': 'Chargement…',
'common.import': 'Importer',
'common.error': 'Erreur',
'common.unknownError': 'Erreur inconnue',
'common.tooManyAttempts': 'Trop de tentatives. Veuillez réessayer plus tard.',
'common.back': 'Retour',
'common.all': 'Tout',
'common.close': 'Fermer',
@@ -404,6 +406,10 @@ const fr: Record<string, string> = {
'login.mfaHint': 'Ouvrez Google Authenticator, Authy ou une autre application TOTP.',
'login.mfaBack': '← Retour à la connexion',
'login.mfaVerify': 'Vérifier',
'login.invalidInviteLink': 'Lien d\'invitation invalide ou expiré',
'login.oidcFailed': 'Échec de connexion OIDC',
'login.usernameRequired': 'Le nom d\'utilisateur est obligatoire',
'login.passwordMinLength': 'Le mot de passe doit comporter au moins 8 caractères',
'login.oidc.tokenFailed': 'L\'authentification a échoué.',
'login.oidc.invalidState': 'Session invalide. Veuillez réessayer.',
'login.demoFailed': 'Échec de la connexion démo',
@@ -1080,9 +1086,13 @@ const fr: Record<string, string> = {
'budget.settlement': 'Règlement',
'budget.settlementInfo': 'Cliquez sur l\'avatar d\'un membre sur un poste budgétaire pour le marquer en vert — cela signifie qu\'il a payé. Le règlement indique ensuite qui doit combien à qui.',
'budget.netBalances': 'Soldes nets',
'budget.linkedToReservation': 'Lié à une réservation — modifiez le nom là-bas',
// Files
'files.title': 'Fichiers',
'files.pageTitle': 'Fichiers et documents',
'files.subtitle': '{count} fichiers pour {trip}',
'files.downloadPdf': 'Télécharger le PDF',
'files.count': '{count} fichiers',
'files.countSingular': '1 fichier',
'files.uploaded': '{count} importés',
@@ -1173,6 +1183,9 @@ const fr: Record<string, string> = {
'packing.templateName': 'Nom du modèle',
'packing.templateSaved': 'Liste de voyage enregistrée comme modèle',
'packing.assignUser': 'Assigner un utilisateur',
'packing.saveAsTemplate': 'Enregistrer comme modèle',
'packing.templateName': 'Nom du modèle',
'packing.templateSaved': 'Liste de bagages enregistrée comme modèle',
'packing.noMembers': 'Aucun membre',
'packing.bags': 'Bagages',
'packing.noBag': 'Non assigné',
@@ -1329,6 +1342,13 @@ const fr: Record<string, string> = {
'backup.keep.forever': 'Conserver indéfiniment',
// Photos
'photos.title': 'Photos',
'photos.subtitle': '{count} photos pour {trip}',
'photos.dropHere': 'Déposez des photos ici...',
'photos.dropHereActive': 'Déposez des photos ici',
'photos.captionForAll': 'Légende (pour tous)',
'photos.captionPlaceholder': 'Légende optionnelle...',
'photos.addCaption': 'Ajouter une légende...',
'photos.allDays': 'Tous les jours',
'photos.noPhotos': 'Aucune photo',
'photos.uploadHint': 'Importez vos photos de voyage',
@@ -1362,6 +1382,7 @@ const fr: Record<string, string> = {
'planner.routeCalculated': 'Itinéraire calculé',
'planner.routeCalcFailed': 'L\'itinéraire n\'a pas pu être calculé',
'planner.routeError': 'Erreur lors du calcul de l\'itinéraire',
'planner.icsExportFailed': 'Échec de l\'export ICS',
'planner.routeOptimized': 'Itinéraire optimisé',
'planner.reservationUpdated': 'Réservation mise à jour',
'planner.reservationAdded': 'Réservation ajoutée',
+18
View File
@@ -8,6 +8,8 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'common.loading': 'Betöltés...',
'common.import': 'Importálás',
'common.error': 'Hiba',
'common.unknownError': 'Ismeretlen hiba',
'common.tooManyAttempts': 'Túl sok próbálkozás. Kérjük, próbálja újra később.',
'common.back': 'Vissza',
'common.all': 'Összes',
'common.close': 'Bezárás',
@@ -411,6 +413,10 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'login.mfaHint': 'Nyisd meg a Google Authenticator, Authy vagy más TOTP alkalmazást.',
'login.mfaBack': '← Vissza a bejelentkezéshez',
'login.mfaVerify': 'Ellenőrzés',
'login.invalidInviteLink': 'Érvénytelen vagy lejárt meghívólink',
'login.oidcFailed': 'OIDC bejelentkezés sikertelen',
'login.usernameRequired': 'A felhasználónév kötelező',
'login.passwordMinLength': 'A jelszónak legalább 8 karakter hosszúnak kell lennie',
// Regisztráció
'register.passwordMismatch': 'A jelszavak nem egyeznek',
@@ -1081,9 +1087,13 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'budget.settlement': 'Elszámolás',
'budget.settlementInfo': 'Kattints egy tag avatárjára egy költségvetési tételen a zöld jelöléshez — ez azt jelenti, hogy fizetett. Az elszámolás ezután mutatja, ki kinek mennyivel tartozik.',
'budget.netBalances': 'Nettó egyenlegek',
'budget.linkedToReservation': 'Foglaláshoz kapcsolva — ott módosítsa a nevet',
// Fájlok
'files.title': 'Fájlok',
'files.pageTitle': 'Fájlok és dokumentumok',
'files.subtitle': '{count} fájl a következőhöz: {trip}',
'files.downloadPdf': 'PDF letöltése',
'files.count': '{count} fájl',
'files.countSingular': '1 fájl',
'files.uploaded': '{count} feltöltve',
@@ -1330,6 +1340,13 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'backup.keep.forever': 'Örökre megőrzés',
// Fotók
'photos.title': 'Fotók',
'photos.subtitle': '{count} fotó a következőhöz: {trip}',
'photos.dropHere': 'Húzza ide a fényképeket...',
'photos.dropHereActive': 'Húzza ide a fényképeket',
'photos.captionForAll': 'Felirat (mindenkinek)',
'photos.captionPlaceholder': 'Opcionális felirat...',
'photos.addCaption': 'Felirat hozzáadása...',
'photos.allDays': 'Minden nap',
'photos.noPhotos': 'Még nincsenek fotók',
'photos.uploadHint': 'Töltsd fel az úti fotóidat',
@@ -1363,6 +1380,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'planner.routeCalculated': 'Útvonal kiszámítva',
'planner.routeCalcFailed': 'Nem sikerült kiszámítani az útvonalat',
'planner.routeError': 'Hiba az útvonalszámítás során',
'planner.icsExportFailed': 'Az ICS-exportálás sikertelen',
'planner.routeOptimized': 'Útvonal optimalizálva',
'planner.reservationUpdated': 'Foglalás frissítve',
'planner.reservationAdded': 'Foglalás hozzáadva',
+18
View File
@@ -8,6 +8,8 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'common.loading': 'Caricamento...',
'common.import': 'Importa',
'common.error': 'Errore',
'common.unknownError': 'Errore sconosciuto',
'common.tooManyAttempts': 'Troppi tentativi. Riprova più tardi.',
'common.back': 'Indietro',
'common.all': 'Tutti',
'common.close': 'Chiudi',
@@ -411,6 +413,10 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'login.mfaHint': 'Apri Google Authenticator, Authy o un\'altra app TOTP.',
'login.mfaBack': '← Torna all\'accesso',
'login.mfaVerify': 'Verifica',
'login.invalidInviteLink': 'Link di invito non valido o scaduto',
'login.oidcFailed': 'Accesso OIDC non riuscito',
'login.usernameRequired': 'Il nome utente è obbligatorio',
'login.passwordMinLength': 'La password deve contenere almeno 8 caratteri',
// Register
'register.passwordMismatch': 'Le password non corrispondono',
@@ -1081,9 +1087,13 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'budget.settlement': 'Regolamento',
'budget.settlementInfo': 'Clicca sull\'avatar di un membro su una voce di budget per contrassegnarlo in verde — significa che ha pagato. Il regolamento mostra poi chi deve quanto a chi.',
'budget.netBalances': 'Saldi netti',
'budget.linkedToReservation': 'Collegato a una prenotazione — modifica il nome lì',
// Files
'files.title': 'File',
'files.pageTitle': 'File e documenti',
'files.subtitle': '{count} file per {trip}',
'files.downloadPdf': 'Scarica PDF',
'files.count': '{count} file',
'files.countSingular': '1 documento',
'files.uploaded': '{count} caricati',
@@ -1330,6 +1340,13 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'backup.keep.forever': 'Conserva per sempre',
// Photos
'photos.title': 'Foto',
'photos.subtitle': '{count} foto per {trip}',
'photos.dropHere': 'Trascina le foto qui...',
'photos.dropHereActive': 'Trascina le foto qui',
'photos.captionForAll': 'Didascalia (per tutti)',
'photos.captionPlaceholder': 'Didascalia opzionale...',
'photos.addCaption': 'Aggiungi didascalia...',
'photos.allDays': 'Tutti i giorni',
'photos.noPhotos': 'Ancora nessuna foto',
'photos.uploadHint': 'Carica le foto del tuo viaggio',
@@ -1363,6 +1380,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'planner.routeCalculated': 'Percorso calcolato',
'planner.routeCalcFailed': 'Il percorso non è stato calcolato',
'planner.routeError': 'Errore nel calcolo del percorso',
'planner.icsExportFailed': 'Esportazione ICS non riuscita',
'planner.routeOptimized': 'Percorso ottimizzato',
'planner.reservationUpdated': 'Prenotazione aggiornata',
'planner.reservationAdded': 'Prenotazione aggiunta',
+18
View File
@@ -8,6 +8,8 @@ const nl: Record<string, string> = {
'common.loading': 'Laden...',
'common.import': 'Importeren',
'common.error': 'Fout',
'common.unknownError': 'Onbekende fout',
'common.tooManyAttempts': 'Te veel pogingen. Probeer het later opnieuw.',
'common.back': 'Terug',
'common.all': 'Alles',
'common.close': 'Sluiten',
@@ -404,6 +406,10 @@ const nl: Record<string, string> = {
'login.mfaHint': 'Open Google Authenticator, Authy of een andere TOTP-app.',
'login.mfaBack': '← Terug naar inloggen',
'login.mfaVerify': 'Verifiëren',
'login.invalidInviteLink': 'Ongeldige of verlopen uitnodigingslink',
'login.oidcFailed': 'OIDC-aanmelding mislukt',
'login.usernameRequired': 'Gebruikersnaam is vereist',
'login.passwordMinLength': 'Wachtwoord moet minimaal 8 tekens bevatten',
'login.oidc.tokenFailed': 'Authenticatie mislukt.',
'login.oidc.invalidState': 'Ongeldige sessie. Probeer het opnieuw.',
'login.demoFailed': 'Demo-login mislukt',
@@ -1080,9 +1086,13 @@ const nl: Record<string, string> = {
'budget.settlement': 'Afrekening',
'budget.settlementInfo': 'Klik op de avatar van een lid bij een budgetpost om deze groen te markeren — dit betekent dat diegene heeft betaald. De afrekening toont vervolgens wie wie hoeveel verschuldigd is.',
'budget.netBalances': 'Nettosaldi',
'budget.linkedToReservation': 'Gekoppeld aan een reservering — bewerk de naam daar',
// Files
'files.title': 'Bestanden',
'files.pageTitle': 'Bestanden en documenten',
'files.subtitle': '{count} bestanden voor {trip}',
'files.downloadPdf': 'PDF downloaden',
'files.count': '{count} bestanden',
'files.countSingular': '1 bestand',
'files.uploaded': '{count} geüpload',
@@ -1329,6 +1339,13 @@ const nl: Record<string, string> = {
'backup.keep.forever': 'Voor altijd bewaren',
// Photos
'photos.title': 'Foto\'s',
'photos.subtitle': '{count} foto\'s voor {trip}',
'photos.dropHere': 'Foto\'s hier neerzetten...',
'photos.dropHereActive': 'Foto\'s hier neerzetten',
'photos.captionForAll': 'Bijschrift (voor alle)',
'photos.captionPlaceholder': 'Optioneel bijschrift...',
'photos.addCaption': 'Bijschrift toevoegen...',
'photos.allDays': 'Alle dagen',
'photos.noPhotos': 'Nog geen foto\'s',
'photos.uploadHint': 'Upload je reisfoto\'s',
@@ -1362,6 +1379,7 @@ const nl: Record<string, string> = {
'planner.routeCalculated': 'Route berekend',
'planner.routeCalcFailed': 'Route kon niet worden berekend',
'planner.routeError': 'Fout bij routeberekening',
'planner.icsExportFailed': 'ICS-export mislukt',
'planner.routeOptimized': 'Route geoptimaliseerd',
'planner.reservationUpdated': 'Reservering bijgewerkt',
'planner.reservationAdded': 'Reservering toegevoegd',
+21
View File
@@ -7,6 +7,8 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'common.add': 'Dodaj',
'common.loading': 'Ładowanie...',
'common.error': 'Błąd',
'common.unknownError': 'Nieznany błąd',
'common.tooManyAttempts': 'Zbyt wiele prób. Spróbuj ponownie później.',
'common.back': 'Wstecz',
'common.all': 'Wszystko',
'common.close': 'Zamknij',
@@ -381,6 +383,10 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'login.mfaHint': 'Otwórz Google Authenticator, Authy lub inną aplikację TOTP.',
'login.mfaBack': '← Powrót do logowania',
'login.mfaVerify': 'Weryfikuj',
'login.invalidInviteLink': 'Nieprawidłowy lub wygasły link zaproszenia',
'login.oidcFailed': 'Logowanie OIDC nie powiodło się',
'login.usernameRequired': 'Nazwa użytkownika jest wymagana',
'login.passwordMinLength': 'Hasło musi mieć co najmniej 8 znaków',
// Register
'register.passwordMismatch': 'Hasła nie są identyczne',
@@ -1042,6 +1048,9 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
// Files
'files.title': 'Pliki',
'files.pageTitle': 'Pliki i dokumenty',
'files.subtitle': '{count} plików dla {trip}',
'files.downloadPdf': 'Pobierz PDF',
'files.count': '{count} plików',
'files.countSingular': '1 plik',
'files.uploaded': '{count} przesłanych',
@@ -1121,6 +1130,9 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'packing.menuUncheckAll': 'Odznacz wszystko',
'packing.menuDeleteCat': 'Usuń kategorię',
'packing.assignUser': 'Przypisz użytkownika',
'packing.saveAsTemplate': 'Zapisz jako szablon',
'packing.templateName': 'Nazwa szablonu',
'packing.templateSaved': 'Lista pakowania zapisana jako szablon',
'packing.noMembers': 'Brak członków podróży',
'packing.addItem': 'Dodaj przedmiot',
'packing.addItemPlaceholder': 'Nazwa przedmiotu...',
@@ -1288,6 +1300,13 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'backup.keep.forever': 'Przechowuj na zawsze',
// Photos
'photos.title': 'Zdjęcia',
'photos.subtitle': '{count} zdjęć dla {trip}',
'photos.dropHere': 'Przeciągnij zdjęcia tutaj...',
'photos.dropHereActive': 'Przeciągnij zdjęcia tutaj',
'photos.captionForAll': 'Podpis (dla wszystkich)',
'photos.captionPlaceholder': 'Opcjonalny podpis...',
'photos.addCaption': 'Dodaj podpis...',
'photos.allDays': 'Wszystkie dni',
'photos.noPhotos': 'Brak zdjęć',
'photos.uploadHint': 'Prześlij zdjęcia z podróży',
@@ -1321,6 +1340,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'planner.routeCalculated': 'Trasa została obliczona',
'planner.routeCalcFailed': 'Nie udało się obliczyć trasy',
'planner.routeError': 'Błąd obliczania trasy',
'planner.icsExportFailed': 'Eksport ICS nie powiódł się',
'planner.routeOptimized': 'Trasa została zoptymalizowana',
'planner.reservationUpdated': 'Rezerwacja została zaktualizowana',
'planner.reservationAdded': 'Rezerwacja została dodana',
@@ -1582,6 +1602,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'inspector.trackStats': 'Statystyki trasy',
'budget.exportCsv': 'Eksportuj CSV',
'budget.table.date': 'Data',
'budget.linkedToReservation': 'Powiązane z rezerwacją — edytuj nazwę tam',
'memories.testFirst': 'Najpierw przetestuj połączenie',
'memories.linkAlbum': 'Połącz album',
'memories.selectAlbum': 'Wybierz album Immich',
+18
View File
@@ -8,6 +8,8 @@ const ru: Record<string, string> = {
'common.loading': 'Загрузка...',
'common.import': 'Импорт',
'common.error': 'Ошибка',
'common.unknownError': 'Неизвестная ошибка',
'common.tooManyAttempts': 'Слишком много попыток. Попробуйте позже.',
'common.back': 'Назад',
'common.all': 'Все',
'common.close': 'Закрыть',
@@ -404,6 +406,10 @@ const ru: Record<string, string> = {
'login.mfaHint': 'Откройте Google Authenticator, Authy или другое TOTP-приложение.',
'login.mfaBack': '← Назад к входу',
'login.mfaVerify': 'Подтвердить',
'login.invalidInviteLink': 'Недействительная или истёкшая ссылка-приглашение',
'login.oidcFailed': 'Ошибка входа через OIDC',
'login.usernameRequired': 'Имя пользователя обязательно',
'login.passwordMinLength': 'Пароль должен содержать не менее 8 символов',
'login.oidc.tokenFailed': 'Аутентификация не удалась.',
'login.oidc.invalidState': 'Недействительная сессия. Попробуйте снова.',
'login.demoFailed': 'Ошибка демо-входа',
@@ -1080,9 +1086,13 @@ const ru: Record<string, string> = {
'budget.settlement': 'Взаиморасчёт',
'budget.settlementInfo': 'Нажмите на аватар участника в строке бюджета, чтобы отметить его зелёным — это значит, что он заплатил. Взаиморасчёт покажет, кто кому и сколько должен.',
'budget.netBalances': 'Чистые балансы',
'budget.linkedToReservation': 'Привязано к бронированию — измените название там',
// Files
'files.title': 'Файлы',
'files.pageTitle': 'Файлы и документы',
'files.subtitle': '{count} файлов для {trip}',
'files.downloadPdf': 'Скачать PDF',
'files.count': '{count} файлов',
'files.countSingular': '1 файл',
'files.uploaded': '{count} загружено',
@@ -1329,6 +1339,13 @@ const ru: Record<string, string> = {
'backup.keep.forever': 'Хранить вечно',
// Photos
'photos.title': 'Фотографии',
'photos.subtitle': '{count} фото для {trip}',
'photos.dropHere': 'Перетащите фото сюда...',
'photos.dropHereActive': 'Перетащите фото сюда',
'photos.captionForAll': 'Подпись (для всех)',
'photos.captionPlaceholder': 'Необязательная подпись...',
'photos.addCaption': 'Добавить подпись...',
'photos.allDays': 'Все дни',
'photos.noPhotos': 'Фото пока нет',
'photos.uploadHint': 'Загрузите фото из путешествия',
@@ -1362,6 +1379,7 @@ const ru: Record<string, string> = {
'planner.routeCalculated': 'Маршрут рассчитан',
'planner.routeCalcFailed': 'Не удалось рассчитать маршрут',
'planner.routeError': 'Ошибка расчёта маршрута',
'planner.icsExportFailed': 'Не удалось экспортировать ICS',
'planner.routeOptimized': 'Маршрут оптимизирован',
'planner.reservationUpdated': 'Бронирование обновлено',
'planner.reservationAdded': 'Бронирование добавлено',
+18
View File
@@ -8,6 +8,8 @@ const zh: Record<string, string> = {
'common.loading': '加载中...',
'common.import': '导入',
'common.error': '错误',
'common.unknownError': '未知错误',
'common.tooManyAttempts': '尝试次数过多,请稍后再试。',
'common.back': '返回',
'common.all': '全部',
'common.close': '关闭',
@@ -404,6 +406,10 @@ const zh: Record<string, string> = {
'login.mfaHint': '打开 Google Authenticator、Authy 或其他 TOTP 应用。',
'login.mfaBack': '← 返回登录',
'login.mfaVerify': '验证',
'login.invalidInviteLink': '邀请链接无效或已过期',
'login.oidcFailed': 'OIDC 登录失败',
'login.usernameRequired': '用户名为必填项',
'login.passwordMinLength': '密码至少需要8个字符',
'login.oidc.tokenFailed': '认证失败。',
'login.oidc.invalidState': '会话无效,请重试。',
'login.demoFailed': '演示登录失败',
@@ -1080,9 +1086,13 @@ const zh: Record<string, string> = {
'budget.settlement': '结算',
'budget.settlementInfo': '点击预算项目上的成员头像将其标记为绿色——表示该成员已付款。结算会显示谁欠谁多少。',
'budget.netBalances': '净余额',
'budget.linkedToReservation': '已链接到预订——在那里编辑名称',
// Files
'files.title': '文件',
'files.pageTitle': '文件与文档',
'files.subtitle': '{trip} 的 {count} 个文件',
'files.downloadPdf': '下载 PDF',
'files.count': '{count} 个文件',
'files.countSingular': '1 个文件',
'files.uploaded': '已上传 {count} 个',
@@ -1329,6 +1339,13 @@ const zh: Record<string, string> = {
'backup.keep.forever': '永久保留',
// Photos
'photos.title': '照片',
'photos.subtitle': '{trip} 的 {count} 张照片',
'photos.dropHere': '将照片拖放至此...',
'photos.dropHereActive': '将照片拖放至此',
'photos.captionForAll': '标题(所有)',
'photos.captionPlaceholder': '可选标题...',
'photos.addCaption': '添加标题...',
'photos.allDays': '所有天',
'photos.noPhotos': '暂无照片',
'photos.uploadHint': '上传你的旅行照片',
@@ -1362,6 +1379,7 @@ const zh: Record<string, string> = {
'planner.routeCalculated': '路线已计算',
'planner.routeCalcFailed': '无法计算路线',
'planner.routeError': '路线计算错误',
'planner.icsExportFailed': 'ICS 导出失败',
'planner.routeOptimized': '路线已优化',
'planner.reservationUpdated': '预订已更新',
'planner.reservationAdded': '预订已添加',
+19
View File
@@ -8,6 +8,8 @@ const zhTw: Record<string, string> = {
'common.loading': '載入中...',
'common.import': '匯入',
'common.error': '錯誤',
'common.unknownError': '未知錯誤',
'common.tooManyAttempts': '嘗試次數過多,請稍後再試。',
'common.back': '返回',
'common.all': '全部',
'common.close': '關閉',
@@ -125,6 +127,8 @@ const zhTw: Record<string, string> = {
'dashboard.coverRemoveError': '移除失敗',
'dashboard.titleRequired': '標題為必填項',
'dashboard.endDateError': '結束日期必須晚於開始日期',
'dashboard.dayCount': '天數',
'dashboard.dayCountHint': '未設定旅行日期時的規劃天數。',
// Settings
'settings.title': '設定',
@@ -428,6 +432,10 @@ const zhTw: Record<string, string> = {
'login.mfaHint': '開啟 Google Authenticator、Authy 或其他 TOTP 應用。',
'login.mfaBack': '← 返回登入',
'login.mfaVerify': '驗證',
'login.invalidInviteLink': '邀請連結無效或已過期',
'login.oidcFailed': 'OIDC 登入失敗',
'login.usernameRequired': '使用者名稱為必填',
'login.passwordMinLength': '密碼至少需要8個字元',
'login.oidc.tokenFailed': '認證失敗。',
'login.oidc.invalidState': '會話無效,請重試。',
'login.demoFailed': '演示登入失敗',
@@ -1108,6 +1116,9 @@ const zhTw: Record<string, string> = {
// Files
'files.title': '檔案',
'files.pageTitle': '檔案與文件',
'files.subtitle': '{trip} 的 {count} 個檔案',
'files.downloadPdf': '下載 PDF',
'files.count': '{count} 個檔案',
'files.countSingular': '1 個檔案',
'files.uploaded': '已上傳 {count} 個',
@@ -1354,6 +1365,13 @@ const zhTw: Record<string, string> = {
'backup.keep.forever': '永久保留',
// Photos
'photos.title': '照片',
'photos.subtitle': '{trip} 的 {count} 張照片',
'photos.dropHere': '將照片拖放至此...',
'photos.dropHereActive': '將照片拖放至此',
'photos.captionForAll': '標題(所有)',
'photos.captionPlaceholder': '可選標題...',
'photos.addCaption': '新增標題...',
'photos.allDays': '所有天',
'photos.noPhotos': '暫無照片',
'photos.uploadHint': '上傳你的旅行照片',
@@ -1387,6 +1405,7 @@ const zhTw: Record<string, string> = {
'planner.routeCalculated': '路線已計算',
'planner.routeCalcFailed': '無法計算路線',
'planner.routeError': '路線計算錯誤',
'planner.icsExportFailed': 'ICS 匯出失敗',
'planner.routeOptimized': '路線已最佳化',
'planner.reservationUpdated': '預訂已更新',
'planner.reservationAdded': '預訂已新增',
+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>
+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
@@ -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}
/>