mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
Merge pull request #593 from isaiastavares/fix/i18n-translations
fix(i18n): comprehensive translation audit and fixes across all 14 languages
This commit is contained in:
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -43,15 +43,15 @@ describe('PhotoUpload', () => {
|
||||
|
||||
it('FE-COMP-PHOTOUPLOAD-001: renders dropzone with upload instructions', () => {
|
||||
render(<PhotoUpload {...defaultProps} />)
|
||||
expect(screen.getByText('Fotos hier ablegen')).toBeInTheDocument()
|
||||
expect(screen.getByText('Drop photos here')).toBeInTheDocument()
|
||||
// Upload icon rendered via lucide-react as SVG
|
||||
expect(document.querySelector('svg')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOUPLOAD-002: options section hidden before files are selected', () => {
|
||||
render(<PhotoUpload {...defaultProps} />)
|
||||
expect(screen.queryByText('Tag verknüpfen')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('Optionale Beschriftung...')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Link Day')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('Optional caption...')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOUPLOAD-003: upload button is disabled when no files selected', () => {
|
||||
@@ -65,27 +65,27 @@ describe('PhotoUpload', () => {
|
||||
render(<PhotoUpload {...defaultProps} />)
|
||||
await uploadFiles([makeFile()])
|
||||
expect(screen.getByAltText('photo.jpg')).toBeInTheDocument()
|
||||
expect(screen.getByText('Tag verknüpfen')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('Optionale Beschriftung...')).toBeInTheDocument()
|
||||
expect(screen.getByText('Link Day')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('Optional caption...')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOUPLOAD-005: file count label updates correctly', async () => {
|
||||
render(<PhotoUpload {...defaultProps} />)
|
||||
await uploadFiles([makeFile('photo1.jpg'), makeFile('photo2.jpg')])
|
||||
expect(screen.getByText('2 Fotos ausgewählt')).toBeInTheDocument()
|
||||
expect(screen.getByText('2 Photos selected')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOUPLOAD-006: remove button removes a file from preview', async () => {
|
||||
render(<PhotoUpload {...defaultProps} />)
|
||||
await uploadFiles([makeFile('photo1.jpg'), makeFile('photo2.jpg')])
|
||||
expect(screen.getByText('2 Fotos ausgewählt')).toBeInTheDocument()
|
||||
expect(screen.getByText('2 Photos selected')).toBeInTheDocument()
|
||||
|
||||
// Remove buttons are inside `.relative.aspect-square` wrappers in the preview grid
|
||||
const removeButtons = document.querySelectorAll('.relative.aspect-square button')
|
||||
expect(removeButtons.length).toBe(2)
|
||||
await userEvent.click(removeButtons[0])
|
||||
|
||||
expect(screen.getByText('1 Foto ausgewählt')).toBeInTheDocument()
|
||||
expect(screen.getByText('1 Photo selected')).toBeInTheDocument()
|
||||
expect(screen.getAllByRole('img').length).toBe(1)
|
||||
})
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('PhotoUpload', () => {
|
||||
render(<PhotoUpload {...defaultProps} />)
|
||||
await uploadFiles([makeFile()])
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('Optionale Beschriftung...'), 'Vacation')
|
||||
await userEvent.type(screen.getByPlaceholderText('Optional caption...'), 'Vacation')
|
||||
|
||||
await userEvent.click(getSubmitButton())
|
||||
|
||||
@@ -146,7 +146,7 @@ describe('PhotoUpload', () => {
|
||||
await userEvent.click(getSubmitButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/wird hochgeladen/i)).toBeInTheDocument()
|
||||
expect(screen.getAllByText(/uploading/i).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
expect(getSubmitButton()).toBeDisabled()
|
||||
|
||||
@@ -85,12 +85,12 @@ 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>
|
||||
<p className="text-gray-400 text-xs mt-2">{t('photos.fileTypeHint')}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@ export function PhotoUpload({ tripId, days, places, onUpload, onClose }: PhotoUp
|
||||
{/* Preview grid */}
|
||||
{files.length > 0 && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">{files.length} Foto{files.length !== 1 ? 's' : ''} ausgewählt</p>
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">{files.length} {t(files.length !== 1 ? 'photos.photosSelected' : 'photos.photoSelected')}</p>
|
||||
<div className="grid grid-cols-4 sm:grid-cols-6 gap-2 max-h-48 overflow-y-auto">
|
||||
{files.map((file, idx) => (
|
||||
<div key={idx} className="relative aspect-square group">
|
||||
@@ -126,15 +126,15 @@ export function PhotoUpload({ tripId, days, places, onUpload, onClose }: PhotoUp
|
||||
{files.length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Tag verknüpfen</label>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">{t('photos.linkDay')}</label>
|
||||
<select
|
||||
value={dayId}
|
||||
onChange={e => setDayId(e.target.value)}
|
||||
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"
|
||||
>
|
||||
<option value="">Kein Tag</option>
|
||||
<option value="">{t('photos.noDay')}</option>
|
||||
{(days || []).map(day => (
|
||||
<option key={day.id} value={day.id}>Tag {day.day_number}</option>
|
||||
<option key={day.id} value={day.id}>{t('photos.dayLabel', { number: day.day_number })}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -169,7 +169,7 @@ export function PhotoUpload({ tripId, days, places, onUpload, onClose }: PhotoUp
|
||||
<div className="bg-slate-50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-4 h-4 border-2 border-slate-900 border-t-transparent rounded-full animate-spin" />
|
||||
<span className="text-sm text-slate-900">Wird hochgeladen...</span>
|
||||
<span className="text-sm text-slate-900">{t('common.uploading')}</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-200 rounded-full h-1.5">
|
||||
<div
|
||||
|
||||
@@ -189,7 +189,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
</div>
|
||||
{!collapsed && formattedDate && <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 1 }}>{formattedDate}</div>}
|
||||
</div>
|
||||
<button onClick={(e) => { e.stopPropagation(); toggleCollapse() }} title={collapsed ? 'Expand' : 'Collapse'}
|
||||
<button onClick={(e) => { e.stopPropagation(); toggleCollapse() }} title={collapsed ? t('common.expand') : t('common.collapse')}
|
||||
style={{ background: 'var(--bg-secondary)', border: 'none', borderRadius: 10, width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0, transition: 'all 0.15s ease' }}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-secondary)'}>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -601,7 +601,7 @@ export default function PlaceInspector({
|
||||
{selectedDayId && (
|
||||
assignmentInDay ? (
|
||||
<ActionButton onClick={() => onRemoveAssignment(selectedDayId, assignmentInDay.id)} variant="ghost" icon={<Minus size={13} />}
|
||||
label={<><span className="hidden sm:inline">{t('inspector.removeFromDay')}</span><span className="sm:hidden">Remove</span></>} />
|
||||
label={<><span className="hidden sm:inline">{t('inspector.removeFromDay')}</span><span className="sm:hidden">{t('inspector.remove')}</span></>} />
|
||||
) : (
|
||||
<ActionButton onClick={() => onAssignToDay(place.id)} variant="primary" icon={<Plus size={13} />} label={t('inspector.addToDay')} />
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -35,14 +35,14 @@ describe('NotificationsTab', () => {
|
||||
http.get('/api/notifications/preferences', () => new Promise(() => {})),
|
||||
);
|
||||
render(<NotificationsTab />);
|
||||
expect(screen.getByText('Loading…')).toBeInTheDocument();
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTIFICATIONS-002: renders the matrix after preferences load', async () => {
|
||||
render(<NotificationsTab />);
|
||||
// The event label is translated; fallback is the key itself
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
// Should render a toggle (ToggleSwitch renders a button)
|
||||
const toggles = await screen.findAllByRole('button');
|
||||
@@ -52,7 +52,7 @@ describe('NotificationsTab', () => {
|
||||
it('FE-COMP-NOTIFICATIONS-003: renders channel header labels', async () => {
|
||||
render(<NotificationsTab />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
// inapp channel header should appear (either translated or raw key)
|
||||
const headers = screen.getAllByText(/inapp|in.?app/i);
|
||||
@@ -72,7 +72,7 @@ describe('NotificationsTab', () => {
|
||||
);
|
||||
render(<NotificationsTab />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
// Should show noChannels message (translated or key)
|
||||
const noChannelEl = await screen.findByText(/no.*channel|noChannels/i);
|
||||
@@ -97,7 +97,7 @@ describe('NotificationsTab', () => {
|
||||
);
|
||||
render(<NotificationsTab />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
// A dash should appear for non-implemented combos
|
||||
const dashes = await screen.findAllByText('—');
|
||||
@@ -116,7 +116,7 @@ describe('NotificationsTab', () => {
|
||||
|
||||
render(<NotificationsTab />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// minimalMatrix has inapp:true and email:false for trip_invite
|
||||
@@ -144,7 +144,7 @@ describe('NotificationsTab', () => {
|
||||
|
||||
render(<NotificationsTab />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Find the inapp toggle for trip_invite — it starts as "on"
|
||||
@@ -156,8 +156,8 @@ describe('NotificationsTab', () => {
|
||||
|
||||
// After the error, the toggle should revert back (still rendered in the DOM)
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Saving…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Saving...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The toggle should still be present (not removed on error)
|
||||
@@ -178,20 +178,20 @@ describe('NotificationsTab', () => {
|
||||
|
||||
render(<NotificationsTab />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const toggleButtons = await screen.findAllByRole('button');
|
||||
await user.click(toggleButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Saving…')).toBeInTheDocument();
|
||||
expect(screen.getByText('Saving...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
resolveRequest();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Saving…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Saving...')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -209,7 +209,7 @@ describe('NotificationsTab', () => {
|
||||
|
||||
render(<NotificationsTab />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Webhook URL input should be present
|
||||
@@ -238,7 +238,7 @@ describe('NotificationsTab', () => {
|
||||
|
||||
render(<NotificationsTab />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const input = await screen.findByRole('textbox');
|
||||
@@ -265,7 +265,7 @@ describe('NotificationsTab', () => {
|
||||
|
||||
render(<NotificationsTab />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const input = await screen.findByRole('textbox');
|
||||
@@ -297,7 +297,7 @@ describe('NotificationsTab', () => {
|
||||
|
||||
render(<NotificationsTab />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
await screen.findByRole('textbox');
|
||||
@@ -330,7 +330,7 @@ describe('NotificationsTab', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const input = await screen.findByRole('textbox');
|
||||
@@ -371,7 +371,7 @@ describe('NotificationsTab', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading…')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const input = await screen.findByRole('textbox');
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function NotificationsTab(): React.ReactElement {
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (!matrix) return <p style={{ fontSize: 12, color: 'var(--text-faint)', fontStyle: 'italic' }}>Loading…</p>
|
||||
if (!matrix) return <p style={{ fontSize: 12, color: 'var(--text-faint)', fontStyle: 'italic' }}>{t('common.loading')}</p>
|
||||
|
||||
if (visibleChannels.length === 0) {
|
||||
return (
|
||||
@@ -119,7 +119,7 @@ export default function NotificationsTab(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
{saving && <p style={{ fontSize: 11, color: 'var(--text-faint)', marginBottom: 8 }}>Saving…</p>}
|
||||
{saving && <p style={{ fontSize: 11, color: 'var(--text-faint)', marginBottom: 8 }}>{t('common.saving')}</p>}
|
||||
{matrix.available_channels.webhook && (
|
||||
<div style={{ marginBottom: 16, padding: '12px', background: 'var(--bg-secondary)', borderRadius: 8, border: '1px solid var(--border-primary)' }}>
|
||||
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 4 }}>
|
||||
|
||||
@@ -253,7 +253,7 @@ export default function PhotoProvidersSection(): React.ReactElement {
|
||||
onClick={() => handleSaveProvider(provider)}
|
||||
disabled={!canSave || !!saving[provider.id] || isProviderSaveDisabled(provider)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-sm hover:bg-slate-700 disabled:bg-slate-400"
|
||||
title={!canSave ? 'Save route is not configured for this provider' : isProviderSaveDisabled(provider) ? 'Please fill all required fields' : ''}
|
||||
title={!canSave ? t('memories.saveRouteNotConfigured') : isProviderSaveDisabled(provider) ? t('memories.fillRequiredFields') : ''}
|
||||
>
|
||||
<Save className="w-4 h-4" /> {t('common.save')}
|
||||
</button>
|
||||
@@ -261,7 +261,7 @@ export default function PhotoProvidersSection(): React.ReactElement {
|
||||
onClick={() => handleTestProvider(provider)}
|
||||
disabled={!canTest || testing}
|
||||
className="flex items-center gap-2 px-4 py-2 border border-slate-200 rounded-lg text-sm hover:bg-slate-50"
|
||||
title={!canTest ? 'Test route is not configured for this provider' : ''}
|
||||
title={!canTest ? t('memories.testRouteNotConfigured') : ''}
|
||||
>
|
||||
{testing
|
||||
? <div className="w-4 h-4 border-2 border-slate-300 border-t-slate-700 rounded-full animate-spin" />
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -385,8 +385,8 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
|
||||
try {
|
||||
await tripsApi.removeMember(trip!.id, m.id)
|
||||
setExistingMembers(prev => prev.filter(x => x.id !== m.id))
|
||||
toast.success(`${m.username} removed`)
|
||||
} catch { toast.error('Failed to remove') }
|
||||
toast.success(t('trips.memberRemoved', { username: m.username }))
|
||||
} catch { toast.error(t('trips.memberRemoveError')) }
|
||||
}}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 5, padding: '4px 10px', borderRadius: 99,
|
||||
@@ -431,8 +431,8 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
|
||||
try {
|
||||
await tripsApi.addMember(trip.id, user.username)
|
||||
setExistingMembers(prev => [...prev, { id: user.id, username: user.username }])
|
||||
toast.success(`${user.username} added`)
|
||||
} catch { toast.error('Failed to add') }
|
||||
toast.success(t('trips.memberAdded', { username: user.username }))
|
||||
} catch { toast.error(t('trips.memberAddError')) }
|
||||
}
|
||||
} else {
|
||||
setSelectedMembers(prev => prev.includes(Number(value)) ? prev : [...prev, Number(value)])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useTripStore } from '../store/tripStore'
|
||||
import { useToast } from '../components/shared/Toast'
|
||||
import { useTranslation } from '../i18n'
|
||||
import type { MergedItem, DayNotesMap, DayNote } from '../types'
|
||||
|
||||
interface NoteUiState {
|
||||
@@ -21,6 +22,7 @@ export function useDayNotes(tripId: number | string) {
|
||||
const noteInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const tripStore = useTripStore()
|
||||
const toast = useToast()
|
||||
const { t } = useTranslation()
|
||||
const dayNotes: DayNotesMap = tripStore.dayNotes || {}
|
||||
|
||||
const openAddNote = (dayId: number, getMergedItems: (dayId: number) => MergedItem[], expandDay?: (dayId: number) => void) => {
|
||||
@@ -50,12 +52,12 @@ export function useDayNotes(tripId: number | string) {
|
||||
await tripStore.updateDayNote(tripId, dayId, ui.noteId!, { text: ui.text.trim(), time: ui.time || null, icon: ui.icon || 'FileText' })
|
||||
}
|
||||
cancelNote(dayId)
|
||||
} 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 deleteNote = async (dayId: number, noteId: number) => {
|
||||
try { await tripStore.deleteDayNote(tripId, dayId, noteId) }
|
||||
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 moveNote = async (dayId: number, noteId: number, direction: 'up' | 'down', getMergedItems: (dayId: number) => MergedItem[]) => {
|
||||
@@ -71,7 +73,7 @@ export function useDayNotes(tripId: number | string) {
|
||||
newSortOrder = idx < merged.length - 2 ? (merged[idx + 1].sortKey + merged[idx + 2].sortKey) / 2 : merged[idx + 1].sortKey + 1
|
||||
}
|
||||
try { await tripStore.updateDayNote(tripId, dayId, noteId, { sort_order: newSortOrder }) }
|
||||
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')) }
|
||||
}
|
||||
|
||||
return { noteUi, setNoteUi, noteInputRef, dayNotes, openAddNote, openEditNote, cancelNote, saveNote, deleteNote, moveNote }
|
||||
|
||||
@@ -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': 'إغلاق',
|
||||
@@ -31,6 +33,12 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.password': 'كلمة المرور',
|
||||
'common.saving': 'جارٍ الحفظ...',
|
||||
'common.saved': 'تم الحفظ',
|
||||
'common.expand': 'توسيع',
|
||||
'common.collapse': 'طي',
|
||||
'trips.memberRemoved': '{username} تمت إزالته',
|
||||
'trips.memberRemoveError': 'فشل في الإزالة',
|
||||
'trips.memberAdded': '{username} تمت إضافته',
|
||||
'trips.memberAddError': 'فشل في الإضافة',
|
||||
'trips.reminder': 'تذكير',
|
||||
'trips.reminderNone': 'بدون',
|
||||
'trips.reminderDay': 'يوم',
|
||||
@@ -416,6 +424,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': 'كلمتا المرور غير متطابقتين',
|
||||
@@ -934,6 +946,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'inspector.files': 'الملفات',
|
||||
'inspector.filesCount': '{count} ملفات',
|
||||
'inspector.removeFromDay': 'إزالة من اليوم',
|
||||
'inspector.remove': 'إزالة',
|
||||
'inspector.addToDay': 'إضافة إلى اليوم',
|
||||
'inspector.confirmedRes': 'حجز مؤكد',
|
||||
'inspector.pendingRes': 'حجز قيد الانتظار',
|
||||
@@ -1084,9 +1097,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}',
|
||||
@@ -1165,7 +1182,6 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.menuCheckAll': 'تحديد الكل',
|
||||
'packing.menuUncheckAll': 'إلغاء تحديد الكل',
|
||||
'packing.menuDeleteCat': 'حذف الفئة',
|
||||
'packing.assignUser': 'تعيين مستخدم',
|
||||
'packing.noMembers': 'لا أعضاء',
|
||||
'packing.addItem': 'إضافة عنصر',
|
||||
'packing.addItemPlaceholder': 'اسم العنصر...',
|
||||
@@ -1333,6 +1349,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': 'ارفع صور رحلتك',
|
||||
@@ -1340,6 +1363,12 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'photos.linkPlace': 'ربط بمكان',
|
||||
'photos.noPlace': 'بلا مكان',
|
||||
'photos.uploadN': 'رفع {n} صورة',
|
||||
'photos.linkDay': 'ربط اليوم',
|
||||
'photos.noDay': 'لا يوم',
|
||||
'photos.dayLabel': 'اليوم {number}',
|
||||
'photos.photoSelected': 'صورة محددة',
|
||||
'photos.photosSelected': 'صور محددة',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · الحد الأقصى 10 ميغابايت · حتى 30 صورة',
|
||||
|
||||
// Backup restore modal
|
||||
'backup.restoreConfirmTitle': 'استعادة النسخة الاحتياطية؟',
|
||||
@@ -1366,6 +1395,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': 'تمت إضافة الحجز',
|
||||
@@ -1478,6 +1508,9 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.saved': 'تم حفظ إعدادات {provider_name}',
|
||||
'memories.providerDisconnectedBanner': 'اتصالك بـ {provider_name} مفقود. أعد الاتصال في الإعدادات لعرض الصور.',
|
||||
'memories.saveError': 'تعذّر حفظ إعدادات {provider_name}',
|
||||
'memories.saveRouteNotConfigured': 'مسار الحفظ غير مهيأ لهذا المزود',
|
||||
'memories.testRouteNotConfigured': 'مسار الاختبار غير مهيأ لهذا المزود',
|
||||
'memories.fillRequiredFields': 'يرجى ملء جميع الحقول المطلوبة',
|
||||
'memories.oldest': 'الأقدم أولاً',
|
||||
'memories.newest': 'الأحدث أولاً',
|
||||
'memories.allLocations': 'جميع المواقع',
|
||||
@@ -1502,6 +1535,10 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.confirmShareTitle': 'مشاركة مع أعضاء الرحلة؟',
|
||||
'memories.confirmShareHint': '{count} صور ستكون مرئية لجميع أعضاء هذه الرحلة. يمكنك جعل الصور الفردية خاصة لاحقًا.',
|
||||
'memories.confirmShareButton': 'مشاركة الصور',
|
||||
'journey.settings.failedToDelete': 'فشل في الحذف',
|
||||
'journey.entries.deleteTitle': 'حذف الإدخال',
|
||||
'journey.photosUploaded': 'تم رفع {count} صورة',
|
||||
'journey.photosAdded': 'تمت إضافة {count} صورة',
|
||||
|
||||
// Collab Addon
|
||||
'collab.tabs.chat': 'الدردشة',
|
||||
|
||||
@@ -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',
|
||||
@@ -27,11 +29,17 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.password': 'Senha',
|
||||
'common.saving': 'Salvando...',
|
||||
'common.saved': 'Salvo',
|
||||
'common.expand': 'Expandir',
|
||||
'common.collapse': 'Recolher',
|
||||
'trips.reminder': 'Lembrete',
|
||||
'trips.reminderNone': 'Nenhum',
|
||||
'trips.reminderDay': 'dia',
|
||||
'trips.reminderDays': 'dias',
|
||||
'trips.reminderCustom': 'Personalizado',
|
||||
'trips.memberRemoved': '{username} removido',
|
||||
'trips.memberRemoveError': 'Falha ao remover',
|
||||
'trips.memberAdded': '{username} adicionado',
|
||||
'trips.memberAddError': 'Falha ao adicionar',
|
||||
'trips.reminderDaysBefore': 'dias antes da partida',
|
||||
'trips.reminderDisabledHint': 'Os lembretes de viagem estão desativados. Ative-os em Admin > Configurações > Notificações.',
|
||||
'common.update': 'Atualizar',
|
||||
@@ -411,6 +419,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',
|
||||
@@ -903,6 +915,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'inspector.files': 'Arquivos',
|
||||
'inspector.filesCount': '{count} arquivos',
|
||||
'inspector.removeFromDay': 'Remover do dia',
|
||||
'inspector.remove': 'Remover',
|
||||
'inspector.addToDay': 'Adicionar ao dia',
|
||||
'inspector.confirmedRes': 'Reserva confirmada',
|
||||
'inspector.pendingRes': 'Reserva pendente',
|
||||
@@ -1053,9 +1066,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 +1141,9 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.allPacked': 'Tudo na mala!',
|
||||
'packing.addPlaceholder': 'Adicionar item...',
|
||||
'packing.categoryPlaceholder': 'Categoria...',
|
||||
'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 +1154,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 +1321,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',
|
||||
@@ -1309,6 +1335,12 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'photos.linkPlace': 'Vincular lugar',
|
||||
'photos.noPlace': 'Sem lugar',
|
||||
'photos.uploadN': 'Enviar {n} foto(s)',
|
||||
'photos.linkDay': 'Vincular dia',
|
||||
'photos.noDay': 'Nenhum dia',
|
||||
'photos.dayLabel': 'Dia {number}',
|
||||
'photos.photoSelected': 'Foto selecionada',
|
||||
'photos.photosSelected': 'Fotos selecionadas',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · máx. 10 MB · até 30 fotos',
|
||||
|
||||
// Backup restore modal
|
||||
'backup.restoreConfirmTitle': 'Restaurar backup?',
|
||||
@@ -1335,6 +1367,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',
|
||||
@@ -1781,6 +1814,9 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.providerUsername': 'Nome de usuário',
|
||||
'memories.providerPassword': 'Senha',
|
||||
'memories.saveError': 'Não foi possível salvar as configurações de {provider_name}',
|
||||
'memories.saveRouteNotConfigured': 'A rota de salvamento não está configurada para este provedor',
|
||||
'memories.testRouteNotConfigured': 'A rota de teste não está configurada para este provedor',
|
||||
'memories.fillRequiredFields': 'Por favor preencha todos os campos obrigatórios',
|
||||
'memories.selectAlbumMultiple': 'Selecionar álbum',
|
||||
'memories.selectPhotosMultiple': 'Selecionar fotos',
|
||||
'journey.title': 'Jornada',
|
||||
@@ -1950,6 +1986,10 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.saveFailed': 'Não foi possível salvar',
|
||||
'journey.settings.coverUpdated': 'Capa atualizada',
|
||||
'journey.settings.coverFailed': 'Falha no envio',
|
||||
'journey.settings.failedToDelete': 'Falha ao excluir',
|
||||
'journey.entries.deleteTitle': 'Excluir entrada',
|
||||
'journey.photosUploaded': '{count} fotos enviadas',
|
||||
'journey.photosAdded': '{count} fotos adicionadas',
|
||||
'journey.public.notFound': 'Não encontrado',
|
||||
'journey.public.notFoundMessage': 'Esta jornada não existe ou o link expirou.',
|
||||
'journey.public.readOnly': 'Somente leitura · Jornada pública',
|
||||
|
||||
@@ -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',
|
||||
@@ -26,6 +28,12 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Heslo',
|
||||
'common.saving': 'Ukládání...',
|
||||
'trips.memberRemoved': '{username} odebrán',
|
||||
'trips.memberRemoveError': 'Odebrání se nezdařilo',
|
||||
'trips.memberAdded': '{username} přidán',
|
||||
'trips.memberAddError': 'Přidání se nezdařilo',
|
||||
'common.expand': 'Rozbalit',
|
||||
'common.collapse': 'Sbalit',
|
||||
'common.saved': 'Uloženo',
|
||||
'trips.reminder': 'Připomínka',
|
||||
'trips.reminderNone': 'Žádná',
|
||||
@@ -411,6 +419,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í',
|
||||
@@ -932,6 +944,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'inspector.files': 'Soubory',
|
||||
'inspector.filesCount': '{count} souborů',
|
||||
'inspector.removeFromDay': 'Odebrat ze dne',
|
||||
'inspector.remove': 'Odstranit',
|
||||
'inspector.addToDay': 'Přidat ke dni',
|
||||
'inspector.confirmedRes': 'Potvrzená rezervace',
|
||||
'inspector.pendingRes': 'Čekající rezervace',
|
||||
@@ -1082,9 +1095,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',
|
||||
@@ -1163,7 +1180,6 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.menuCheckAll': 'Označit vše',
|
||||
'packing.menuUncheckAll': 'Odznačit vše',
|
||||
'packing.menuDeleteCat': 'Smazat kategorii',
|
||||
'packing.assignUser': 'Přiřadit uživatele',
|
||||
'packing.noMembers': 'Žádní členové cesty',
|
||||
'packing.addItem': 'Přidat položku',
|
||||
'packing.addItemPlaceholder': 'Název položky...',
|
||||
@@ -1331,6 +1347,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',
|
||||
@@ -1338,6 +1361,12 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'photos.linkPlace': 'Propojit s místem',
|
||||
'photos.noPlace': 'Žádné místo',
|
||||
'photos.uploadN': 'Nahrát {n} fotek',
|
||||
'photos.linkDay': 'Propojit den',
|
||||
'photos.noDay': 'Žádný den',
|
||||
'photos.dayLabel': 'Den {number}',
|
||||
'photos.photoSelected': 'Fotografie vybrána',
|
||||
'photos.photosSelected': 'Fotografie vybrány',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · max. 10 MB · až 30 fotografií',
|
||||
|
||||
// Obnovení zálohy
|
||||
'backup.restoreConfirmTitle': 'Obnovit zálohu?',
|
||||
@@ -1364,6 +1393,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',
|
||||
@@ -1786,6 +1816,9 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.providerUsername': 'Uživatelské jméno',
|
||||
'memories.providerPassword': 'Heslo',
|
||||
'memories.saveError': 'Nepodařilo se uložit nastavení {provider_name}',
|
||||
'memories.saveRouteNotConfigured': 'Trasa uložení není nakonfigurována pro tohoto poskytovatele',
|
||||
'memories.testRouteNotConfigured': 'Testovací trasa není nakonfigurována pro tohoto poskytovatele',
|
||||
'memories.fillRequiredFields': 'Prosím vyplňte všechna povinná pole',
|
||||
'memories.selectAlbumMultiple': 'Vybrat album',
|
||||
'memories.selectPhotosMultiple': 'Vybrat fotky',
|
||||
'journey.title': 'Cestovní deník',
|
||||
@@ -1955,6 +1988,10 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.saveFailed': 'Uložení selhalo',
|
||||
'journey.settings.coverUpdated': 'Obal aktualizován',
|
||||
'journey.settings.coverFailed': 'Nahrávání selhalo',
|
||||
'journey.settings.failedToDelete': 'Smazání se nezdařilo',
|
||||
'journey.entries.deleteTitle': 'Smazat záznam',
|
||||
'journey.photosUploaded': '{count} fotografií nahráno',
|
||||
'journey.photosAdded': '{count} fotografií přidáno',
|
||||
'journey.public.notFound': 'Nenalezeno',
|
||||
'journey.public.notFoundMessage': 'Tento cestovní deník neexistuje nebo odkaz vypršel.',
|
||||
'journey.public.readOnly': 'Pouze ke čtení · Veřejný cestovní deník',
|
||||
|
||||
@@ -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',
|
||||
@@ -26,6 +28,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.email': 'E-Mail',
|
||||
'common.password': 'Passwort',
|
||||
'common.saving': 'Speichern...',
|
||||
'common.expand': 'Erweitern',
|
||||
'common.collapse': 'Einklappen',
|
||||
'common.justNow': 'gerade eben',
|
||||
'common.hoursAgo': 'vor {count}h',
|
||||
'common.daysAgo': 'vor {count}T',
|
||||
@@ -35,6 +39,10 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'trips.reminderDay': 'Tag',
|
||||
'trips.reminderDays': 'Tage',
|
||||
'trips.reminderCustom': 'Benutzerdefiniert',
|
||||
'trips.memberRemoved': '{username} entfernt',
|
||||
'trips.memberRemoveError': 'Entfernen fehlgeschlagen',
|
||||
'trips.memberAdded': '{username} hinzugefügt',
|
||||
'trips.memberAddError': 'Hinzufügen fehlgeschlagen',
|
||||
'trips.reminderDaysBefore': 'Tage vor Abreise',
|
||||
'trips.reminderDisabledHint': 'Reiseerinnerungen sind deaktiviert. Aktivieren Sie sie unter Admin > Einstellungen > Benachrichtigungen.',
|
||||
'common.update': 'Aktualisieren',
|
||||
@@ -414,6 +422,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',
|
||||
@@ -934,6 +946,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'inspector.files': 'Dateien',
|
||||
'inspector.filesCount': '{count} Dateien',
|
||||
'inspector.removeFromDay': 'Vom Tag entfernen',
|
||||
'inspector.remove': 'Entfernen',
|
||||
'inspector.addToDay': 'Zum Tag hinzufügen',
|
||||
'inspector.confirmedRes': 'Bestätigte Reservierung',
|
||||
'inspector.pendingRes': 'Ausstehende Reservierung',
|
||||
@@ -1087,6 +1100,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',
|
||||
@@ -1165,7 +1181,6 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.menuCheckAll': 'Alle abhaken',
|
||||
'packing.menuUncheckAll': 'Alle Haken entfernen',
|
||||
'packing.menuDeleteCat': 'Kategorie löschen',
|
||||
'packing.assignUser': 'Benutzer zuweisen',
|
||||
'packing.noMembers': 'Keine Mitglieder',
|
||||
'packing.addItem': 'Eintrag hinzufügen',
|
||||
'packing.addItemPlaceholder': 'Artikelname...',
|
||||
@@ -1333,6 +1348,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',
|
||||
@@ -1340,6 +1362,12 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'photos.linkPlace': 'Ort verknüpfen',
|
||||
'photos.noPlace': 'Kein Ort',
|
||||
'photos.uploadN': '{n} Foto(s) hochladen',
|
||||
'photos.linkDay': 'Tag verknüpfen',
|
||||
'photos.noDay': 'Kein Tag',
|
||||
'photos.dayLabel': 'Tag {number}',
|
||||
'photos.photoSelected': 'Foto ausgewählt',
|
||||
'photos.photosSelected': 'Fotos ausgewählt',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · max. 10 MB · bis zu 30 Fotos',
|
||||
|
||||
// Backup restore modal
|
||||
'backup.restoreConfirmTitle': 'Backup wiederherstellen?',
|
||||
@@ -1366,6 +1394,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',
|
||||
@@ -1478,6 +1507,9 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.saved': '{provider_name}-Einstellungen gespeichert',
|
||||
'memories.providerDisconnectedBanner': 'Deine {provider_name}-Verbindung wurde getrennt. Verbinde erneut in den Einstellungen, um Fotos anzuzeigen.',
|
||||
'memories.saveError': '{provider_name}-Einstellungen konnten nicht gespeichert werden',
|
||||
'memories.saveRouteNotConfigured': 'Speicherroute ist für diesen Anbieter nicht konfiguriert',
|
||||
'memories.testRouteNotConfigured': 'Testroute ist für diesen Anbieter nicht konfiguriert',
|
||||
'memories.fillRequiredFields': 'Bitte füllen Sie alle Pflichtfelder aus',
|
||||
'memories.addPhotos': 'Fotos hinzufügen',
|
||||
'memories.linkAlbum': 'Album verknüpfen',
|
||||
'memories.selectAlbum': 'Immich-Album auswählen',
|
||||
@@ -1944,6 +1976,10 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.saveFailed': 'Speichern fehlgeschlagen',
|
||||
'journey.settings.coverUpdated': 'Titelbild aktualisiert',
|
||||
'journey.settings.coverFailed': 'Upload fehlgeschlagen',
|
||||
'journey.settings.failedToDelete': 'Löschen fehlgeschlagen',
|
||||
'journey.entries.deleteTitle': 'Eintrag löschen',
|
||||
'journey.photosUploaded': '{count} Fotos hochgeladen',
|
||||
'journey.photosAdded': '{count} Fotos hinzugefügt',
|
||||
'journey.public.notFound': 'Nicht gefunden',
|
||||
'journey.public.notFoundMessage': 'Diese Journey existiert nicht oder der Link ist abgelaufen.',
|
||||
'journey.public.readOnly': 'Nur lesen · Öffentliche Journey',
|
||||
|
||||
@@ -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',
|
||||
@@ -30,6 +32,10 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.hoursAgo': '{count}h ago',
|
||||
'common.daysAgo': '{count}d ago',
|
||||
'common.saved': 'Saved',
|
||||
'trips.memberRemoved': '{username} removed',
|
||||
'trips.memberRemoveError': 'Failed to remove',
|
||||
'trips.memberAdded': '{username} added',
|
||||
'trips.memberAddError': 'Failed to add',
|
||||
'trips.reminder': 'Reminder',
|
||||
'trips.reminderNone': 'None',
|
||||
'trips.reminderDay': 'day',
|
||||
@@ -42,6 +48,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.uploading': 'Uploading…',
|
||||
'common.backToPlanning': 'Back to Planning',
|
||||
'common.reset': 'Reset',
|
||||
'common.expand': 'Expand',
|
||||
'common.collapse': 'Collapse',
|
||||
|
||||
// Navbar
|
||||
'nav.trip': 'Trip',
|
||||
@@ -438,6 +446,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',
|
||||
@@ -955,6 +967,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'inspector.showHours': 'Show opening hours',
|
||||
'inspector.files': 'Files',
|
||||
'inspector.filesCount': '{count} files',
|
||||
'inspector.remove': 'Remove',
|
||||
'inspector.removeFromDay': 'Remove from Day',
|
||||
'inspector.addToDay': 'Add to Day',
|
||||
'inspector.confirmedRes': 'Confirmed Reservation',
|
||||
@@ -1109,6 +1122,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',
|
||||
@@ -1187,7 +1203,6 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.menuCheckAll': 'Check All',
|
||||
'packing.menuUncheckAll': 'Uncheck All',
|
||||
'packing.menuDeleteCat': 'Delete Category',
|
||||
'packing.assignUser': 'Assign user',
|
||||
'packing.noMembers': 'No trip members',
|
||||
'packing.addItem': 'Add item',
|
||||
'packing.addItemPlaceholder': 'Item name...',
|
||||
@@ -1200,7 +1215,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 +1370,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',
|
||||
@@ -1363,6 +1384,12 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'photos.linkPlace': 'Link Place',
|
||||
'photos.noPlace': 'No Place',
|
||||
'photos.uploadN': '{n} photo(s) upload',
|
||||
'photos.linkDay': 'Link Day',
|
||||
'photos.noDay': 'No Day',
|
||||
'photos.dayLabel': 'Day {number}',
|
||||
'photos.photoSelected': 'Photo selected',
|
||||
'photos.photosSelected': 'Photos selected',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · max. 10 MB · up to 30 photos',
|
||||
|
||||
// Backup restore modal
|
||||
'backup.restoreConfirmTitle': 'Restore Backup?',
|
||||
@@ -1389,6 +1416,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',
|
||||
@@ -1536,6 +1564,9 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.error.addPhotos': 'Failed to add photos',
|
||||
'memories.error.removePhoto': 'Failed to remove photo',
|
||||
'memories.error.toggleSharing': 'Failed to update sharing',
|
||||
'memories.saveRouteNotConfigured': 'Save route is not configured for this provider',
|
||||
'memories.testRouteNotConfigured': 'Test route is not configured for this provider',
|
||||
'memories.fillRequiredFields': 'Please fill all required fields',
|
||||
|
||||
// Collab Addon
|
||||
'collab.tabs.chat': 'Chat',
|
||||
@@ -1968,6 +1999,10 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.saveFailed': 'Failed to save',
|
||||
'journey.settings.coverUpdated': 'Cover updated',
|
||||
'journey.settings.coverFailed': 'Upload failed',
|
||||
'journey.settings.failedToDelete': 'Failed to delete',
|
||||
'journey.entries.deleteTitle': 'Delete Entry',
|
||||
'journey.photosUploaded': '{count} photos uploaded',
|
||||
'journey.photosAdded': '{count} photos added',
|
||||
|
||||
// Journey — Public Page
|
||||
'journey.public.notFound': 'Not Found',
|
||||
|
||||
@@ -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',
|
||||
@@ -27,11 +29,17 @@ const es: Record<string, string> = {
|
||||
'common.password': 'Contraseña',
|
||||
'common.saving': 'Guardando...',
|
||||
'common.saved': 'Guardado',
|
||||
'common.expand': 'Expandir',
|
||||
'common.collapse': 'Contraer',
|
||||
'trips.reminder': 'Recordatorio',
|
||||
'trips.reminderNone': 'Ninguno',
|
||||
'trips.reminderDay': 'día',
|
||||
'trips.reminderDays': 'días',
|
||||
'trips.reminderCustom': 'Personalizado',
|
||||
'trips.memberRemoved': '{username} eliminado',
|
||||
'trips.memberRemoveError': 'Error al eliminar',
|
||||
'trips.memberAdded': '{username} añadido',
|
||||
'trips.memberAddError': 'Error al añadir',
|
||||
'trips.reminderDaysBefore': 'días antes de la salida',
|
||||
'trips.reminderDisabledHint': 'Los recordatorios de viaje están desactivados. Actívalos en Admin > Configuración > Notificaciones.',
|
||||
'common.update': 'Actualizar',
|
||||
@@ -403,6 +411,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',
|
||||
@@ -907,6 +919,7 @@ const es: Record<string, string> = {
|
||||
'inspector.files': 'Archivos',
|
||||
'inspector.filesCount': '{count} archivos',
|
||||
'inspector.removeFromDay': 'Quitar del día',
|
||||
'inspector.remove': 'Eliminar',
|
||||
'inspector.addToDay': 'Añadir al día',
|
||||
'inspector.confirmedRes': 'Reserva confirmada',
|
||||
'inspector.pendingRes': 'Reserva pendiente',
|
||||
@@ -1040,9 +1053,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',
|
||||
@@ -1110,7 +1127,6 @@ const es: Record<string, string> = {
|
||||
'packing.saveAsTemplate': 'Guardar como plantilla',
|
||||
'packing.templateName': 'Nombre de la plantilla',
|
||||
'packing.templateSaved': 'Lista de equipaje guardada como plantilla',
|
||||
'packing.assignUser': 'Asignar usuario',
|
||||
'packing.noMembers': 'Sin miembros',
|
||||
'packing.bags': 'Equipaje',
|
||||
'packing.noBag': 'Sin asignar',
|
||||
@@ -1267,6 +1283,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',
|
||||
@@ -1274,6 +1297,12 @@ const es: Record<string, string> = {
|
||||
'photos.linkPlace': 'Vincular lugar',
|
||||
'photos.noPlace': 'Sin lugar',
|
||||
'photos.uploadN': 'Subida de {n} foto(s)',
|
||||
'photos.linkDay': 'Vincular día',
|
||||
'photos.noDay': 'Ningún día',
|
||||
'photos.dayLabel': 'Día {number}',
|
||||
'photos.photoSelected': 'Foto seleccionada',
|
||||
'photos.photosSelected': 'Fotos seleccionadas',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · máx. 10 MB · hasta 30 fotos',
|
||||
'admin.addons.catalog.memories.name': 'Fotos (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Comparte fotos de viaje a través de tu instancia de Immich',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
@@ -1316,6 +1345,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',
|
||||
@@ -1427,6 +1457,9 @@ const es: Record<string, string> = {
|
||||
'memories.saved': 'Configuración de {provider_name} guardada',
|
||||
'memories.providerDisconnectedBanner': 'Se perdió la conexión con {provider_name}. Vuelve a conectar en Configuración para ver las fotos.',
|
||||
'memories.saveError': 'No se pudieron guardar los ajustes de {provider_name}',
|
||||
'memories.saveRouteNotConfigured': 'La ruta de guardado no está configurada para este proveedor',
|
||||
'memories.testRouteNotConfigured': 'La ruta de prueba no está configurada para este proveedor',
|
||||
'memories.fillRequiredFields': 'Por favor complete todos los campos requeridos',
|
||||
'memories.oldest': 'Más antiguas',
|
||||
'memories.newest': 'Más recientes',
|
||||
'memories.allLocations': 'Todas las ubicaciones',
|
||||
@@ -1957,6 +1990,10 @@ const es: Record<string, string> = {
|
||||
'journey.settings.saveFailed': 'No se pudo guardar',
|
||||
'journey.settings.coverUpdated': 'Portada actualizada',
|
||||
'journey.settings.coverFailed': 'Error al subir',
|
||||
'journey.settings.failedToDelete': 'Error al eliminar',
|
||||
'journey.entries.deleteTitle': 'Eliminar entrada',
|
||||
'journey.photosUploaded': '{count} fotos subidas',
|
||||
'journey.photosAdded': '{count} fotos añadidas',
|
||||
'journey.public.notFound': 'No encontrado',
|
||||
'journey.public.notFoundMessage': 'Esta travesía no existe o el enlace ha expirado.',
|
||||
'journey.public.readOnly': 'Solo lectura · Travesía pública',
|
||||
|
||||
@@ -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',
|
||||
@@ -27,6 +29,12 @@ const fr: Record<string, string> = {
|
||||
'common.password': 'Mot de passe',
|
||||
'common.saving': 'Enregistrement…',
|
||||
'common.saved': 'Enregistré',
|
||||
'trips.memberRemoved': '{username} supprimé',
|
||||
'trips.memberRemoveError': 'Échec de la suppression',
|
||||
'trips.memberAdded': '{username} ajouté',
|
||||
'trips.memberAddError': "Échec de l'ajout",
|
||||
'common.expand': 'Développer',
|
||||
'common.collapse': 'Réduire',
|
||||
'trips.reminder': 'Rappel',
|
||||
'trips.reminderNone': 'Aucun',
|
||||
'trips.reminderDay': 'jour',
|
||||
@@ -404,6 +412,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',
|
||||
@@ -930,6 +942,7 @@ const fr: Record<string, string> = {
|
||||
'inspector.files': 'Fichiers',
|
||||
'inspector.filesCount': '{count} fichiers',
|
||||
'inspector.removeFromDay': 'Retirer du jour',
|
||||
'inspector.remove': 'Supprimer',
|
||||
'inspector.addToDay': 'Ajouter au jour',
|
||||
'inspector.confirmedRes': 'Réservation confirmée',
|
||||
'inspector.pendingRes': 'Réservation en attente',
|
||||
@@ -1080,9 +1093,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',
|
||||
@@ -1172,7 +1189,6 @@ const fr: Record<string, string> = {
|
||||
'packing.saveAsTemplate': 'Enregistrer comme modèle',
|
||||
'packing.templateName': 'Nom du modèle',
|
||||
'packing.templateSaved': 'Liste de voyage enregistrée comme modèle',
|
||||
'packing.assignUser': 'Assigner un utilisateur',
|
||||
'packing.noMembers': 'Aucun membre',
|
||||
'packing.bags': 'Bagages',
|
||||
'packing.noBag': 'Non assigné',
|
||||
@@ -1329,6 +1345,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',
|
||||
@@ -1336,6 +1359,12 @@ const fr: Record<string, string> = {
|
||||
'photos.linkPlace': 'Lier au lieu',
|
||||
'photos.noPlace': 'Aucun lieu',
|
||||
'photos.uploadN': '{n} photo(s) importée(s)',
|
||||
'photos.linkDay': 'Lier le jour',
|
||||
'photos.noDay': 'Aucun jour',
|
||||
'photos.dayLabel': 'Jour {number}',
|
||||
'photos.photoSelected': 'Photo sélectionnée',
|
||||
'photos.photosSelected': 'Photos sélectionnées',
|
||||
'photos.fileTypeHint': "JPG, PNG, WebP · max. 10 Mo · jusqu'à 30 photos",
|
||||
|
||||
// Backup restore modal
|
||||
'backup.restoreConfirmTitle': 'Restaurer la sauvegarde ?',
|
||||
@@ -1362,6 +1391,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',
|
||||
@@ -1474,6 +1504,9 @@ const fr: Record<string, string> = {
|
||||
'memories.saved': 'Paramètres {provider_name} enregistrés',
|
||||
'memories.providerDisconnectedBanner': 'Votre connexion {provider_name} est perdue. Reconnectez-vous dans les Paramètres pour voir les photos.',
|
||||
'memories.saveError': 'Impossible d\'enregistrer les paramètres de {provider_name}',
|
||||
'memories.saveRouteNotConfigured': "La route de sauvegarde n'est pas configurée pour ce fournisseur",
|
||||
'memories.testRouteNotConfigured': "La route de test n'est pas configurée pour ce fournisseur",
|
||||
'memories.fillRequiredFields': 'Veuillez remplir tous les champs obligatoires',
|
||||
'memories.oldest': 'Plus anciennes',
|
||||
'memories.newest': 'Plus récentes',
|
||||
'memories.allLocations': 'Tous les lieux',
|
||||
@@ -1951,6 +1984,10 @@ const fr: Record<string, string> = {
|
||||
'journey.settings.saveFailed': 'Échec de l\'enregistrement',
|
||||
'journey.settings.coverUpdated': 'Couverture mise à jour',
|
||||
'journey.settings.coverFailed': 'Échec du téléversement',
|
||||
'journey.settings.failedToDelete': 'Échec de la suppression',
|
||||
'journey.entries.deleteTitle': "Supprimer l'entrée",
|
||||
'journey.photosUploaded': '{count} photos téléversées',
|
||||
'journey.photosAdded': '{count} photos ajoutées',
|
||||
'journey.public.notFound': 'Introuvable',
|
||||
'journey.public.notFoundMessage': 'Ce journal n\'existe pas ou le lien a expiré.',
|
||||
'journey.public.readOnly': 'Lecture seule · Journal public',
|
||||
|
||||
@@ -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',
|
||||
@@ -26,6 +28,12 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Jelszó',
|
||||
'common.saving': 'Mentés...',
|
||||
'trips.memberRemoved': '{username} eltávolítva',
|
||||
'trips.memberRemoveError': 'Eltávolítás sikertelen',
|
||||
'trips.memberAdded': '{username} hozzáadva',
|
||||
'trips.memberAddError': 'Hozzáadás sikertelen',
|
||||
'common.expand': 'Kibontás',
|
||||
'common.collapse': 'Összecsukás',
|
||||
'common.saved': 'Mentve',
|
||||
'trips.reminder': 'Emlékeztető',
|
||||
'trips.reminderNone': 'Nincs',
|
||||
@@ -411,6 +419,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',
|
||||
@@ -931,6 +943,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'inspector.files': 'Fájlok',
|
||||
'inspector.filesCount': '{count} fájl',
|
||||
'inspector.removeFromDay': 'Eltávolítás a napról',
|
||||
'inspector.remove': 'Eltávolítás',
|
||||
'inspector.addToDay': 'Hozzáadás a naphoz',
|
||||
'inspector.confirmedRes': 'Megerősített foglalás',
|
||||
'inspector.pendingRes': 'Függőben lévő foglalás',
|
||||
@@ -1081,9 +1094,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',
|
||||
@@ -1162,7 +1179,6 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.menuCheckAll': 'Összes kipipálása',
|
||||
'packing.menuUncheckAll': 'Összes jelölés törlése',
|
||||
'packing.menuDeleteCat': 'Kategória törlése',
|
||||
'packing.assignUser': 'Felhasználó hozzárendelése',
|
||||
'packing.noMembers': 'Nincsenek utazási tagok',
|
||||
'packing.addItem': 'Tétel hozzáadása',
|
||||
'packing.addItemPlaceholder': 'Tétel neve...',
|
||||
@@ -1330,6 +1346,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',
|
||||
@@ -1337,6 +1360,12 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'photos.linkPlace': 'Hely társítása',
|
||||
'photos.noPlace': 'Nincs hely',
|
||||
'photos.uploadN': '{n} fotó feltöltése',
|
||||
'photos.linkDay': 'Nap csatolása',
|
||||
'photos.noDay': 'Nincs nap',
|
||||
'photos.dayLabel': '{number}. nap',
|
||||
'photos.photoSelected': 'Fotó kiválasztva',
|
||||
'photos.photosSelected': 'Fotók kiválasztva',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · max. 10 MB · legfeljebb 30 fotó',
|
||||
|
||||
// Mentés visszaállítása modal
|
||||
'backup.restoreConfirmTitle': 'Mentés visszaállítása?',
|
||||
@@ -1363,6 +1392,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',
|
||||
@@ -1783,6 +1813,9 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.providerUsername': 'Felhasználónév',
|
||||
'memories.providerPassword': 'Jelszó',
|
||||
'memories.saveError': 'Nem sikerült menteni a(z) {provider_name} beállításait',
|
||||
'memories.saveRouteNotConfigured': 'A mentési útvonal nincs konfigurálva ehhez a szolgáltatóhoz',
|
||||
'memories.testRouteNotConfigured': 'A tesztútvonal nincs konfigurálva ehhez a szolgáltatóhoz',
|
||||
'memories.fillRequiredFields': 'Kérjük töltse ki az összes kötelező mezőt',
|
||||
'memories.selectAlbumMultiple': 'Album kiválasztása',
|
||||
'memories.selectPhotosMultiple': 'Fotók kiválasztása',
|
||||
'journey.title': 'Útinaplók',
|
||||
@@ -1952,6 +1985,10 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.saveFailed': 'Nem sikerült menteni',
|
||||
'journey.settings.coverUpdated': 'Borítókép frissítve',
|
||||
'journey.settings.coverFailed': 'A feltöltés sikertelen',
|
||||
'journey.settings.failedToDelete': 'Törlés sikertelen',
|
||||
'journey.entries.deleteTitle': 'Bejegyzés törlése',
|
||||
'journey.photosUploaded': '{count} fotó feltöltve',
|
||||
'journey.photosAdded': '{count} fotó hozzáadva',
|
||||
'journey.public.notFound': 'Nem található',
|
||||
'journey.public.notFoundMessage': 'Ez az útinapló nem létezik vagy a link lejárt.',
|
||||
'journey.public.readOnly': 'Csak olvasható · Nyilvános útinapló',
|
||||
|
||||
@@ -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',
|
||||
@@ -27,6 +29,12 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.password': 'Password',
|
||||
'common.saving': 'Salvataggio...',
|
||||
'common.saved': 'Salvato',
|
||||
'trips.memberRemoved': '{username} rimosso',
|
||||
'trips.memberRemoveError': 'Rimozione non riuscita',
|
||||
'trips.memberAdded': '{username} aggiunto',
|
||||
'trips.memberAddError': 'Aggiunta non riuscita',
|
||||
'common.expand': 'Espandi',
|
||||
'common.collapse': 'Comprimi',
|
||||
'trips.reminder': 'Promemoria',
|
||||
'trips.reminderNone': 'Nessuno',
|
||||
'trips.reminderDay': 'giorno',
|
||||
@@ -411,6 +419,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',
|
||||
@@ -931,6 +943,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'inspector.files': 'File',
|
||||
'inspector.filesCount': '{count} file',
|
||||
'inspector.removeFromDay': 'Rimuovi dal giorno',
|
||||
'inspector.remove': 'Rimuovi',
|
||||
'inspector.addToDay': 'Aggiungi al giorno',
|
||||
'inspector.confirmedRes': 'Prenotazione confermata',
|
||||
'inspector.pendingRes': 'Prenotazione in attesa',
|
||||
@@ -1081,9 +1094,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',
|
||||
@@ -1162,7 +1179,6 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.menuCheckAll': 'Seleziona tutti',
|
||||
'packing.menuUncheckAll': 'Deseleziona tutti',
|
||||
'packing.menuDeleteCat': 'Elimina categoria',
|
||||
'packing.assignUser': 'Assegna utente',
|
||||
'packing.noMembers': 'Nessun membro del viaggio',
|
||||
'packing.addItem': 'Aggiungi elemento',
|
||||
'packing.addItemPlaceholder': 'Nome elemento...',
|
||||
@@ -1330,6 +1346,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',
|
||||
@@ -1337,6 +1360,12 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'photos.linkPlace': 'Collega luogo',
|
||||
'photos.noPlace': 'Nessun luogo',
|
||||
'photos.uploadN': 'Caricamento di {n} foto',
|
||||
'photos.linkDay': 'Collega giorno',
|
||||
'photos.noDay': 'Nessun giorno',
|
||||
'photos.dayLabel': 'Giorno {number}',
|
||||
'photos.photoSelected': 'Foto selezionata',
|
||||
'photos.photosSelected': 'Foto selezionate',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · max. 10 MB · fino a 30 foto',
|
||||
|
||||
// Backup restore modal
|
||||
'backup.restoreConfirmTitle': 'Ripristinare il backup?',
|
||||
@@ -1363,6 +1392,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',
|
||||
@@ -1475,6 +1505,9 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.saved': 'Impostazioni {provider_name} salvate',
|
||||
'memories.providerDisconnectedBanner': 'La connessione a {provider_name} è persa. Riconnetti nelle Impostazioni per visualizzare le foto.',
|
||||
'memories.saveError': 'Impossibile salvare le impostazioni di {provider_name}',
|
||||
'memories.saveRouteNotConfigured': 'La route di salvataggio non è configurata per questo provider',
|
||||
'memories.testRouteNotConfigured': 'La route di test non è configurata per questo provider',
|
||||
'memories.fillRequiredFields': 'Per favore compila tutti i campi obbligatori',
|
||||
'memories.addPhotos': 'Aggiungi foto',
|
||||
'memories.linkAlbum': 'Collega album',
|
||||
'memories.selectAlbum': 'Seleziona album Immich',
|
||||
@@ -1952,6 +1985,10 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.saveFailed': 'Salvataggio fallito',
|
||||
'journey.settings.coverUpdated': 'Copertina aggiornata',
|
||||
'journey.settings.coverFailed': 'Caricamento fallito',
|
||||
'journey.settings.failedToDelete': 'Eliminazione non riuscita',
|
||||
'journey.entries.deleteTitle': 'Elimina voce',
|
||||
'journey.photosUploaded': '{count} foto caricate',
|
||||
'journey.photosAdded': '{count} foto aggiunte',
|
||||
'journey.public.notFound': 'Non trovato',
|
||||
'journey.public.notFoundMessage': 'Questo diario non esiste o il link è scaduto.',
|
||||
'journey.public.readOnly': 'Sola lettura · Diario pubblico',
|
||||
|
||||
@@ -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',
|
||||
@@ -27,6 +29,12 @@ const nl: Record<string, string> = {
|
||||
'common.password': 'Wachtwoord',
|
||||
'common.saving': 'Opslaan...',
|
||||
'common.saved': 'Opgeslagen',
|
||||
'trips.memberRemoved': '{username} verwijderd',
|
||||
'trips.memberRemoveError': 'Verwijderen mislukt',
|
||||
'trips.memberAdded': '{username} toegevoegd',
|
||||
'trips.memberAddError': 'Toevoegen mislukt',
|
||||
'common.expand': 'Uitvouwen',
|
||||
'common.collapse': 'Inklappen',
|
||||
'trips.reminder': 'Herinnering',
|
||||
'trips.reminderNone': 'Geen',
|
||||
'trips.reminderDay': 'dag',
|
||||
@@ -404,6 +412,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',
|
||||
@@ -930,6 +942,7 @@ const nl: Record<string, string> = {
|
||||
'inspector.files': 'Bestanden',
|
||||
'inspector.filesCount': '{count} bestanden',
|
||||
'inspector.removeFromDay': 'Verwijderen van dag',
|
||||
'inspector.remove': 'Verwijderen',
|
||||
'inspector.addToDay': 'Toevoegen aan dag',
|
||||
'inspector.confirmedRes': 'Bevestigde reservering',
|
||||
'inspector.pendingRes': 'Reservering in behandeling',
|
||||
@@ -1080,9 +1093,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',
|
||||
@@ -1161,7 +1178,6 @@ const nl: Record<string, string> = {
|
||||
'packing.menuCheckAll': 'Alles aanvinken',
|
||||
'packing.menuUncheckAll': 'Alles uitvinken',
|
||||
'packing.menuDeleteCat': 'Categorie verwijderen',
|
||||
'packing.assignUser': 'Gebruiker toewijzen',
|
||||
'packing.addItem': 'Item toevoegen',
|
||||
'packing.addItemPlaceholder': 'Itemnaam...',
|
||||
'packing.addCategory': 'Categorie toevoegen',
|
||||
@@ -1329,6 +1345,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',
|
||||
@@ -1336,6 +1359,12 @@ const nl: Record<string, string> = {
|
||||
'photos.linkPlace': 'Koppel plaats',
|
||||
'photos.noPlace': 'Geen plaats',
|
||||
'photos.uploadN': '{n} foto(\'s) uploaden',
|
||||
'photos.linkDay': 'Dag koppelen',
|
||||
'photos.noDay': 'Geen dag',
|
||||
'photos.dayLabel': 'Dag {number}',
|
||||
'photos.photoSelected': 'Foto geselecteerd',
|
||||
'photos.photosSelected': "Foto's geselecteerd",
|
||||
'photos.fileTypeHint': "JPG, PNG, WebP · max. 10 MB · tot 30 foto's",
|
||||
|
||||
// Backup restore modal
|
||||
'backup.restoreConfirmTitle': 'Back-up herstellen?',
|
||||
@@ -1362,6 +1391,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',
|
||||
@@ -1474,6 +1504,9 @@ const nl: Record<string, string> = {
|
||||
'memories.saved': '{provider_name}-instellingen opgeslagen',
|
||||
'memories.providerDisconnectedBanner': 'Je {provider_name}-verbinding is verbroken. Maak opnieuw verbinding in Instellingen om foto\'s te bekijken.',
|
||||
'memories.saveError': '{provider_name}-instellingen konden niet worden opgeslagen',
|
||||
'memories.saveRouteNotConfigured': 'Opslagroute is niet geconfigureerd voor deze provider',
|
||||
'memories.testRouteNotConfigured': 'Testroute is niet geconfigureerd voor deze provider',
|
||||
'memories.fillRequiredFields': 'Vul alle verplichte velden in',
|
||||
'memories.oldest': 'Oudste eerst',
|
||||
'memories.newest': 'Nieuwste eerst',
|
||||
'memories.allLocations': 'Alle locaties',
|
||||
@@ -1951,6 +1984,10 @@ const nl: Record<string, string> = {
|
||||
'journey.settings.saveFailed': 'Opslaan mislukt',
|
||||
'journey.settings.coverUpdated': 'Omslag bijgewerkt',
|
||||
'journey.settings.coverFailed': 'Uploaden mislukt',
|
||||
'journey.settings.failedToDelete': 'Verwijderen mislukt',
|
||||
'journey.entries.deleteTitle': 'Vermelding verwijderen',
|
||||
'journey.photosUploaded': "{count} foto's geüpload",
|
||||
'journey.photosAdded': "{count} foto's toegevoegd",
|
||||
'journey.public.notFound': 'Niet gevonden',
|
||||
'journey.public.notFoundMessage': 'Dit reisverslag bestaat niet of de link is verlopen.',
|
||||
'journey.public.readOnly': 'Alleen-lezen · Openbaar reisverslag',
|
||||
|
||||
@@ -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',
|
||||
@@ -25,6 +27,12 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Hasło',
|
||||
'common.saving': 'Zapisywanie...',
|
||||
'trips.memberRemoved': '{username} usunięty',
|
||||
'trips.memberRemoveError': 'Nie udało się usunąć',
|
||||
'trips.memberAdded': '{username} dodany',
|
||||
'trips.memberAddError': 'Nie udało się dodać',
|
||||
'common.expand': 'Rozwiń',
|
||||
'common.collapse': 'Zwiń',
|
||||
'common.update': 'Aktualizuj',
|
||||
'common.change': 'Zmień',
|
||||
'common.uploading': 'Przesyłanie...',
|
||||
@@ -381,6 +389,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',
|
||||
@@ -892,6 +904,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'inspector.files': 'Pliki',
|
||||
'inspector.filesCount': '{count} plików',
|
||||
'inspector.removeFromDay': 'Usuń z dnia',
|
||||
'inspector.remove': 'Usuń',
|
||||
'inspector.addToDay': 'Dodaj do dnia',
|
||||
'inspector.confirmedRes': 'Potwierdzona rezerwacja',
|
||||
'inspector.pendingRes': 'Oczekująca rezerwacja',
|
||||
@@ -1042,6 +1055,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',
|
||||
@@ -1120,7 +1136,9 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'packing.menuCheckAll': 'Zaznacz wszystko',
|
||||
'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 +1306,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',
|
||||
@@ -1295,6 +1320,12 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'photos.linkPlace': 'Połącz z miejscem',
|
||||
'photos.noPlace': 'Brak miejsca',
|
||||
'photos.uploadN': 'Prześlij {n} zdjęć',
|
||||
'photos.linkDay': 'Połącz dzień',
|
||||
'photos.noDay': 'Brak dnia',
|
||||
'photos.dayLabel': 'Dzień {number}',
|
||||
'photos.photoSelected': 'Zdjęcie wybrane',
|
||||
'photos.photosSelected': 'Zdjęcia wybrane',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · maks. 10 MB · do 30 zdjęć',
|
||||
|
||||
// Backup restore modal
|
||||
'backup.restoreConfirmTitle': 'Przywrócić kopię zapasową?',
|
||||
@@ -1321,6 +1352,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 +1614,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',
|
||||
@@ -1775,6 +1808,9 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.providerUsername': 'Nazwa użytkownika',
|
||||
'memories.providerPassword': 'Hasło',
|
||||
'memories.saveError': 'Nie udało się zapisać ustawień {provider_name}',
|
||||
'memories.saveRouteNotConfigured': 'Trasa zapisu nie jest skonfigurowana dla tego dostawcy',
|
||||
'memories.testRouteNotConfigured': 'Trasa testowa nie jest skonfigurowana dla tego dostawcy',
|
||||
'memories.fillRequiredFields': 'Proszę wypełnić wszystkie wymagane pola',
|
||||
'memories.selectAlbumMultiple': 'Wybierz album',
|
||||
'memories.selectPhotosMultiple': 'Wybierz zdjęcia',
|
||||
'journey.title': 'Dziennik podróży',
|
||||
@@ -1944,6 +1980,10 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.saveFailed': 'Zapisywanie nie powiodło się',
|
||||
'journey.settings.coverUpdated': 'Okładka zaktualizowana',
|
||||
'journey.settings.coverFailed': 'Przesyłanie nie powiodło się',
|
||||
'journey.settings.failedToDelete': 'Nie udało się usunąć',
|
||||
'journey.entries.deleteTitle': 'Usuń wpis',
|
||||
'journey.photosUploaded': '{count} zdjęć przesłanych',
|
||||
'journey.photosAdded': '{count} zdjęć dodanych',
|
||||
'journey.public.notFound': 'Nie znaleziono',
|
||||
'journey.public.notFoundMessage': 'Ten dziennik podróży nie istnieje lub link wygasł.',
|
||||
'journey.public.readOnly': 'Tylko do odczytu · Publiczny dziennik podróży',
|
||||
|
||||
@@ -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': 'Закрыть',
|
||||
@@ -27,6 +29,12 @@ const ru: Record<string, string> = {
|
||||
'common.password': 'Пароль',
|
||||
'common.saving': 'Сохранение...',
|
||||
'common.saved': 'Сохранено',
|
||||
'common.expand': 'Развернуть',
|
||||
'common.collapse': 'Свернуть',
|
||||
'trips.memberRemoved': '{username} удалён',
|
||||
'trips.memberRemoveError': 'Не удалось удалить',
|
||||
'trips.memberAdded': '{username} добавлен',
|
||||
'trips.memberAddError': 'Не удалось добавить',
|
||||
'trips.reminder': 'Напоминание',
|
||||
'trips.reminderNone': 'Нет',
|
||||
'trips.reminderDay': 'день',
|
||||
@@ -404,6 +412,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': 'Ошибка демо-входа',
|
||||
@@ -930,6 +942,7 @@ const ru: Record<string, string> = {
|
||||
'inspector.files': 'Файлы',
|
||||
'inspector.filesCount': '{count} файлов',
|
||||
'inspector.removeFromDay': 'Убрать из дня',
|
||||
'inspector.remove': 'Удалить',
|
||||
'inspector.addToDay': 'Добавить в день',
|
||||
'inspector.confirmedRes': 'Подтверждённое бронирование',
|
||||
'inspector.pendingRes': 'Ожидающее бронирование',
|
||||
@@ -1080,9 +1093,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} загружено',
|
||||
@@ -1172,7 +1189,6 @@ const ru: Record<string, string> = {
|
||||
'packing.saveAsTemplate': 'Сохранить как шаблон',
|
||||
'packing.templateName': 'Название шаблона',
|
||||
'packing.templateSaved': 'Список вещей сохранён как шаблон',
|
||||
'packing.assignUser': 'Назначить пользователя',
|
||||
'packing.noMembers': 'Нет участников',
|
||||
'packing.bags': 'Багаж',
|
||||
'packing.noBag': 'Не назначено',
|
||||
@@ -1329,6 +1345,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': 'Загрузите фото из путешествия',
|
||||
@@ -1336,6 +1359,12 @@ const ru: Record<string, string> = {
|
||||
'photos.linkPlace': 'Привязать место',
|
||||
'photos.noPlace': 'Без места',
|
||||
'photos.uploadN': '{n} фото загружено',
|
||||
'photos.linkDay': 'Связать день',
|
||||
'photos.noDay': 'Нет дня',
|
||||
'photos.dayLabel': 'День {number}',
|
||||
'photos.photoSelected': 'Фото выбрано',
|
||||
'photos.photosSelected': 'Фото выбраны',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · макс. 10 МБ · до 30 фото',
|
||||
|
||||
// Backup restore modal
|
||||
'backup.restoreConfirmTitle': 'Восстановить копию?',
|
||||
@@ -1362,6 +1391,7 @@ const ru: Record<string, string> = {
|
||||
'planner.routeCalculated': 'Маршрут рассчитан',
|
||||
'planner.routeCalcFailed': 'Не удалось рассчитать маршрут',
|
||||
'planner.routeError': 'Ошибка расчёта маршрута',
|
||||
'planner.icsExportFailed': 'Не удалось экспортировать ICS',
|
||||
'planner.routeOptimized': 'Маршрут оптимизирован',
|
||||
'planner.reservationUpdated': 'Бронирование обновлено',
|
||||
'planner.reservationAdded': 'Бронирование добавлено',
|
||||
@@ -1782,6 +1812,9 @@ const ru: Record<string, string> = {
|
||||
'memories.providerUsername': 'Имя пользователя',
|
||||
'memories.providerPassword': 'Пароль',
|
||||
'memories.saveError': 'Не удалось сохранить настройки {provider_name}',
|
||||
'memories.saveRouteNotConfigured': 'Маршрут сохранения не настроен для этого провайдера',
|
||||
'memories.testRouteNotConfigured': 'Маршрут тестирования не настроен для этого провайдера',
|
||||
'memories.fillRequiredFields': 'Пожалуйста, заполните все обязательные поля',
|
||||
'memories.selectAlbumMultiple': 'Выбрать альбом',
|
||||
'memories.selectPhotosMultiple': 'Выбрать фото',
|
||||
'journey.title': 'Путешествие',
|
||||
@@ -1951,6 +1984,10 @@ const ru: Record<string, string> = {
|
||||
'journey.settings.saveFailed': 'Не удалось сохранить',
|
||||
'journey.settings.coverUpdated': 'Обложка обновлена',
|
||||
'journey.settings.coverFailed': 'Загрузка не удалась',
|
||||
'journey.settings.failedToDelete': 'Не удалось удалить',
|
||||
'journey.entries.deleteTitle': 'Удалить запись',
|
||||
'journey.photosUploaded': '{count} фото загружено',
|
||||
'journey.photosAdded': '{count} фото добавлено',
|
||||
'journey.public.notFound': 'Не найдено',
|
||||
'journey.public.notFoundMessage': 'Это путешествие не существует или ссылка устарела.',
|
||||
'journey.public.readOnly': 'Только для чтения · Публичное путешествие',
|
||||
|
||||
@@ -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': '关闭',
|
||||
@@ -27,6 +29,12 @@ const zh: Record<string, string> = {
|
||||
'common.password': '密码',
|
||||
'common.saving': '保存中...',
|
||||
'common.saved': '已保存',
|
||||
'common.expand': '展开',
|
||||
'common.collapse': '折叠',
|
||||
'trips.memberRemoved': '{username} 已移除',
|
||||
'trips.memberRemoveError': '移除失败',
|
||||
'trips.memberAdded': '{username} 已添加',
|
||||
'trips.memberAddError': '添加失败',
|
||||
'trips.reminder': '提醒',
|
||||
'trips.reminderNone': '无',
|
||||
'trips.reminderDay': '天',
|
||||
@@ -404,6 +412,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': '演示登录失败',
|
||||
@@ -930,6 +942,7 @@ const zh: Record<string, string> = {
|
||||
'inspector.files': '文件',
|
||||
'inspector.filesCount': '{count} 个文件',
|
||||
'inspector.removeFromDay': '从当天移除',
|
||||
'inspector.remove': '删除',
|
||||
'inspector.addToDay': '添加到当天',
|
||||
'inspector.confirmedRes': '已确认预订',
|
||||
'inspector.pendingRes': '待确认预订',
|
||||
@@ -1080,9 +1093,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} 个',
|
||||
@@ -1172,7 +1189,6 @@ const zh: Record<string, string> = {
|
||||
'packing.saveAsTemplate': '保存为模板',
|
||||
'packing.templateName': '模板名称',
|
||||
'packing.templateSaved': '行李清单已保存为模板',
|
||||
'packing.assignUser': '分配用户',
|
||||
'packing.noMembers': '无成员',
|
||||
'packing.bags': '行李',
|
||||
'packing.noBag': '未分配',
|
||||
@@ -1329,6 +1345,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': '上传你的旅行照片',
|
||||
@@ -1336,6 +1359,12 @@ const zh: Record<string, string> = {
|
||||
'photos.linkPlace': '关联地点',
|
||||
'photos.noPlace': '无地点',
|
||||
'photos.uploadN': '上传 {n} 张照片',
|
||||
'photos.linkDay': '关联天数',
|
||||
'photos.noDay': '无天数',
|
||||
'photos.dayLabel': '第 {number} 天',
|
||||
'photos.photoSelected': '张照片已选择',
|
||||
'photos.photosSelected': '张照片已选择',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · 最大 10 MB · 最多 30 张照片',
|
||||
|
||||
// Backup restore modal
|
||||
'backup.restoreConfirmTitle': '恢复备份?',
|
||||
@@ -1362,6 +1391,7 @@ const zh: Record<string, string> = {
|
||||
'planner.routeCalculated': '路线已计算',
|
||||
'planner.routeCalcFailed': '无法计算路线',
|
||||
'planner.routeError': '路线计算错误',
|
||||
'planner.icsExportFailed': 'ICS 导出失败',
|
||||
'planner.routeOptimized': '路线已优化',
|
||||
'planner.reservationUpdated': '预订已更新',
|
||||
'planner.reservationAdded': '预订已添加',
|
||||
@@ -1782,6 +1812,9 @@ const zh: Record<string, string> = {
|
||||
'memories.providerUsername': '用户名',
|
||||
'memories.providerPassword': '密码',
|
||||
'memories.saveError': '无法保存 {provider_name} 设置',
|
||||
'memories.saveRouteNotConfigured': '此提供商未配置保存路由',
|
||||
'memories.testRouteNotConfigured': '此提供商未配置测试路由',
|
||||
'memories.fillRequiredFields': '请填写所有必填字段',
|
||||
'memories.selectAlbumMultiple': '选择相册',
|
||||
'memories.selectPhotosMultiple': '选择照片',
|
||||
'journey.title': '旅程',
|
||||
@@ -1951,6 +1984,10 @@ const zh: Record<string, string> = {
|
||||
'journey.settings.saveFailed': '保存失败',
|
||||
'journey.settings.coverUpdated': '封面已更新',
|
||||
'journey.settings.coverFailed': '上传失败',
|
||||
'journey.settings.failedToDelete': '删除失败',
|
||||
'journey.entries.deleteTitle': '删除条目',
|
||||
'journey.photosUploaded': '{count} 张照片已上传',
|
||||
'journey.photosAdded': '{count} 张照片已添加',
|
||||
'journey.public.notFound': '未找到',
|
||||
'journey.public.notFoundMessage': '此旅程不存在或链接已过期。',
|
||||
'journey.public.readOnly': '只读 · 公开旅程',
|
||||
|
||||
@@ -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': '關閉',
|
||||
@@ -27,6 +29,12 @@ const zhTw: Record<string, string> = {
|
||||
'common.password': '密碼',
|
||||
'common.saving': '儲存中...',
|
||||
'common.saved': '已儲存',
|
||||
'common.expand': '展開',
|
||||
'common.collapse': '折疊',
|
||||
'trips.memberRemoved': '{username} 已移除',
|
||||
'trips.memberRemoveError': '移除失敗',
|
||||
'trips.memberAdded': '{username} 已新增',
|
||||
'trips.memberAddError': '新增失敗',
|
||||
'trips.reminder': '提醒',
|
||||
'trips.reminderNone': '無',
|
||||
'trips.reminderDay': '天',
|
||||
@@ -125,6 +133,8 @@ const zhTw: Record<string, string> = {
|
||||
'dashboard.coverRemoveError': '移除失敗',
|
||||
'dashboard.titleRequired': '標題為必填項',
|
||||
'dashboard.endDateError': '結束日期必須晚於開始日期',
|
||||
'dashboard.dayCount': '天數',
|
||||
'dashboard.dayCountHint': '未設定旅行日期時的規劃天數。',
|
||||
|
||||
// Settings
|
||||
'settings.title': '設定',
|
||||
@@ -428,6 +438,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': '演示登入失敗',
|
||||
@@ -955,6 +969,7 @@ const zhTw: Record<string, string> = {
|
||||
'inspector.files': '檔案',
|
||||
'inspector.filesCount': '{count} 個檔案',
|
||||
'inspector.removeFromDay': '從當天移除',
|
||||
'inspector.remove': '刪除',
|
||||
'inspector.addToDay': '新增到當天',
|
||||
'inspector.confirmedRes': '已確認預訂',
|
||||
'inspector.pendingRes': '待確認預訂',
|
||||
@@ -1108,6 +1123,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} 個',
|
||||
@@ -1186,7 +1204,6 @@ const zhTw: Record<string, string> = {
|
||||
'packing.menuCheckAll': '全部勾選',
|
||||
'packing.menuUncheckAll': '取消全部勾選',
|
||||
'packing.menuDeleteCat': '刪除分類',
|
||||
'packing.assignUser': '指派使用者',
|
||||
'packing.addItem': '新增物品',
|
||||
'packing.addItemPlaceholder': '物品名稱...',
|
||||
'packing.addCategory': '新增分類',
|
||||
@@ -1354,6 +1371,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': '上傳你的旅行照片',
|
||||
@@ -1361,6 +1385,12 @@ const zhTw: Record<string, string> = {
|
||||
'photos.linkPlace': '關聯地點',
|
||||
'photos.noPlace': '無地點',
|
||||
'photos.uploadN': '上傳 {n} 張照片',
|
||||
'photos.linkDay': '關聯天數',
|
||||
'photos.noDay': '無天數',
|
||||
'photos.dayLabel': '第 {number} 天',
|
||||
'photos.photoSelected': '張照片已選擇',
|
||||
'photos.photosSelected': '張照片已選擇',
|
||||
'photos.fileTypeHint': 'JPG, PNG, WebP · 最大 10 MB · 最多 30 張照片',
|
||||
|
||||
// Backup restore modal
|
||||
'backup.restoreConfirmTitle': '恢復備份?',
|
||||
@@ -1387,6 +1417,7 @@ const zhTw: Record<string, string> = {
|
||||
'planner.routeCalculated': '路線已計算',
|
||||
'planner.routeCalcFailed': '無法計算路線',
|
||||
'planner.routeError': '路線計算錯誤',
|
||||
'planner.icsExportFailed': 'ICS 匯出失敗',
|
||||
'planner.routeOptimized': '路線已最佳化',
|
||||
'planner.reservationUpdated': '預訂已更新',
|
||||
'planner.reservationAdded': '預訂已新增',
|
||||
@@ -1742,6 +1773,9 @@ const zhTw: Record<string, string> = {
|
||||
'memories.providerUsername': '使用者名稱',
|
||||
'memories.providerPassword': '密碼',
|
||||
'memories.saveError': '無法儲存 {provider_name} 設定',
|
||||
'memories.saveRouteNotConfigured': '此提供商未設定儲存路由',
|
||||
'memories.testRouteNotConfigured': '此提供商未設定測試路由',
|
||||
'memories.fillRequiredFields': '請填寫所有必填欄位',
|
||||
'memories.selectAlbumMultiple': '選擇相簿',
|
||||
'memories.selectPhotosMultiple': '選擇照片',
|
||||
'journey.title': '旅程',
|
||||
@@ -1911,6 +1945,10 @@ const zhTw: Record<string, string> = {
|
||||
'journey.settings.saveFailed': '儲存失敗',
|
||||
'journey.settings.coverUpdated': '封面已更新',
|
||||
'journey.settings.coverFailed': '上傳失敗',
|
||||
'journey.settings.failedToDelete': '刪除失敗',
|
||||
'journey.entries.deleteTitle': '刪除條目',
|
||||
'journey.photosUploaded': '{count} 張照片已上傳',
|
||||
'journey.photosAdded': '{count} 張照片已新增',
|
||||
'journey.public.notFound': '未找到',
|
||||
'journey.public.notFoundMessage': '此旅程不存在或連結已過期。',
|
||||
'journey.public.readOnly': '唯讀 · 公開旅程',
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('FilesPage', () => {
|
||||
expect(screen.getByTestId('file-manager')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText(/2 Dateien/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/2 files for/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,7 +205,7 @@ describe('FilesPage', () => {
|
||||
expect(screen.getByTestId('file-manager')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByRole('heading', { name: /Dateien & Dokumente/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /Files & Documents/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -563,9 +563,9 @@ export default function JourneyDetailPage() {
|
||||
setDeleteTarget(null)
|
||||
loadJourney(Number(id))
|
||||
}}
|
||||
title="Delete Entry"
|
||||
message={`Delete "${deleteTarget?.title || 'this entry'}"? This cannot be undone.`}
|
||||
confirmLabel="Delete"
|
||||
title={t('journey.entries.deleteTitle')}
|
||||
message={t('journey.deleteConfirmMessage', { title: deleteTarget?.title || 'this entry' })}
|
||||
confirmLabel={t('common.delete')}
|
||||
danger
|
||||
/>
|
||||
|
||||
@@ -584,9 +584,9 @@ export default function JourneyDetailPage() {
|
||||
toast.error(t('journey.trips.unlinkFailed'))
|
||||
}
|
||||
}}
|
||||
title="Unlink Trip"
|
||||
message={`Unlink "${unlinkTrip?.title}"? All synced entries and photos from this trip will be permanently deleted. This cannot be undone.`}
|
||||
confirmLabel="Unlink"
|
||||
title={t('journey.trips.unlinkTrip')}
|
||||
message={t('journey.trips.unlinkMessage', { title: unlinkTrip?.title })}
|
||||
confirmLabel={t('journey.trips.unlink')}
|
||||
danger
|
||||
/>
|
||||
|
||||
@@ -811,7 +811,7 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres
|
||||
for (const f of files) formData.append('photos', f)
|
||||
try {
|
||||
await journeyApi.uploadPhotos(entryId, formData)
|
||||
toast.success(`${files.length} photos uploaded`)
|
||||
toast.success(t('journey.photosUploaded', { count: files.length }))
|
||||
onRefresh()
|
||||
} catch {
|
||||
toast.error(t('journey.settings.coverFailed'))
|
||||
@@ -938,7 +938,7 @@ function GalleryView({ entries, journeyId, userId, trips, onPhotoClick, onRefres
|
||||
} catch {}
|
||||
}
|
||||
if (added > 0) {
|
||||
toast.success(`${added} photos added`)
|
||||
toast.success(t('journey.photosAdded', { count: added }))
|
||||
onRefresh()
|
||||
}
|
||||
setShowPicker(false)
|
||||
@@ -1179,8 +1179,8 @@ function EntryCard({ entry, onEdit, onDelete, onPhotoClick }: {
|
||||
<>
|
||||
<div className="fixed inset-0 z-[99]" onClick={() => setMenuOpen(false)} />
|
||||
<div className="fixed z-[100] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-lg py-1 min-w-[120px]" style={{ top: (menuBtnRef.current?.getBoundingClientRect().bottom || 0) + 4, right: window.innerWidth - (menuBtnRef.current?.getBoundingClientRect().right || 0) }}>
|
||||
<button onClick={() => { setMenuOpen(false); onEdit() }} className="w-full text-left px-3 py-1.5 text-[12px] text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700 flex items-center gap-2"><Pencil size={12} /> Edit</button>
|
||||
<button onClick={() => { setMenuOpen(false); onDelete() }} className="w-full text-left px-3 py-1.5 text-[12px] text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2"><Trash2 size={12} /> Delete</button>
|
||||
<button onClick={() => { setMenuOpen(false); onEdit() }} className="w-full text-left px-3 py-1.5 text-[12px] text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700 flex items-center gap-2"><Pencil size={12} /> {t('common.edit')}</button>
|
||||
<button onClick={() => { setMenuOpen(false); onDelete() }} className="w-full text-left px-3 py-1.5 text-[12px] text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2"><Trash2 size={12} /> {t('common.delete')}</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -2177,7 +2177,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
|
||||
<button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">{t('common.cancel')}</button>
|
||||
<button onClick={handleSave} disabled={saving} className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-50">
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
{saving ? t('common.saving') : t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2546,7 +2546,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
|
||||
await updateJourney(journey.id, { title, subtitle: subtitle || null })
|
||||
onSaved()
|
||||
} catch {
|
||||
toast.error('Failed to save')
|
||||
toast.error(t('journey.settings.saveFailed'))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -2559,10 +2559,10 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
|
||||
formData.append('cover', file)
|
||||
try {
|
||||
await journeyApi.uploadCover(journey.id, formData)
|
||||
toast.success('Cover updated')
|
||||
toast.success(t('journey.settings.coverUpdated'))
|
||||
onSaved()
|
||||
} catch {
|
||||
toast.error('Upload failed')
|
||||
toast.error(t('journey.settings.coverFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2573,7 +2573,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
|
||||
await deleteJourney(journey.id)
|
||||
navigate('/journey')
|
||||
} catch {
|
||||
toast.error('Failed to delete')
|
||||
toast.error(t('journey.settings.failedToDelete'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2633,14 +2633,14 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
|
||||
|
||||
{/* Synced Trips */}
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">Synced Trips</label>
|
||||
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.detail.syncedTrips')}</label>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{journey.trips.map((trip: any) => (
|
||||
<div key={trip.trip_id} className="flex items-center gap-2.5 p-2 rounded-lg bg-zinc-50 dark:bg-zinc-800">
|
||||
<div className="w-8 h-8 rounded-md flex-shrink-0" style={{ background: pickGradient(trip.trip_id) }} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[12px] font-medium text-zinc-900 dark:text-white">{trip.title}</div>
|
||||
<div className="text-[10px] text-zinc-500">{trip.place_count || 0} places</div>
|
||||
<div className="text-[10px] text-zinc-500">{trip.place_count || 0} {t('journey.synced.places')}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setUnlinkTarget({ trip_id: trip.trip_id, title: trip.title })}
|
||||
@@ -2651,19 +2651,19 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{journey.trips.length === 0 && <p className="text-[11px] text-zinc-400">No trips linked</p>}
|
||||
{journey.trips.length === 0 && <p className="text-[11px] text-zinc-400">{t('journey.trips.noTripsLinkedSettings')}</p>}
|
||||
<button
|
||||
onClick={() => setShowAddTrip(true)}
|
||||
className="w-full mt-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-600 text-[12px] font-medium text-zinc-500 hover:border-zinc-400 hover:text-zinc-700 dark:hover:border-zinc-500 dark:hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
<Plus size={14} /> Add Trip
|
||||
<Plus size={14} /> {t('journey.trips.addTrip')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contributors */}
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">Contributors</label>
|
||||
<label className="text-[10px] font-semibold tracking-[0.12em] uppercase text-zinc-500 block mb-2">{t('journey.detail.contributors')}</label>
|
||||
<div className="flex flex-col gap-2">
|
||||
{journey.contributors.map((c: any) => (
|
||||
<div key={c.user_id} className="flex items-center gap-2.5">
|
||||
@@ -2678,7 +2678,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
|
||||
onClick={onOpenInvite}
|
||||
className="w-full mt-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-600 text-[12px] font-medium text-zinc-500 hover:border-zinc-400 hover:text-zinc-700 dark:hover:border-zinc-500 dark:hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
<UserPlus size={14} /> Invite Contributor
|
||||
<UserPlus size={14} /> {t('journey.contributors.invite')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2697,11 +2697,11 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
|
||||
className="flex items-center gap-1.5 text-[12px] font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg px-2.5 py-2 mr-auto"
|
||||
>
|
||||
<Trash2 size={13} />
|
||||
Delete
|
||||
{t('journey.settings.delete')}
|
||||
</button>
|
||||
<button onClick={onClose} className="px-3.5 py-2 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">{t('common.cancel')}</button>
|
||||
<button onClick={handleSave} disabled={saving || !title.trim()} className="px-3.5 py-2 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-40">
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
{saving ? t('common.saving') : t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2714,16 +2714,16 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
|
||||
if (!unlinkTarget) return
|
||||
try {
|
||||
await journeyApi.removeTrip(journey.id, unlinkTarget.trip_id)
|
||||
toast.success('Trip unlinked')
|
||||
toast.success(t('journey.trips.tripUnlinked'))
|
||||
setUnlinkTarget(null)
|
||||
onSaved()
|
||||
} catch {
|
||||
toast.error('Failed to unlink trip')
|
||||
toast.error(t('journey.trips.unlinkFailed'))
|
||||
}
|
||||
}}
|
||||
title="Unlink Trip"
|
||||
message={`Unlink "${unlinkTarget?.title}"? All synced entries and photos from this trip will be permanently deleted. This cannot be undone.`}
|
||||
confirmLabel="Unlink"
|
||||
title={t('journey.trips.unlinkTrip')}
|
||||
message={t('journey.trips.unlinkMessage', { title: unlinkTarget?.title })}
|
||||
confirmLabel={t('journey.trips.unlink')}
|
||||
danger
|
||||
/>
|
||||
|
||||
@@ -2741,9 +2741,9 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
|
||||
isOpen={showDeleteConfirm}
|
||||
onClose={() => setShowDeleteConfirm(false)}
|
||||
onConfirm={handleDelete}
|
||||
title="Delete Journey"
|
||||
message={`Delete "${journey.title}"? All entries and photos will be lost.`}
|
||||
confirmLabel="Delete"
|
||||
title={t('journey.settings.deleteJourney')}
|
||||
message={t('journey.settings.deleteMessage', { title: journey.title })}
|
||||
confirmLabel={t('common.delete')}
|
||||
danger
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('PhotosPage', () => {
|
||||
expect(screen.getByTestId('photo-gallery')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText(/1 Fotos/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/1 photos for/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,7 +224,7 @@ describe('PhotosPage', () => {
|
||||
expect(screen.getByTestId('photo-gallery')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByRole('heading', { name: /fotos/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /photos/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user