diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 49894e51..156da726 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -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> = { + 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) } ) diff --git a/client/src/components/Files/FileManager.tsx b/client/src/components/Files/FileManager.tsx index 4295c46a..b4b4e200 100644 --- a/client/src/components/Files/FileManager.tsx +++ b/client/src/components/Files/FileManager.tsx @@ -778,7 +778,7 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate, title={previewFile.original_name} >

- +

diff --git a/client/src/components/Photos/PhotoLightbox.tsx b/client/src/components/Photos/PhotoLightbox.tsx index cbd483b5..ba6a5738 100644 --- a/client/src/components/Photos/PhotoLightbox.tsx +++ b/client/src/components/Photos/PhotoLightbox.tsx @@ -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 || Beschriftung hinzufügen...} + {photo.caption || {t('photos.addCaption')}}

@@ -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 ?
diff --git a/client/src/components/Todo/TodoListPanel.tsx b/client/src/components/Todo/TodoListPanel.tsx index 10c2d392..9f8a396f 100644 --- a/client/src/components/Todo/TodoListPanel.tsx +++ b/client/src/components/Todo/TodoListPanel.tsx @@ -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) } diff --git a/client/src/components/Trips/TripFormModal.tsx b/client/src/components/Trips/TripFormModal.tsx index 4b2865ce..47f1184a 100644 --- a/client/src/components/Trips/TripFormModal.tsx +++ b/client/src/components/Trips/TripFormModal.tsx @@ -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)]) diff --git a/client/src/hooks/useDayNotes.ts b/client/src/hooks/useDayNotes.ts index 296c197a..ef03154b 100644 --- a/client/src/hooks/useDayNotes.ts +++ b/client/src/hooks/useDayNotes.ts @@ -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(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 } diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 3dfd170c..cff3eb41 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -12,6 +12,8 @@ const ar: Record = { 'common.loading': 'جارٍ التحميل...', 'common.import': 'استيراد', 'common.error': 'خطأ', + 'common.unknownError': 'خطأ غير معروف', + 'common.tooManyAttempts': 'محاولات كثيرة جدًا. يرجى المحاولة لاحقًا.', 'common.back': 'رجوع', 'common.all': 'الكل', 'common.close': 'إغلاق', @@ -31,6 +33,12 @@ const ar: Record = { '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 = { '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 = { 'inspector.files': 'الملفات', 'inspector.filesCount': '{count} ملفات', 'inspector.removeFromDay': 'إزالة من اليوم', + 'inspector.remove': 'إزالة', 'inspector.addToDay': 'إضافة إلى اليوم', 'inspector.confirmedRes': 'حجز مؤكد', 'inspector.pendingRes': 'حجز قيد الانتظار', @@ -1084,9 +1097,13 @@ const ar: Record = { '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 = { 'packing.menuCheckAll': 'تحديد الكل', 'packing.menuUncheckAll': 'إلغاء تحديد الكل', 'packing.menuDeleteCat': 'حذف الفئة', - 'packing.assignUser': 'تعيين مستخدم', 'packing.noMembers': 'لا أعضاء', 'packing.addItem': 'إضافة عنصر', 'packing.addItemPlaceholder': 'اسم العنصر...', @@ -1333,6 +1349,13 @@ const ar: Record = { '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 = { '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 = { 'planner.routeCalculated': 'تم حساب المسار', 'planner.routeCalcFailed': 'تعذر حساب المسار', 'planner.routeError': 'خطأ أثناء حساب المسار', + 'planner.icsExportFailed': 'فشل تصدير ICS', 'planner.routeOptimized': 'تم تحسين المسار', 'planner.reservationUpdated': 'تم تحديث الحجز', 'planner.reservationAdded': 'تمت إضافة الحجز', @@ -1478,6 +1508,9 @@ const ar: Record = { '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 = { 'memories.confirmShareTitle': 'مشاركة مع أعضاء الرحلة؟', 'memories.confirmShareHint': '{count} صور ستكون مرئية لجميع أعضاء هذه الرحلة. يمكنك جعل الصور الفردية خاصة لاحقًا.', 'memories.confirmShareButton': 'مشاركة الصور', + 'journey.settings.failedToDelete': 'فشل في الحذف', + 'journey.entries.deleteTitle': 'حذف الإدخال', + 'journey.photosUploaded': 'تم رفع {count} صورة', + 'journey.photosAdded': 'تمت إضافة {count} صورة', // Collab Addon 'collab.tabs.chat': 'الدردشة', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index e9675d25..437096d2 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -8,6 +8,8 @@ const br: Record = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 2b8dbb78..87b35787 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -8,6 +8,8 @@ const cs: Record = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 2f93e9e7..f2aaffb6 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -8,6 +8,8 @@ const de: Record = { '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 = { '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 = { '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 = { '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 = { '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 = { // 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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index da78e7c4..13593e11 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -8,6 +8,8 @@ const en: Record = { '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 = { '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 = { '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 = { '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 = { '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 = { // 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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 4200c973..1b68816f 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -8,6 +8,8 @@ const es: Record = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index 001034ef..d424831d 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -8,6 +8,8 @@ const fr: Record = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index bc92c8b7..759ef703 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -8,6 +8,8 @@ const hu: Record = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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ó', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index 756a8a57..dabaf43a 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -8,6 +8,8 @@ const it: Record = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 4353ee60..3f077d72 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -8,6 +8,8 @@ const nl: Record = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index 5bb8d49a..8d96eb66 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -7,6 +7,8 @@ const pl: Record = { '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 = { '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 = { '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 = { '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 = { // 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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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 = { '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', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 56d98696..8a88c721 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -8,6 +8,8 @@ const ru: Record = { 'common.loading': 'Загрузка...', 'common.import': 'Импорт', 'common.error': 'Ошибка', + 'common.unknownError': 'Неизвестная ошибка', + 'common.tooManyAttempts': 'Слишком много попыток. Попробуйте позже.', 'common.back': 'Назад', 'common.all': 'Все', 'common.close': 'Закрыть', @@ -27,6 +29,12 @@ const ru: Record = { '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 = { '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 = { 'inspector.files': 'Файлы', 'inspector.filesCount': '{count} файлов', 'inspector.removeFromDay': 'Убрать из дня', + 'inspector.remove': 'Удалить', 'inspector.addToDay': 'Добавить в день', 'inspector.confirmedRes': 'Подтверждённое бронирование', 'inspector.pendingRes': 'Ожидающее бронирование', @@ -1080,9 +1093,13 @@ const ru: Record = { '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 = { 'packing.saveAsTemplate': 'Сохранить как шаблон', 'packing.templateName': 'Название шаблона', 'packing.templateSaved': 'Список вещей сохранён как шаблон', - 'packing.assignUser': 'Назначить пользователя', 'packing.noMembers': 'Нет участников', 'packing.bags': 'Багаж', 'packing.noBag': 'Не назначено', @@ -1329,6 +1345,13 @@ const ru: Record = { '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 = { '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 = { 'planner.routeCalculated': 'Маршрут рассчитан', 'planner.routeCalcFailed': 'Не удалось рассчитать маршрут', 'planner.routeError': 'Ошибка расчёта маршрута', + 'planner.icsExportFailed': 'Не удалось экспортировать ICS', 'planner.routeOptimized': 'Маршрут оптимизирован', 'planner.reservationUpdated': 'Бронирование обновлено', 'planner.reservationAdded': 'Бронирование добавлено', @@ -1782,6 +1812,9 @@ const ru: Record = { '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 = { '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': 'Только для чтения · Публичное путешествие', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index f4ad849f..93ad9772 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -8,6 +8,8 @@ const zh: Record = { 'common.loading': '加载中...', 'common.import': '导入', 'common.error': '错误', + 'common.unknownError': '未知错误', + 'common.tooManyAttempts': '尝试次数过多,请稍后再试。', 'common.back': '返回', 'common.all': '全部', 'common.close': '关闭', @@ -27,6 +29,12 @@ const zh: Record = { '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 = { '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 = { 'inspector.files': '文件', 'inspector.filesCount': '{count} 个文件', 'inspector.removeFromDay': '从当天移除', + 'inspector.remove': '删除', 'inspector.addToDay': '添加到当天', 'inspector.confirmedRes': '已确认预订', 'inspector.pendingRes': '待确认预订', @@ -1080,9 +1093,13 @@ const zh: Record = { '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 = { 'packing.saveAsTemplate': '保存为模板', 'packing.templateName': '模板名称', 'packing.templateSaved': '行李清单已保存为模板', - 'packing.assignUser': '分配用户', 'packing.noMembers': '无成员', 'packing.bags': '行李', 'packing.noBag': '未分配', @@ -1329,6 +1345,13 @@ const zh: Record = { '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 = { '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 = { 'planner.routeCalculated': '路线已计算', 'planner.routeCalcFailed': '无法计算路线', 'planner.routeError': '路线计算错误', + 'planner.icsExportFailed': 'ICS 导出失败', 'planner.routeOptimized': '路线已优化', 'planner.reservationUpdated': '预订已更新', 'planner.reservationAdded': '预订已添加', @@ -1782,6 +1812,9 @@ const zh: Record = { '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 = { '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': '只读 · 公开旅程', diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index 9f901897..49e38eba 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -8,6 +8,8 @@ const zhTw: Record = { 'common.loading': '載入中...', 'common.import': '匯入', 'common.error': '錯誤', + 'common.unknownError': '未知錯誤', + 'common.tooManyAttempts': '嘗試次數過多,請稍後再試。', 'common.back': '返回', 'common.all': '全部', 'common.close': '關閉', @@ -27,6 +29,12 @@ const zhTw: Record = { '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 = { 'dashboard.coverRemoveError': '移除失敗', 'dashboard.titleRequired': '標題為必填項', 'dashboard.endDateError': '結束日期必須晚於開始日期', + 'dashboard.dayCount': '天數', + 'dashboard.dayCountHint': '未設定旅行日期時的規劃天數。', // Settings 'settings.title': '設定', @@ -428,6 +438,10 @@ const zhTw: Record = { '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 = { 'inspector.files': '檔案', 'inspector.filesCount': '{count} 個檔案', 'inspector.removeFromDay': '從當天移除', + 'inspector.remove': '刪除', 'inspector.addToDay': '新增到當天', 'inspector.confirmedRes': '已確認預訂', 'inspector.pendingRes': '待確認預訂', @@ -1108,6 +1123,9 @@ const zhTw: Record = { // 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 = { 'packing.menuCheckAll': '全部勾選', 'packing.menuUncheckAll': '取消全部勾選', 'packing.menuDeleteCat': '刪除分類', - 'packing.assignUser': '指派使用者', 'packing.addItem': '新增物品', 'packing.addItemPlaceholder': '物品名稱...', 'packing.addCategory': '新增分類', @@ -1354,6 +1371,13 @@ const zhTw: Record = { '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 = { '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 = { 'planner.routeCalculated': '路線已計算', 'planner.routeCalcFailed': '無法計算路線', 'planner.routeError': '路線計算錯誤', + 'planner.icsExportFailed': 'ICS 匯出失敗', 'planner.routeOptimized': '路線已最佳化', 'planner.reservationUpdated': '預訂已更新', 'planner.reservationAdded': '預訂已新增', @@ -1742,6 +1773,9 @@ const zhTw: Record = { '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 = { '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': '唯讀 · 公開旅程', diff --git a/client/src/pages/FilesPage.test.tsx b/client/src/pages/FilesPage.test.tsx index 8455b41f..22298b8c 100644 --- a/client/src/pages/FilesPage.test.tsx +++ b/client/src/pages/FilesPage.test.tsx @@ -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(); }); }); }); diff --git a/client/src/pages/FilesPage.tsx b/client/src/pages/FilesPage.tsx index 8f5152d7..2a6c4eac 100644 --- a/client/src/pages/FilesPage.tsx +++ b/client/src/pages/FilesPage.tsx @@ -78,8 +78,8 @@ export default function FilesPage(): React.ReactElement {
-

Dateien & Dokumente

-

{files.length} Dateien für {trip?.name}

+

{t('files.pageTitle')}

+

{t('files.subtitle', { count: files.length, trip: trip?.name })}

diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx index ac17ee4e..f038ad0a 100644 --- a/client/src/pages/JourneyDetailPage.tsx +++ b/client/src/pages/JourneyDetailPage.tsx @@ -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 }: { <>
setMenuOpen(false)} />
- - + +
)} @@ -2177,7 +2177,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
@@ -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 */}
- +
{journey.trips.map((trip: any) => (
{trip.title}
-
{trip.place_count || 0} places
+
{trip.place_count || 0} {t('journey.synced.places')}
))} - {journey.trips.length === 0 &&

No trips linked

} + {journey.trips.length === 0 &&

{t('journey.trips.noTripsLinkedSettings')}

}
{/* Contributors */}
- +
{journey.contributors.map((c: any) => (
@@ -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" > - Invite Contributor + {t('journey.contributors.invite')}
@@ -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" > - Delete + {t('journey.settings.delete')}
@@ -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 />
diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index 56719fa1..ac5fb4f3 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -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) diff --git a/client/src/pages/PhotosPage.test.tsx b/client/src/pages/PhotosPage.test.tsx index 49d05bc9..cf205337 100644 --- a/client/src/pages/PhotosPage.test.tsx +++ b/client/src/pages/PhotosPage.test.tsx @@ -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(); }); }); }); diff --git a/client/src/pages/PhotosPage.tsx b/client/src/pages/PhotosPage.tsx index 759b1820..4d99c472 100644 --- a/client/src/pages/PhotosPage.tsx +++ b/client/src/pages/PhotosPage.tsx @@ -89,8 +89,8 @@ export default function PhotosPage(): React.ReactElement {
-

Fotos

-

{photos.length} Fotos für {trip?.name}

+

{t('photos.title')}

+

{t('photos.subtitle', { count: photos.length, trip: trip?.name })}

diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx index 1002531e..e519243e 100644 --- a/client/src/pages/TripPlannerPage.tsx +++ b/client/src/pages/TripPlannerPage.tsx @@ -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} />