Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
jubnl
2026-04-03 14:45:34 +02:00
20 changed files with 17163 additions and 16726 deletions

View File

@@ -1,5 +1,8 @@
name: Tests
permissions:
contents: read
on:
push:
branches: [main, dev]

View File

@@ -119,7 +119,7 @@ export default function GitHubPanel() {
return (
<div className="space-y-3">
{/* Support cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<a
href="https://ko-fi.com/mauriceboe"
target="_blank"
@@ -156,6 +156,24 @@ export default function GitHubPanel() {
</div>
<ExternalLink size={14} className="ml-auto flex-shrink-0" style={{ color: 'var(--text-faint)' }} />
</a>
<a
href="https://discord.gg/nSdKaXgN"
target="_blank"
rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-all"
style={{ background: 'var(--bg-card)', borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#5865F2'; e.currentTarget.style.boxShadow = '0 0 0 1px #5865F222' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
>
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#5865F215', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="#5865F2"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</div>
<div>
<div className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>Discord</div>
<div className="text-xs" style={{ color: 'var(--text-faint)' }}>Join the community</div>
</div>
<ExternalLink size={14} className="ml-auto flex-shrink-0" style={{ color: 'var(--text-faint)' }} />
</a>
</div>
{/* Loading / Error / Releases */}

View File

@@ -232,9 +232,18 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }:
</button>
{appVersion && (
<div className="px-4 pt-2 pb-2.5 text-center" style={{ marginTop: 4, borderTop: '1px solid var(--border-secondary)' }}>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 5, background: 'var(--bg-tertiary)', borderRadius: 99, padding: '4px 12px' }}>
<img src={dark ? '/text-light.svg' : '/text-dark.svg'} alt="TREK" style={{ height: 10, opacity: 0.5 }} />
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-faint)' }}>v{appVersion}</span>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 5, background: 'var(--bg-tertiary)', borderRadius: 99, padding: '4px 12px' }}>
<img src={dark ? '/text-light.svg' : '/text-dark.svg'} alt="TREK" style={{ height: 10, opacity: 0.5 }} />
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-faint)' }}>v{appVersion}</span>
</div>
<a href="https://discord.gg/nSdKaXgN" target="_blank" rel="noopener noreferrer"
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 24, height: 24, borderRadius: 99, background: 'var(--bg-tertiary)', transition: 'background 0.15s' }}
onMouseEnter={e => e.currentTarget.style.background = '#5865F220'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-tertiary)'}
title="Discord">
<svg width="12" height="12" viewBox="0 0 24 24" fill="var(--text-faint)"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</a>
</div>
</div>
)}

View File

@@ -1494,17 +1494,17 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'perm.actionHint.collab_edit': 'من يمكنه إنشاء ملاحظات واستطلاعات وإرسال رسائل',
'perm.actionHint.share_manage': 'من يمكنه إنشاء أو حذف روابط المشاركة العامة',
// Undo
'undo.button': 'Undo',
'undo.tooltip': 'Undo: {action}',
'undo.assignPlace': 'Place assigned to day',
'undo.removeAssignment': 'Place removed from day',
'undo.reorder': 'Places reordered',
'undo.optimize': 'Route optimized',
'undo.deletePlace': 'Place deleted',
'undo.moveDay': 'Place moved to another day',
'undo.lock': 'Place lock toggled',
'undo.importGpx': 'GPX import',
'undo.importGoogleList': 'Google Maps import',
'undo.button': 'تراجع',
'undo.tooltip': 'تراجع: {action}',
'undo.assignPlace': 'تم تعيين المكان لليوم',
'undo.removeAssignment': 'تم إزالة المكان من اليوم',
'undo.reorder': 'تمت إعادة ترتيب الأماكن',
'undo.optimize': 'تم تحسين المسار',
'undo.deletePlace': 'تم حذف المكان',
'undo.moveDay': 'تم نقل المكان إلى يوم آخر',
'undo.lock': 'تم تبديل قفل المكان',
'undo.importGpx': 'استيراد GPX',
'undo.importGoogleList': 'استيراد خرائط Google',
// Notifications
'notifications.title': 'الإشعارات',
@@ -1519,6 +1519,29 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'notifications.markUnread': 'تحديد كغير مقروء',
'notifications.delete': 'حذف',
'notifications.system': 'النظام',
'memories.error.loadAlbums': 'فشل تحميل الألبومات',
'memories.error.linkAlbum': 'فشل ربط الألبوم',
'memories.error.unlinkAlbum': 'فشل إلغاء ربط الألبوم',
'memories.error.syncAlbum': 'فشل مزامنة الألبوم',
'memories.error.loadPhotos': 'فشل تحميل الصور',
'memories.error.addPhotos': 'فشل إضافة الصور',
'memories.error.removePhoto': 'فشل حذف الصورة',
'memories.error.toggleSharing': 'فشل تحديث إعدادات المشاركة',
'undo.addPlace': 'تمت إضافة المكان',
'undo.done': 'تم التراجع: {action}',
'notifications.test.title': 'إشعار تجريبي من {actor}',
'notifications.test.text': 'هذا إشعار تجريبي بسيط.',
'notifications.test.booleanTitle': 'يطلب منك {actor} الموافقة',
'notifications.test.booleanText': 'إشعار تجريبي يتطلب إجابة.',
'notifications.test.accept': 'موافقة',
'notifications.test.decline': 'رفض',
'notifications.test.navigateTitle': 'تحقق من شيء ما',
'notifications.test.navigateText': 'إشعار تجريبي للتنقل.',
'notifications.test.goThere': 'اذهب إلى هناك',
'notifications.test.adminTitle': 'إذاعة المسؤول',
'notifications.test.adminText': 'أرسل {actor} إشعاراً تجريبياً لجميع المسؤولين.',
'notifications.test.tripTitle': 'نشر {actor} في رحلتك',
'notifications.test.tripText': 'إشعار تجريبي للرحلة "{trip}".',
}
export default ar

View File

@@ -1489,17 +1489,17 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'perm.actionHint.collab_edit': 'Quem pode criar notas, enquetes e enviar mensagens',
'perm.actionHint.share_manage': 'Quem pode criar ou excluir links de compartilhamento públicos',
// Undo
'undo.button': 'Undo',
'undo.tooltip': 'Undo: {action}',
'undo.assignPlace': 'Place assigned to day',
'undo.removeAssignment': 'Place removed from day',
'undo.reorder': 'Places reordered',
'undo.optimize': 'Route optimized',
'undo.deletePlace': 'Place deleted',
'undo.moveDay': 'Place moved to another day',
'undo.lock': 'Place lock toggled',
'undo.importGpx': 'GPX import',
'undo.importGoogleList': 'Google Maps import',
'undo.button': 'Desfazer',
'undo.tooltip': 'Desfazer: {action}',
'undo.assignPlace': 'Local atribuído ao dia',
'undo.removeAssignment': 'Local removido do dia',
'undo.reorder': 'Locais reordenados',
'undo.optimize': 'Rota otimizada',
'undo.deletePlace': 'Local excluído',
'undo.moveDay': 'Local movido para outro dia',
'undo.lock': 'Bloqueio do local alternado',
'undo.importGpx': 'Importação de GPX',
'undo.importGoogleList': 'Importação do Google Maps',
// Notifications
'notifications.title': 'Notificações',
@@ -1514,6 +1514,29 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'notifications.markUnread': 'Marcar como não lido',
'notifications.delete': 'Excluir',
'notifications.system': 'Sistema',
'memories.error.loadAlbums': 'Falha ao carregar álbuns',
'memories.error.linkAlbum': 'Falha ao vincular álbum',
'memories.error.unlinkAlbum': 'Falha ao desvincular álbum',
'memories.error.syncAlbum': 'Falha ao sincronizar álbum',
'memories.error.loadPhotos': 'Falha ao carregar fotos',
'memories.error.addPhotos': 'Falha ao adicionar fotos',
'memories.error.removePhoto': 'Falha ao remover foto',
'memories.error.toggleSharing': 'Falha ao atualizar compartilhamento',
'undo.addPlace': 'Local adicionado',
'undo.done': 'Desfeito: {action}',
'notifications.test.title': 'Notificação de teste de {actor}',
'notifications.test.text': 'Esta é uma notificação de teste simples.',
'notifications.test.booleanTitle': '{actor} solicita sua aprovação',
'notifications.test.booleanText': 'Notificação de teste booleana.',
'notifications.test.accept': 'Aprovar',
'notifications.test.decline': 'Recusar',
'notifications.test.navigateTitle': 'Confira algo',
'notifications.test.navigateText': 'Notificação de teste de navegação.',
'notifications.test.goThere': 'Ir lá',
'notifications.test.adminTitle': 'Transmissão do admin',
'notifications.test.adminText': '{actor} enviou uma notificação de teste para todos os admins.',
'notifications.test.tripTitle': '{actor} postou na sua viagem',
'notifications.test.tripText': 'Notificação de teste para a viagem "{trip}".',
}
export default br

View File

@@ -1492,17 +1492,17 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'perm.actionHint.collab_edit': 'Kdo může vytvářet poznámky, hlasování a posílat zprávy',
'perm.actionHint.share_manage': 'Kdo může vytvářet nebo mazat veřejné odkazy ke sdílení',
// Undo
'undo.button': 'Undo',
'undo.tooltip': 'Undo: {action}',
'undo.assignPlace': 'Place assigned to day',
'undo.removeAssignment': 'Place removed from day',
'undo.reorder': 'Places reordered',
'undo.optimize': 'Route optimized',
'undo.deletePlace': 'Place deleted',
'undo.moveDay': 'Place moved to another day',
'undo.lock': 'Place lock toggled',
'undo.importGpx': 'GPX import',
'undo.importGoogleList': 'Google Maps import',
'undo.button': 'Zpět',
'undo.tooltip': 'Zpět: {action}',
'undo.assignPlace': 'Místo přiřazeno ke dni',
'undo.removeAssignment': 'Místo odebráno ze dne',
'undo.reorder': 'Místa přeseřazena',
'undo.optimize': 'Trasa optimalizována',
'undo.deletePlace': 'Místo smazáno',
'undo.moveDay': 'Místo přesunuto na jiný den',
'undo.lock': 'Zámek místa přepnut',
'undo.importGpx': 'Import GPX',
'undo.importGoogleList': 'Import z Google Maps',
// Notifications
'notifications.title': 'Oznámení',
@@ -1517,6 +1517,31 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'notifications.markUnread': 'Označit jako nepřečtené',
'notifications.delete': 'Smazat',
'notifications.system': 'Systém',
'settings.mustChangePassword': 'Před pokračováním musíte změnit heslo.',
'atlas.searchCountry': 'Hledat zemi...',
'memories.error.loadAlbums': 'Načtení alb se nezdařilo',
'memories.error.linkAlbum': 'Propojení alba se nezdařilo',
'memories.error.unlinkAlbum': 'Odpojení alba se nezdařilo',
'memories.error.syncAlbum': 'Synchronizace alba se nezdařila',
'memories.error.loadPhotos': 'Načtení fotek se nezdařilo',
'memories.error.addPhotos': 'Přidání fotek se nezdařilo',
'memories.error.removePhoto': 'Odebrání fotky se nezdařilo',
'memories.error.toggleSharing': 'Aktualizace sdílení se nezdařila',
'undo.addPlace': 'Místo přidáno',
'undo.done': 'Vráceno zpět: {action}',
'notifications.test.title': 'Testovací oznámení od {actor}',
'notifications.test.text': 'Toto je jednoduché testovací oznámení.',
'notifications.test.booleanTitle': '{actor} žádá o vaše schválení',
'notifications.test.booleanText': 'Testovací oznámení s volbou.',
'notifications.test.accept': 'Schválit',
'notifications.test.decline': 'Odmítnout',
'notifications.test.navigateTitle': 'Podívejte se na toto',
'notifications.test.navigateText': 'Testovací navigační oznámení.',
'notifications.test.goThere': 'Přejít tam',
'notifications.test.adminTitle': 'Hromadná zpráva pro správce',
'notifications.test.adminText': '{actor} odeslal testovací oznámení všem správcům.',
'notifications.test.tripTitle': '{actor} přispěl do vašeho výletu',
'notifications.test.tripText': 'Testovací oznámení pro výlet "{trip}".',
}
export default cs

View File

@@ -1491,17 +1491,17 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'perm.actionHint.collab_edit': 'Wer kann Notizen, Umfragen erstellen und Nachrichten senden',
'perm.actionHint.share_manage': 'Wer kann öffentliche Freigabelinks erstellen oder löschen',
// Undo
'undo.button': 'Undo',
'undo.tooltip': 'Undo: {action}',
'undo.assignPlace': 'Place assigned to day',
'undo.removeAssignment': 'Place removed from day',
'undo.reorder': 'Places reordered',
'undo.optimize': 'Route optimized',
'undo.deletePlace': 'Place deleted',
'undo.moveDay': 'Place moved to another day',
'undo.lock': 'Place lock toggled',
'undo.importGpx': 'GPX import',
'undo.importGoogleList': 'Google Maps import',
'undo.button': 'Rückgängig',
'undo.tooltip': 'Rückgängig: {action}',
'undo.assignPlace': 'Ort einem Tag zugewiesen',
'undo.removeAssignment': 'Ort von Tag entfernt',
'undo.reorder': 'Orte neu sortiert',
'undo.optimize': 'Route optimiert',
'undo.deletePlace': 'Ort gelöscht',
'undo.moveDay': 'Ort zu anderem Tag verschoben',
'undo.lock': 'Ortssperre umgeschaltet',
'undo.importGpx': 'GPX-Import',
'undo.importGoogleList': 'Google Maps-Import',
// Notifications
'notifications.title': 'Benachrichtigungen',
@@ -1516,6 +1516,29 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'notifications.markUnread': 'Als ungelesen markieren',
'notifications.delete': 'Löschen',
'notifications.system': 'System',
'memories.error.loadAlbums': 'Alben konnten nicht geladen werden',
'memories.error.linkAlbum': 'Album konnte nicht verknüpft werden',
'memories.error.unlinkAlbum': 'Album konnte nicht getrennt werden',
'memories.error.syncAlbum': 'Album konnte nicht synchronisiert werden',
'memories.error.loadPhotos': 'Fotos konnten nicht geladen werden',
'memories.error.addPhotos': 'Fotos konnten nicht hinzugefügt werden',
'memories.error.removePhoto': 'Foto konnte nicht entfernt werden',
'memories.error.toggleSharing': 'Freigabe konnte nicht aktualisiert werden',
'undo.addPlace': 'Ort hinzugefügt',
'undo.done': 'Rückgängig gemacht: {action}',
'notifications.test.title': 'Testbenachrichtigung von {actor}',
'notifications.test.text': 'Dies ist eine einfache Testbenachrichtigung.',
'notifications.test.booleanTitle': '{actor} bittet um Ihre Zustimmung',
'notifications.test.booleanText': 'Dies ist eine boolesche Testbenachrichtigung.',
'notifications.test.accept': 'Genehmigen',
'notifications.test.decline': 'Ablehnen',
'notifications.test.navigateTitle': 'Etwas ansehen',
'notifications.test.navigateText': 'Dies ist eine Navigations-Testbenachrichtigung.',
'notifications.test.goThere': 'Dorthin',
'notifications.test.adminTitle': 'Admin-Broadcast',
'notifications.test.adminText': '{actor} hat eine Testbenachrichtigung an alle Admins gesendet.',
'notifications.test.tripTitle': '{actor} hat in Ihrer Reise gepostet',
'notifications.test.tripText': 'Testbenachrichtigung für Reise "{trip}".',
}
export default de

View File

@@ -1496,17 +1496,17 @@ const es: Record<string, string> = {
'perm.actionHint.collab_edit': 'Quién puede crear notas, encuestas y enviar mensajes',
'perm.actionHint.share_manage': 'Quién puede crear o eliminar enlaces compartidos públicos',
// Undo
'undo.button': 'Undo',
'undo.tooltip': 'Undo: {action}',
'undo.assignPlace': 'Place assigned to day',
'undo.removeAssignment': 'Place removed from day',
'undo.reorder': 'Places reordered',
'undo.optimize': 'Route optimized',
'undo.deletePlace': 'Place deleted',
'undo.moveDay': 'Place moved to another day',
'undo.lock': 'Place lock toggled',
'undo.importGpx': 'GPX import',
'undo.importGoogleList': 'Google Maps import',
'undo.button': 'Deshacer',
'undo.tooltip': 'Deshacer: {action}',
'undo.assignPlace': 'Lugar asignado al día',
'undo.removeAssignment': 'Lugar eliminado del día',
'undo.reorder': 'Lugares reordenados',
'undo.optimize': 'Ruta optimizada',
'undo.deletePlace': 'Lugar eliminado',
'undo.moveDay': 'Lugar movido a otro día',
'undo.lock': 'Bloqueo de lugar activado/desactivado',
'undo.importGpx': 'Importación GPX',
'undo.importGoogleList': 'Importación de Google Maps',
// Notifications
'notifications.title': 'Notificaciones',
@@ -1521,6 +1521,29 @@ const es: Record<string, string> = {
'notifications.markUnread': 'Marcar como no leída',
'notifications.delete': 'Eliminar',
'notifications.system': 'Sistema',
'memories.error.loadAlbums': 'Error al cargar los álbumes',
'memories.error.linkAlbum': 'Error al vincular el álbum',
'memories.error.unlinkAlbum': 'Error al desvincular el álbum',
'memories.error.syncAlbum': 'Error al sincronizar el álbum',
'memories.error.loadPhotos': 'Error al cargar las fotos',
'memories.error.addPhotos': 'Error al agregar las fotos',
'memories.error.removePhoto': 'Error al eliminar la foto',
'memories.error.toggleSharing': 'Error al actualizar el uso compartido',
'undo.addPlace': 'Lugar agregado',
'undo.done': 'Deshecho: {action}',
'notifications.test.title': 'Notificación de prueba de {actor}',
'notifications.test.text': 'Esta es una notificación de prueba simple.',
'notifications.test.booleanTitle': '{actor} solicita tu aprobación',
'notifications.test.booleanText': 'Notificación de prueba booleana.',
'notifications.test.accept': 'Aprobar',
'notifications.test.decline': 'Rechazar',
'notifications.test.navigateTitle': 'Mira esto',
'notifications.test.navigateText': 'Notificación de prueba de navegación.',
'notifications.test.goThere': 'Ir allí',
'notifications.test.adminTitle': 'Difusión de administrador',
'notifications.test.adminText': '{actor} envió una notificación de prueba a todos los administradores.',
'notifications.test.tripTitle': '{actor} publicó en tu viaje',
'notifications.test.tripText': 'Notificación de prueba para el viaje "{trip}".',
}
export default es

View File

@@ -1490,17 +1490,17 @@ const fr: Record<string, string> = {
'perm.actionHint.collab_edit': 'Qui peut créer des notes, des sondages et envoyer des messages',
'perm.actionHint.share_manage': 'Qui peut créer ou supprimer des liens de partage publics',
// Undo
'undo.button': 'Undo',
'undo.tooltip': 'Undo: {action}',
'undo.assignPlace': 'Place assigned to day',
'undo.removeAssignment': 'Place removed from day',
'undo.reorder': 'Places reordered',
'undo.optimize': 'Route optimized',
'undo.deletePlace': 'Place deleted',
'undo.moveDay': 'Place moved to another day',
'undo.lock': 'Place lock toggled',
'undo.importGpx': 'GPX import',
'undo.importGoogleList': 'Google Maps import',
'undo.button': 'Annuler',
'undo.tooltip': 'Annuler : {action}',
'undo.assignPlace': 'Lieu ajouté au jour',
'undo.removeAssignment': 'Lieu retiré du jour',
'undo.reorder': 'Lieux réorganisés',
'undo.optimize': 'Itinéraire optimi',
'undo.deletePlace': 'Lieu supprimé',
'undo.moveDay': 'Lieu déplacé vers un autre jour',
'undo.lock': 'Verrouillage du lieu modifié',
'undo.importGpx': 'Import GPX',
'undo.importGoogleList': 'Import Google Maps',
// Notifications
'notifications.title': 'Notifications',
@@ -1515,6 +1515,29 @@ const fr: Record<string, string> = {
'notifications.markUnread': 'Marquer comme non lu',
'notifications.delete': 'Supprimer',
'notifications.system': 'Système',
'memories.error.loadAlbums': 'Impossible de charger les albums',
'memories.error.linkAlbum': 'Impossible de lier l\'album',
'memories.error.unlinkAlbum': 'Impossible de dissocier l\'album',
'memories.error.syncAlbum': 'Impossible de synchroniser l\'album',
'memories.error.loadPhotos': 'Impossible de charger les photos',
'memories.error.addPhotos': 'Impossible d\'ajouter les photos',
'memories.error.removePhoto': 'Impossible de supprimer la photo',
'memories.error.toggleSharing': 'Impossible de mettre à jour le partage',
'undo.addPlace': 'Lieu ajouté',
'undo.done': 'Annulé : {action}',
'notifications.test.title': 'Notification test de {actor}',
'notifications.test.text': 'Ceci est une simple notification de test.',
'notifications.test.booleanTitle': '{actor} demande votre approbation',
'notifications.test.booleanText': 'Notification de test booléenne.',
'notifications.test.accept': 'Approuver',
'notifications.test.decline': 'Refuser',
'notifications.test.navigateTitle': 'Allez voir quelque chose',
'notifications.test.navigateText': 'Notification de test de navigation.',
'notifications.test.goThere': 'Y aller',
'notifications.test.adminTitle': 'Diffusion admin',
'notifications.test.adminText': '{actor} a envoyé une notification de test à tous les admins.',
'notifications.test.tripTitle': '{actor} a publié dans votre voyage',
'notifications.test.tripText': 'Notification de test pour le voyage "{trip}".',
}
export default fr

View File

@@ -1491,17 +1491,17 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'perm.actionHint.collab_edit': 'Ki hozhat létre jegyzeteket, szavazásokat és küldhet üzeneteket',
'perm.actionHint.share_manage': 'Ki hozhat létre vagy törölhet nyilvános megosztási linkeket',
// Undo
'undo.button': 'Undo',
'undo.tooltip': 'Undo: {action}',
'undo.assignPlace': 'Place assigned to day',
'undo.removeAssignment': 'Place removed from day',
'undo.reorder': 'Places reordered',
'undo.optimize': 'Route optimized',
'undo.deletePlace': 'Place deleted',
'undo.moveDay': 'Place moved to another day',
'undo.lock': 'Place lock toggled',
'undo.importGpx': 'GPX import',
'undo.importGoogleList': 'Google Maps import',
'undo.button': 'Visszavonás',
'undo.tooltip': 'Visszavonás: {action}',
'undo.assignPlace': 'Hely naphoz rendelve',
'undo.removeAssignment': 'Hely eltávolítva a napról',
'undo.reorder': 'Helyek átrendezve',
'undo.optimize': 'Útvonal optimalizálva',
'undo.deletePlace': 'Hely törölve',
'undo.moveDay': 'Hely áthelyezve másik napra',
'undo.lock': 'Hely zárolása váltva',
'undo.importGpx': 'GPX importálás',
'undo.importGoogleList': 'Google Maps importálás',
// Notifications
'notifications.title': 'Értesítések',
@@ -1516,6 +1516,29 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'notifications.markUnread': 'Olvasatlannak jelölés',
'notifications.delete': 'Törlés',
'notifications.system': 'Rendszer',
'memories.error.loadAlbums': 'Az albumok betöltése sikertelen',
'memories.error.linkAlbum': 'Az album csatolása sikertelen',
'memories.error.unlinkAlbum': 'Az album leválasztása sikertelen',
'memories.error.syncAlbum': 'Az album szinkronizálása sikertelen',
'memories.error.loadPhotos': 'A fotók betöltése sikertelen',
'memories.error.addPhotos': 'A fotók hozzáadása sikertelen',
'memories.error.removePhoto': 'A fotó eltávolítása sikertelen',
'memories.error.toggleSharing': 'A megosztás frissítése sikertelen',
'undo.addPlace': 'Hely hozzáadva',
'undo.done': 'Visszavonva: {action}',
'notifications.test.title': 'Teszt értesítés {actor} részéről',
'notifications.test.text': 'Ez egy egyszerű teszt értesítés.',
'notifications.test.booleanTitle': '{actor} jóváhagyásodat kéri',
'notifications.test.booleanText': 'Teszt igen/nem értesítés.',
'notifications.test.accept': 'Jóváhagyás',
'notifications.test.decline': 'Elutasítás',
'notifications.test.navigateTitle': 'Nézz meg valamit',
'notifications.test.navigateText': 'Teszt navigációs értesítés.',
'notifications.test.goThere': 'Odamegyek',
'notifications.test.adminTitle': 'Adminisztrátor üzenet',
'notifications.test.adminText': '{actor} teszt értesítést küldött az összes adminisztrátornak.',
'notifications.test.tripTitle': '{actor} üzenetet küldött az utazásodba',
'notifications.test.tripText': 'Teszt értesítés a(z) "{trip}" utazáshoz.',
}
export default hu

View File

@@ -1518,6 +1518,27 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'notifications.markUnread': 'Segna come non letto',
'notifications.delete': 'Elimina',
'notifications.system': 'Sistema',
'memories.error.loadAlbums': 'Caricamento album non riuscito',
'memories.error.linkAlbum': 'Collegamento album non riuscito',
'memories.error.unlinkAlbum': 'Scollegamento album non riuscito',
'memories.error.syncAlbum': 'Sincronizzazione album non riuscita',
'memories.error.loadPhotos': 'Caricamento foto non riuscito',
'memories.error.addPhotos': 'Aggiunta foto non riuscita',
'memories.error.removePhoto': 'Rimozione foto non riuscita',
'memories.error.toggleSharing': 'Aggiornamento condivisione non riuscito',
'notifications.test.title': 'Notifica di test da {actor}',
'notifications.test.text': 'Questa è una semplice notifica di test.',
'notifications.test.booleanTitle': '{actor} richiede la tua approvazione',
'notifications.test.booleanText': 'Notifica di test con risposta.',
'notifications.test.accept': 'Approva',
'notifications.test.decline': 'Rifiuta',
'notifications.test.navigateTitle': 'Dai un\'occhiata',
'notifications.test.navigateText': 'Notifica di test con navigazione.',
'notifications.test.goThere': 'Vai',
'notifications.test.adminTitle': 'Comunicazione admin',
'notifications.test.adminText': '{actor} ha inviato una notifica di test a tutti gli amministratori.',
'notifications.test.tripTitle': '{actor} ha pubblicato nel tuo viaggio',
'notifications.test.tripText': 'Notifica di test per il viaggio "{trip}".',
}
export default it

View File

@@ -1490,17 +1490,17 @@ const nl: Record<string, string> = {
'perm.actionHint.collab_edit': 'Wie kan notities, polls aanmaken en berichten versturen',
'perm.actionHint.share_manage': 'Wie kan openbare deellinks aanmaken of verwijderen',
// Undo
'undo.button': 'Undo',
'undo.tooltip': 'Undo: {action}',
'undo.assignPlace': 'Place assigned to day',
'undo.removeAssignment': 'Place removed from day',
'undo.reorder': 'Places reordered',
'undo.optimize': 'Route optimized',
'undo.deletePlace': 'Place deleted',
'undo.moveDay': 'Place moved to another day',
'undo.lock': 'Place lock toggled',
'undo.importGpx': 'GPX import',
'undo.importGoogleList': 'Google Maps import',
'undo.button': 'Ongedaan maken',
'undo.tooltip': 'Ongedaan maken: {action}',
'undo.assignPlace': 'Locatie aan dag toegewezen',
'undo.removeAssignment': 'Locatie uit dag verwijderd',
'undo.reorder': 'Locaties hergeordend',
'undo.optimize': 'Route geoptimaliseerd',
'undo.deletePlace': 'Locatie verwijderd',
'undo.moveDay': 'Locatie naar andere dag verplaatst',
'undo.lock': 'Vergrendeling locatie gewijzigd',
'undo.importGpx': 'GPX-import',
'undo.importGoogleList': 'Google Maps-import',
// Notifications
'notifications.title': 'Meldingen',
@@ -1515,6 +1515,29 @@ const nl: Record<string, string> = {
'notifications.markUnread': 'Markeren als ongelezen',
'notifications.delete': 'Verwijderen',
'notifications.system': 'Systeem',
'memories.error.loadAlbums': 'Albums laden mislukt',
'memories.error.linkAlbum': 'Album koppelen mislukt',
'memories.error.unlinkAlbum': 'Album ontkoppelen mislukt',
'memories.error.syncAlbum': 'Album synchroniseren mislukt',
'memories.error.loadPhotos': 'Foto\'s laden mislukt',
'memories.error.addPhotos': 'Foto\'s toevoegen mislukt',
'memories.error.removePhoto': 'Foto verwijderen mislukt',
'memories.error.toggleSharing': 'Delen bijwerken mislukt',
'undo.addPlace': 'Locatie toegevoegd',
'undo.done': 'Ongedaan gemaakt: {action}',
'notifications.test.title': 'Testmelding van {actor}',
'notifications.test.text': 'Dit is een eenvoudige testmelding.',
'notifications.test.booleanTitle': '{actor} vraagt om uw goedkeuring',
'notifications.test.booleanText': 'Booleaanse testmelding.',
'notifications.test.accept': 'Goedkeuren',
'notifications.test.decline': 'Afwijzen',
'notifications.test.navigateTitle': 'Bekijk iets',
'notifications.test.navigateText': 'Navigatie-testmelding.',
'notifications.test.goThere': 'Ga erheen',
'notifications.test.adminTitle': 'Admin-broadcast',
'notifications.test.adminText': '{actor} heeft een testmelding naar alle admins gestuurd.',
'notifications.test.tripTitle': '{actor} heeft gepost in uw reis',
'notifications.test.tripText': 'Testmelding voor reis "{trip}".',
}
export default nl

View File

@@ -661,7 +661,6 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'atlas.statsTab': 'Statystyki',
'atlas.bucketTab': 'Lista marzeń',
'atlas.addBucket': 'Dodaj do listy marzeń',
'atlas.bucketNamePlaceholder': 'Miejsce lub cel podróży...',
'atlas.bucketNotesPlaceholder': 'Notatki (opcjonalnie)',
'atlas.bucketEmpty': 'Twoja lista marzeń jest pusta',
'atlas.bucketEmptyHint': 'Dodaj miejsca, które chcesz odwiedzić',
@@ -674,7 +673,6 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'atlas.nextTrip': 'Następna podróż',
'atlas.daysLeft': 'dni do wyjazdu',
'atlas.streak': 'Streak',
'atlas.year': 'rok',
'atlas.years': 'lata',
'atlas.yearInRow': 'rok z rzędu',
'atlas.yearsInRow': 'lat z rzędu',
@@ -1388,6 +1386,151 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'collab.polls.options': 'Opcje',
'collab.polls.delete': 'Usuń',
'collab.polls.closedSection': 'Zamknięte',
'common.import': 'Importuj',
'common.saved': 'Zapisano',
'trips.reminder': 'Przypomnienie',
'trips.reminderNone': 'Brak',
'trips.reminderDay': 'dzień',
'trips.reminderDays': 'dni',
'trips.reminderCustom': 'Niestandardowe',
'trips.reminderDaysBefore': 'dni przed wyjazdem',
'trips.reminderDisabledHint': 'Przypomnienia o podróżach są wyłączone.',
'dashboard.members': 'Towarzysze',
'dashboard.copyTrip': 'Kopiuj',
'dashboard.copySuffix': 'kopia',
'dashboard.toast.copied': 'Podróż skopiowana!',
'dashboard.toast.copyError': 'Nie udało się skopiować podróży',
'admin.notifications.title': 'Powiadomienia',
'admin.notifications.hint': 'Wybierz jeden kanał powiadomień.',
'admin.notifications.none': 'Wyłączone',
'admin.notifications.email': 'Email (SMTP)',
'admin.notifications.webhook': 'Webhook',
'admin.notifications.events': 'Zdarzenia powiadomień',
'admin.notifications.eventsHint': 'Wybierz zdarzenia wyzwalające powiadomienia.',
'admin.notifications.configureFirst': 'Najpierw skonfiguruj ustawienia SMTP lub webhook.',
'admin.notifications.save': 'Zapisz ustawienia powiadomień',
'admin.notifications.saved': 'Ustawienia powiadomień zapisane',
'admin.notifications.testWebhook': 'Wyślij testowy webhook',
'admin.notifications.testWebhookSuccess': 'Testowy webhook wysłany pomyślnie',
'admin.notifications.testWebhookFailed': 'Testowy webhook nie powiódł się',
'admin.webhook.hint': 'Wysyłaj powiadomienia do zewnętrznego webhooka.',
'settings.notificationsDisabled': 'Powiadomienia nie są skonfigurowane.',
'settings.notificationsActive': 'Aktywny kanał',
'settings.notificationsManagedByAdmin': 'Zdarzenia konfigurowane przez administratora.',
'settings.mustChangePassword': 'Musisz zmienić hasło przed kontynuowaniem.',
'login.setNewPassword': 'Ustaw nowe hasło',
'login.setNewPasswordHint': 'Musisz zmienić hasło.',
'atlas.searchCountry': 'Szukaj kraju...',
'trip.loadingPhotos': 'Ładowanie zdjęć...',
'places.importGoogleList': 'Lista Google',
'places.googleListHint': 'Wklej link do listy Google Maps.',
'places.googleListImported': 'Zaimportowano {count} miejsc',
'places.googleListError': 'Nie udało się zaimportować listy',
'places.viewDetails': 'Zobacz szczegóły',
'inspector.trackStats': 'Statystyki trasy',
'budget.exportCsv': 'Eksportuj CSV',
'budget.table.date': 'Data',
'memories.testFirst': 'Najpierw przetestuj połączenie',
'memories.linkAlbum': 'Połącz album',
'memories.selectAlbum': 'Wybierz album Immich',
'memories.noAlbums': 'Nie znaleziono albumów',
'memories.syncAlbum': 'Synchronizuj album',
'memories.unlinkAlbum': 'Odłącz album',
'memories.photos': 'zdjęcia',
'memories.error.loadAlbums': 'Nie udało się załadować albumów',
'memories.error.linkAlbum': 'Nie udało się połączyć albumu',
'memories.error.unlinkAlbum': 'Nie udało się odłączyć albumu',
'memories.error.syncAlbum': 'Nie udało się zsynchronizować albumu',
'memories.error.loadPhotos': 'Nie udało się załadować zdjęć',
'memories.error.addPhotos': 'Nie udało się dodać zdjęć',
'memories.error.removePhoto': 'Nie udało się usunąć zdjęcia',
'memories.error.toggleSharing': 'Nie udało się zaktualizować udostępniania',
'collab.chat.reply': 'Odpowiedz',
'admin.tabs.permissions': 'Uprawnienia',
'perm.title': 'Ustawienia uprawnień',
'perm.subtitle': 'Kontroluj uprawnienia w aplikacji',
'perm.saved': 'Ustawienia uprawnień zapisane',
'perm.resetDefaults': 'Przywróć domyślne',
'perm.customized': 'dostosowane',
'perm.level.admin': 'Tylko admin',
'perm.level.tripOwner': 'Właściciel podróży',
'perm.level.tripMember': 'Członkowie podróży',
'perm.level.everybody': 'Wszyscy',
'perm.cat.trip': 'Zarządzanie podróżami',
'perm.cat.members': 'Zarządzanie członkami',
'perm.cat.files': 'Pliki',
'perm.cat.content': 'Treść i harmonogram',
'perm.cat.extras': 'Budżet, pakowanie i współpraca',
'perm.action.trip_create': 'Tworzenie podróży',
'perm.action.trip_edit': 'Edytowanie podróży',
'perm.action.trip_delete': 'Usuwanie podróży',
'perm.action.trip_archive': 'Archiwizacja podróży',
'perm.action.trip_cover_upload': 'Przesyłanie okładki',
'perm.action.member_manage': 'Zarządzanie członkami',
'perm.action.file_upload': 'Przesyłanie plików',
'perm.action.file_edit': 'Edytowanie plików',
'perm.action.file_delete': 'Usuwanie plików',
'perm.action.place_edit': 'Zarządzanie miejscami',
'perm.action.day_edit': 'Edytowanie dni',
'perm.action.reservation_edit': 'Zarządzanie rezerwacjami',
'perm.action.budget_edit': 'Zarządzanie budżetem',
'perm.action.packing_edit': 'Zarządzanie pakowaniem',
'perm.action.collab_edit': 'Współpraca',
'perm.action.share_manage': 'Zarządzanie udostępnianiem',
'perm.actionHint.trip_create': 'Kto może tworzyć nowe podróże',
'perm.actionHint.trip_edit': 'Kto może edytować szczegóły podróży',
'perm.actionHint.trip_delete': 'Kto może usunąć podróż',
'perm.actionHint.trip_archive': 'Kto może archiwizować podróż',
'perm.actionHint.trip_cover_upload': 'Kto może zmieniać okładkę',
'perm.actionHint.member_manage': 'Kto może zapraszać lub usuwać członków',
'perm.actionHint.file_upload': 'Kto może przesyłać pliki',
'perm.actionHint.file_edit': 'Kto może edytować pliki',
'perm.actionHint.file_delete': 'Kto może usuwać pliki',
'perm.actionHint.place_edit': 'Kto może zarządzać miejscami',
'perm.actionHint.day_edit': 'Kto może edytować dni i przypisania',
'perm.actionHint.reservation_edit': 'Kto może zarządzać rezerwacjami',
'perm.actionHint.budget_edit': 'Kto może zarządzać budżetem',
'perm.actionHint.packing_edit': 'Kto może zarządzać pakowaniem',
'perm.actionHint.collab_edit': 'Kto może korzystać ze współpracy',
'perm.actionHint.share_manage': 'Kto może zarządzać linkami',
'undo.button': 'Cofnij',
'undo.tooltip': 'Cofnij: {action}',
'undo.assignPlace': 'Miejsce przypisane do dnia',
'undo.removeAssignment': 'Miejsce usunięte z dnia',
'undo.reorder': 'Kolejność zmieniona',
'undo.optimize': 'Trasa zoptymalizowana',
'undo.deletePlace': 'Miejsce usunięte',
'undo.moveDay': 'Miejsce przeniesione',
'undo.lock': 'Blokada przełączona',
'undo.importGpx': 'Import GPX',
'undo.importGoogleList': 'Import Google Maps',
'undo.addPlace': 'Miejsce dodane',
'undo.done': 'Cofnięto: {action}',
'notifications.title': 'Powiadomienia',
'notifications.markAllRead': 'Oznacz wszystkie jako przeczytane',
'notifications.deleteAll': 'Usuń wszystkie',
'notifications.showAll': 'Pokaż wszystkie',
'notifications.empty': 'Brak powiadomień',
'notifications.emptyDescription': "You're all caught up!",
'notifications.all': 'Wszystkie',
'notifications.unreadOnly': 'Nieprzeczytane',
'notifications.markRead': 'Oznacz jako przeczytane',
'notifications.markUnread': 'Oznacz jako nieprzeczytane',
'notifications.delete': 'Usuń',
'notifications.system': 'System',
'notifications.test.title': 'Testowe powiadomienie od {actor}',
'notifications.test.text': 'To jest powiadomienie testowe.',
'notifications.test.booleanTitle': '{actor} prosi o akceptację',
'notifications.test.booleanText': 'Testowe powiadomienie z wyborem.',
'notifications.test.accept': 'Zatwierdź',
'notifications.test.decline': 'Odrzuć',
'notifications.test.navigateTitle': 'Sprawdź coś',
'notifications.test.navigateText': 'Testowe powiadomienie nawigacyjne.',
'notifications.test.goThere': 'Przejdź tam',
'notifications.test.adminTitle': 'Komunikat administracyjny',
'notifications.test.adminText': '{actor} wysłał testowe powiadomienie.',
'notifications.test.tripTitle': '{actor} opublikował w Twojej podróży',
'notifications.test.tripText': 'Testowe powiadomienie dla podróży "{trip}".',
}
export default pl

View File

@@ -1490,17 +1490,17 @@ const ru: Record<string, string> = {
'perm.actionHint.collab_edit': 'Кто может создавать заметки, опросы и отправлять сообщения',
'perm.actionHint.share_manage': 'Кто может создавать или удалять публичные ссылки для обмена',
// Undo
'undo.button': 'Undo',
'undo.tooltip': 'Undo: {action}',
'undo.assignPlace': 'Place assigned to day',
'undo.removeAssignment': 'Place removed from day',
'undo.reorder': 'Places reordered',
'undo.optimize': 'Route optimized',
'undo.deletePlace': 'Place deleted',
'undo.moveDay': 'Place moved to another day',
'undo.lock': 'Place lock toggled',
'undo.importGpx': 'GPX import',
'undo.importGoogleList': 'Google Maps import',
'undo.button': 'Отменить',
'undo.tooltip': 'Отменить: {action}',
'undo.assignPlace': 'Место добавлено в день',
'undo.removeAssignment': 'Место удалено из дня',
'undo.reorder': 'Места переупорядочены',
'undo.optimize': 'Маршрут оптимизирован',
'undo.deletePlace': 'Место удалено',
'undo.moveDay': 'Место перемещено в другой день',
'undo.lock': 'Блокировка места изменена',
'undo.importGpx': 'Импорт GPX',
'undo.importGoogleList': 'Импорт из Google Maps',
// Notifications
'notifications.title': 'Уведомления',
@@ -1515,6 +1515,29 @@ const ru: Record<string, string> = {
'notifications.markUnread': 'Отметить как непрочитанное',
'notifications.delete': 'Удалить',
'notifications.system': 'Система',
'memories.error.loadAlbums': 'Не удалось загрузить альбомы',
'memories.error.linkAlbum': 'Не удалось привязать альбом',
'memories.error.unlinkAlbum': 'Не удалось отвязать альбом',
'memories.error.syncAlbum': 'Не удалось синхронизировать альбом',
'memories.error.loadPhotos': 'Не удалось загрузить фотографии',
'memories.error.addPhotos': 'Не удалось добавить фотографии',
'memories.error.removePhoto': 'Не удалось удалить фотографию',
'memories.error.toggleSharing': 'Не удалось обновить настройки доступа',
'undo.addPlace': 'Место добавлено',
'undo.done': 'Отменено: {action}',
'notifications.test.title': 'Тестовое уведомление от {actor}',
'notifications.test.text': 'Это простое тестовое уведомление.',
'notifications.test.booleanTitle': '{actor} запрашивает подтверждение',
'notifications.test.booleanText': 'Тестовое уведомление с выбором.',
'notifications.test.accept': 'Подтвердить',
'notifications.test.decline': 'Отклонить',
'notifications.test.navigateTitle': 'Посмотрите на это',
'notifications.test.navigateText': 'Тестовое уведомление с переходом.',
'notifications.test.goThere': 'Перейти',
'notifications.test.adminTitle': 'Рассылка администратора',
'notifications.test.adminText': '{actor} отправил тестовое уведомление всем администраторам.',
'notifications.test.tripTitle': '{actor} написал в вашей поездке',
'notifications.test.tripText': 'Тестовое уведомление для поездки "{trip}".',
}
export default ru

View File

@@ -1490,17 +1490,17 @@ const zh: Record<string, string> = {
'perm.actionHint.collab_edit': '谁可以创建笔记、投票和发送消息',
'perm.actionHint.share_manage': '谁可以创建或删除公开分享链接',
// Undo
'undo.button': 'Undo',
'undo.tooltip': 'Undo: {action}',
'undo.assignPlace': 'Place assigned to day',
'undo.removeAssignment': 'Place removed from day',
'undo.reorder': 'Places reordered',
'undo.optimize': 'Route optimized',
'undo.deletePlace': 'Place deleted',
'undo.moveDay': 'Place moved to another day',
'undo.lock': 'Place lock toggled',
'undo.importGpx': 'GPX import',
'undo.importGoogleList': 'Google Maps import',
'undo.button': '撤销',
'undo.tooltip': '撤销:{action}',
'undo.assignPlace': '地点已分配至某天',
'undo.removeAssignment': '地点已从某天移除',
'undo.reorder': '地点已重新排序',
'undo.optimize': '路线已优化',
'undo.deletePlace': '地点已删除',
'undo.moveDay': '地点已移至另一天',
'undo.lock': '地点锁定已切换',
'undo.importGpx': 'GPX 导入',
'undo.importGoogleList': 'Google 地图导入',
// Notifications
'notifications.title': '通知',
@@ -1515,6 +1515,29 @@ const zh: Record<string, string> = {
'notifications.markUnread': '标为未读',
'notifications.delete': '删除',
'notifications.system': '系统',
'memories.error.loadAlbums': '加载相册失败',
'memories.error.linkAlbum': '关联相册失败',
'memories.error.unlinkAlbum': '取消关联相册失败',
'memories.error.syncAlbum': '同步相册失败',
'memories.error.loadPhotos': '加载照片失败',
'memories.error.addPhotos': '添加照片失败',
'memories.error.removePhoto': '删除照片失败',
'memories.error.toggleSharing': '更新共享设置失败',
'undo.addPlace': '地点已添加',
'undo.done': '已撤销:{action}',
'notifications.test.title': '来自 {actor} 的测试通知',
'notifications.test.text': '这是一条简单的测试通知。',
'notifications.test.booleanTitle': '{actor} 请求您的审批',
'notifications.test.booleanText': '测试布尔通知。',
'notifications.test.accept': '批准',
'notifications.test.decline': '拒绝',
'notifications.test.navigateTitle': '查看详情',
'notifications.test.navigateText': '测试跳转通知。',
'notifications.test.goThere': '前往',
'notifications.test.adminTitle': '管理员广播',
'notifications.test.adminText': '{actor} 向所有管理员发送了测试通知。',
'notifications.test.tripTitle': '{actor} 在您的行程中发帖',
'notifications.test.tripText': '行程"{trip}"的测试通知。',
}
export default zh

View File

@@ -48,6 +48,9 @@ interface AuthState {
demoLogin: () => Promise<AuthResponse>
}
// Sequence counter to prevent stale loadUser responses from overwriting fresh auth state
let authSequence = 0
export const useAuthStore = create<AuthState>((set, get) => ({
user: null,
isAuthenticated: false,
@@ -61,6 +64,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
tripRemindersEnabled: false,
login: async (email: string, password: string) => {
authSequence++
set({ isLoading: true, error: null })
try {
const data = await authApi.login({ email, password }) as AuthResponse & { mfa_required?: boolean; mfa_token?: string }
@@ -84,6 +88,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
},
completeMfaLogin: async (mfaToken: string, code: string) => {
authSequence++
set({ isLoading: true, error: null })
try {
const data = await authApi.verifyMfaLogin({ mfa_token: mfaToken, code: code.replace(/\s/g, '') })
@@ -103,6 +108,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
},
register: async (username: string, email: string, password: string, invite_token?: string) => {
authSequence++
set({ isLoading: true, error: null })
try {
const data = await authApi.register({ username, email, password, invite_token })
@@ -138,10 +144,12 @@ export const useAuthStore = create<AuthState>((set, get) => ({
},
loadUser: async (opts?: { silent?: boolean }) => {
const seq = authSequence
const silent = !!opts?.silent
if (!silent) set({ isLoading: true })
try {
const data = await authApi.me()
if (seq !== authSequence) return // stale response — a login/register happened meanwhile
set({
user: data.user,
isAuthenticated: true,
@@ -149,6 +157,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
})
connect()
} catch (err: unknown) {
if (seq !== authSequence) return // stale response — ignore
// Only clear auth state on 401 (invalid/expired token), not on network errors
const isAuthError = err && typeof err === 'object' && 'response' in err &&
(err as { response?: { status?: number } }).response?.status === 401
@@ -219,6 +228,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
setTripRemindersEnabled: (val: boolean) => set({ tripRemindersEnabled: val }),
demoLogin: async () => {
authSequence++
set({ isLoading: true, error: null })
try {
const data = await authApi.demoLogin()

View File

@@ -3,10 +3,10 @@ import multer from 'multer';
import path from 'path';
import fs from 'fs';
import { v4 as uuidv4 } from 'uuid';
import { canAccessTrip } from '../db/database';
import { db, canAccessTrip } from '../db/database';
import { authenticate, demoUploadBlock } from '../middleware/auth';
import { broadcast } from '../websocket';
import { AuthRequest } from '../types';
import { AuthRequest, Trip } from '../types';
import { writeAudit, getClientIp, logInfo } from '../services/auditLog';
import { checkPermission } from '../services/permissions';
import {
@@ -26,6 +26,7 @@ import {
verifyTripAccess,
NotFoundError,
ValidationError,
TRIP_SELECT,
} from '../services/tripService';
const router = express.Router();

View File

@@ -7,7 +7,7 @@ export function cookieOptions(clear = false) {
return {
httpOnly: true,
secure,
sameSite: 'strict' as const,
sameSite: 'lax' as const,
path: '/',
...(clear ? {} : { maxAge: 24 * 60 * 60 * 1000 }), // 24h — matches JWT expiry
};

View File

@@ -6,7 +6,7 @@ import { Trip, User } from '../types';
export const MS_PER_DAY = 86400000;
export const MAX_TRIP_DAYS = 365;
const TRIP_SELECT = `
export const TRIP_SELECT = `
SELECT t.*,
(SELECT COUNT(*) FROM days d WHERE d.trip_id = t.id) as day_count,
(SELECT COUNT(*) FROM places p WHERE p.trip_id = t.id) as place_count,

View File

@@ -11,8 +11,8 @@ describe('cookieOptions', () => {
expect(cookieOptions()).toHaveProperty('httpOnly', true);
});
it('always sets sameSite: strict', () => {
expect(cookieOptions()).toHaveProperty('sameSite', 'strict');
it('always sets sameSite: lax', () => {
expect(cookieOptions()).toHaveProperty('sameSite', 'lax');
});
it('always sets path: /', () => {