From 577f2b05ca82a62d59a9c2a8af67758a370a7977 Mon Sep 17 00:00:00 2001 From: xenocent Date: Sat, 11 Apr 2026 15:26:16 +0700 Subject: [PATCH] feat(i18n): add Indonesian translation --- client/src/components/Layout/DemoBanner.tsx | 32 + client/src/i18n/TranslationContext.tsx | 8 +- client/src/i18n/translations/id.ts | 1703 +++++++++++++++++++ server/src/services/notifications.ts | 11 + 4 files changed, 1751 insertions(+), 3 deletions(-) create mode 100644 client/src/i18n/translations/id.ts diff --git a/client/src/components/Layout/DemoBanner.tsx b/client/src/components/Layout/DemoBanner.tsx index 1bbc53c1..ba77cd40 100644 --- a/client/src/components/Layout/DemoBanner.tsx +++ b/client/src/components/Layout/DemoBanner.tsx @@ -214,6 +214,38 @@ const texts: Record = { selfHostLink: 'استضفه بنفسك', close: 'فهمت', }, + id: { + titleBefore: 'Selamat datang di ', + titleAfter: '', + title: 'Selamat datang di Demo TREK', + description: 'Anda dapat melihat, mengedit, dan membuat perjalanan. Semua perubahan akan diatur ulang secara otomatis setiap jam.', + resetIn: 'Atur ulang berikutnya dalam', + minutes: 'menit', + uploadNote: 'Unggah file (foto, dokumen, sampul) dinonaktifkan dalam mode demo.', + fullVersionTitle: 'Selain itu dalam versi lengkap:', + features: [ + 'Unggah file (foto, dokumen, sampul)', + 'Manajemen kunci API (Google Maps, Cuaca)', + 'Manajemen pengguna & izin', + 'Pencadangan otomatis', + 'Manajemen Addon (aktifkan/nonaktifkan)', + 'OIDC / SSO single sign-on', + ], + addonsTitle: 'Addon Modular (dapat dinonaktifkan di versi lengkap)', + addons: [ + ['Vacay', 'Perencana liburan dengan kalender, hari libur & penggabungan pengguna'], + ['Atlas', 'Peta dunia dengan negara yang dikunjungi & statistik perjalanan'], + ['Pengepakan', 'Daftar periksa per perjalanan'], + ['Anggaran', 'Pelacakan pengeluaran dengan pemisahan tagihan'], + ['Dokumen', 'Lampirkan file ke perjalanan'], + ['Widget', 'Konverter mata uang & zona waktu'], + ], + whatIs: 'Apa itu TREK?', + whatIsDesc: 'Perencana perjalanan yang di-host sendiri dengan kolaborasi real-time, peta interaktif, login OIDC, dan mode gelap.', + selfHost: 'Buka sumber — ', + selfHostLink: 'host mandiri', + close: 'Mengerti', + }, } const featureIcons = [Upload, Key, Users, Database, Puzzle, Shield] diff --git a/client/src/i18n/TranslationContext.tsx b/client/src/i18n/TranslationContext.tsx index a8a595a9..e9a990a8 100644 --- a/client/src/i18n/TranslationContext.tsx +++ b/client/src/i18n/TranslationContext.tsx @@ -10,6 +10,7 @@ import ru from './translations/ru' import zh from './translations/zh' import zhTw from './translations/zhTw' import nl from './translations/nl' +import id from './translations/id' import ar from './translations/ar' import br from './translations/br' import cs from './translations/cs' @@ -32,10 +33,11 @@ export const SUPPORTED_LANGUAGES = [ { value: 'zh-TW', label: '繁體中文' }, { value: 'it', label: 'Italiano' }, { value: 'ar', label: 'العربية' }, + { value: 'id', label: 'Bahasa Indonesia' }, ] as const -const translations: Record = { de, en, es, fr, hu, it, ru, zh, 'zh-TW': zhTw, nl, ar, br, cs, pl } -const LOCALES: Record = { de: 'de-DE', en: 'en-US', es: 'es-ES', fr: 'fr-FR', hu: 'hu-HU', it: 'it-IT', ru: 'ru-RU', zh: 'zh-CN', 'zh-TW': 'zh-TW', nl: 'nl-NL', ar: 'ar-SA', br: 'pt-BR', cs: 'cs-CZ', pl: 'pl-PL' } +const translations: Record = { de, en, es, fr, hu, it, ru, zh, 'zh-TW': zhTw, nl, id, ar, br, cs, pl } +const LOCALES: Record = { de: 'de-DE', en: 'en-US', es: 'es-ES', fr: 'fr-FR', hu: 'hu-HU', it: 'it-IT', ru: 'ru-RU', zh: 'zh-CN', 'zh-TW': 'zh-TW', nl: 'nl-NL', id: 'id-ID', ar: 'ar-SA', br: 'pt-BR', cs: 'cs-CZ', pl: 'pl-PL' } const RTL_LANGUAGES = new Set(['ar']) export function getLocaleForLanguage(language: string): string { @@ -44,7 +46,7 @@ export function getLocaleForLanguage(language: string): string { export function getIntlLanguage(language: string): string { if (language === 'br') return 'pt-BR' - return ['de', 'es', 'fr', 'hu', 'it', 'ru', 'zh', 'zh-TW', 'nl', 'ar', 'cs', 'pl'].includes(language) ? language : 'en' + return ['de', 'es', 'fr', 'hu', 'it', 'ru', 'zh', 'zh-TW', 'nl', 'ar', 'cs', 'pl', 'id'].includes(language) ? language : 'en' } export function isRtlLanguage(language: string): boolean { diff --git a/client/src/i18n/translations/id.ts b/client/src/i18n/translations/id.ts new file mode 100644 index 00000000..2cb895a1 --- /dev/null +++ b/client/src/i18n/translations/id.ts @@ -0,0 +1,1703 @@ +const en: Record = { + // Common + 'common.save': 'Save', + 'common.cancel': 'Cancel', + 'common.delete': 'Delete', + 'common.edit': 'Edit', + 'common.add': 'Add', + 'common.loading': 'Loading...', + 'common.import': 'Import', + 'common.error': 'Error', + 'common.back': 'Back', + 'common.all': 'All', + 'common.close': 'Close', + 'common.open': 'Open', + 'common.upload': 'Upload', + 'common.search': 'Search', + 'common.confirm': 'Confirm', + 'common.ok': 'OK', + 'common.yes': 'Yes', + 'common.no': 'No', + 'common.or': 'or', + 'common.none': 'None', + 'common.date': 'Date', + 'common.rename': 'Rename', + 'common.name': 'Name', + 'common.email': 'Email', + 'common.password': 'Password', + 'common.saving': 'Saving...', + 'common.saved': 'Saved', + 'trips.reminder': 'Reminder', + 'trips.reminderNone': 'None', + 'trips.reminderDay': 'day', + 'trips.reminderDays': 'days', + 'trips.reminderCustom': 'Custom', + 'trips.reminderDaysBefore': 'days before departure', + 'trips.reminderDisabledHint': 'Trip reminders are disabled. Enable them in Admin > Settings > Notifications.', + 'common.update': 'Update', + 'common.change': 'Change', + 'common.uploading': 'Uploading…', + 'common.backToPlanning': 'Back to Planning', + 'common.reset': 'Reset', + + // Navbar + 'nav.trip': 'Trip', + 'nav.share': 'Share', + 'nav.settings': 'Settings', + 'nav.admin': 'Admin', + 'nav.logout': 'Log out', + 'nav.lightMode': 'Light Mode', + 'nav.darkMode': 'Dark Mode', + 'nav.autoMode': 'Auto Mode', + 'nav.administrator': 'Administrator', + + // Dashboard + 'dashboard.title': 'My Trips', + 'dashboard.subtitle.loading': 'Loading trips...', + 'dashboard.subtitle.trips': '{count} trips ({archived} archived)', + 'dashboard.subtitle.empty': 'Start your first trip', + 'dashboard.subtitle.activeOne': '{count} active trip', + 'dashboard.subtitle.activeMany': '{count} active trips', + 'dashboard.subtitle.archivedSuffix': ' · {count} archived', + 'dashboard.newTrip': 'New Trip', + 'dashboard.gridView': 'Grid view', + 'dashboard.listView': 'List view', + 'dashboard.currency': 'Currency', + 'dashboard.timezone': 'Timezones', + 'dashboard.localTime': 'Local', + 'dashboard.timezoneCustomTitle': 'Custom Timezone', + 'dashboard.timezoneCustomLabelPlaceholder': 'Label (optional)', + 'dashboard.timezoneCustomTzPlaceholder': 'e.g. America/New_York', + 'dashboard.timezoneCustomAdd': 'Add', + 'dashboard.timezoneCustomErrorEmpty': 'Enter a timezone identifier', + 'dashboard.timezoneCustomErrorInvalid': 'Invalid timezone. Use format like Europe/Berlin', + 'dashboard.timezoneCustomErrorDuplicate': 'Already added', + 'dashboard.emptyTitle': 'No trips yet', + 'dashboard.emptyText': 'Create your first trip and start planning!', + 'dashboard.emptyButton': 'Create First Trip', + 'dashboard.nextTrip': 'Next Trip', + 'dashboard.shared': 'Shared', + 'dashboard.sharedBy': 'Shared by {name}', + 'dashboard.days': 'Days', + 'dashboard.places': 'Places', + 'dashboard.members': 'Buddies', + 'dashboard.archive': 'Archive', + 'dashboard.copyTrip': 'Copy', + 'dashboard.copySuffix': 'copy', + 'dashboard.restore': 'Restore', + 'dashboard.archived': 'Archived', + 'dashboard.status.ongoing': 'Ongoing', + 'dashboard.status.today': 'Today', + 'dashboard.status.tomorrow': 'Tomorrow', + 'dashboard.status.past': 'Past', + 'dashboard.status.daysLeft': '{count} days left', + 'dashboard.toast.loadError': 'Failed to load trips', + 'dashboard.toast.created': 'Trip created successfully!', + 'dashboard.toast.createError': 'Failed to create trip', + 'dashboard.toast.updated': 'Trip updated!', + 'dashboard.toast.updateError': 'Failed to update trip', + 'dashboard.toast.deleted': 'Trip deleted', + 'dashboard.toast.deleteError': 'Failed to delete trip', + 'dashboard.toast.archived': 'Trip archived', + 'dashboard.toast.archiveError': 'Failed to archive trip', + 'dashboard.toast.restored': 'Trip restored', + 'dashboard.toast.restoreError': 'Failed to restore trip', + 'dashboard.toast.copied': 'Trip copied!', + 'dashboard.toast.copyError': 'Failed to copy trip', + 'dashboard.confirm.delete': 'Delete trip "{title}"? All places and plans will be permanently deleted.', + 'dashboard.editTrip': 'Edit Trip', + 'dashboard.createTrip': 'Create New Trip', + 'dashboard.tripTitle': 'Title', + 'dashboard.tripTitlePlaceholder': 'e.g. Summer in Japan', + 'dashboard.tripDescription': 'Description', + 'dashboard.tripDescriptionPlaceholder': 'What is this trip about?', + 'dashboard.startDate': 'Start Date', + 'dashboard.endDate': 'End Date', + 'dashboard.dayCount': 'Number of Days', + 'dashboard.dayCountHint': 'How many days to plan for when no travel dates are set.', + 'dashboard.noDateHint': 'No date set — 7 default days will be created. You can change this anytime.', + 'dashboard.coverImage': 'Cover Image', + 'dashboard.addCoverImage': 'Add cover image (or drag & drop)', + 'dashboard.addMembers': 'Travel buddies', + 'dashboard.addMember': 'Add member', + 'dashboard.coverSaved': 'Cover image saved', + 'dashboard.coverUploadError': 'Failed to upload', + 'dashboard.coverRemoveError': 'Failed to remove', + 'dashboard.titleRequired': 'Title is required', + 'dashboard.endDateError': 'End date must be after start date', + + // Settings + 'settings.title': 'Settings', + 'settings.subtitle': 'Configure your personal settings', + 'settings.tabs.display': 'Display', + 'settings.tabs.map': 'Map', + 'settings.tabs.notifications': 'Notifications', + 'settings.tabs.integrations': 'Integrations', + 'settings.tabs.account': 'Account', + 'settings.tabs.about': 'About', + 'settings.map': 'Map', + 'settings.mapTemplate': 'Map Template', + 'settings.mapTemplatePlaceholder.select': 'Select template...', + 'settings.mapDefaultHint': 'Leave empty for OpenStreetMap (default)', + 'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + 'settings.mapHint': 'URL template for map tiles', + 'settings.latitude': 'Latitude', + 'settings.longitude': 'Longitude', + 'settings.saveMap': 'Save Map', + 'settings.apiKeys': 'API Keys', + 'settings.mapsKey': 'Google Maps API Key', + 'settings.mapsKeyHint': 'For place search. Requires Places API (New). Get at console.cloud.google.com', + 'settings.weatherKey': 'OpenWeatherMap API Key', + 'settings.weatherKeyHint': 'For weather data. Free at openweathermap.org/api', + 'settings.keyPlaceholder': 'Enter key...', + 'settings.configured': 'Configured', + 'settings.saveKeys': 'Save Keys', + 'settings.display': 'Display', + 'settings.colorMode': 'Color Mode', + 'settings.light': 'Light', + 'settings.dark': 'Dark', + 'settings.auto': 'Auto', + 'settings.language': 'Language', + 'settings.temperature': 'Temperature Unit', + 'settings.timeFormat': 'Time Format', + 'settings.routeCalculation': 'Route Calculation', + 'settings.blurBookingCodes': 'Blur Booking Codes', + 'settings.notifications': 'Notifications', + 'settings.notifyTripInvite': 'Trip invitations', + 'settings.notifyBookingChange': 'Booking changes', + 'settings.notifyTripReminder': 'Trip reminders', + 'settings.notifyVacayInvite': 'Vacay fusion invitations', + 'settings.notifyPhotosShared': 'Shared photos (Immich)', + 'settings.notifyCollabMessage': 'Chat messages (Collab)', + 'settings.notifyPackingTagged': 'Packing list: assignments', + 'settings.notifyWebhook': 'Webhook notifications', + 'settings.notifyVersionAvailable': 'New version available', + 'settings.notificationPreferences.email': 'Email', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', + 'settings.webhookUrl.label': 'Webhook URL', + 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', + 'settings.webhookUrl.hint': 'Enter your Discord, Slack, or custom webhook URL to receive notifications.', + 'settings.webhookUrl.save': 'Save', + 'settings.webhookUrl.saved': 'Webhook URL saved', + 'settings.webhookUrl.test': 'Test', + 'settings.webhookUrl.testSuccess': 'Test webhook sent successfully', + 'settings.webhookUrl.testFailed': 'Test webhook failed', + 'admin.notifications.title': 'Notifications', + 'admin.notifications.hint': 'Choose one notification channel. Only one can be active at a time.', + 'admin.notifications.none': 'Disabled', + 'admin.notifications.email': 'Email (SMTP)', + 'admin.notifications.webhook': 'Webhook', + 'admin.notifications.save': 'Save notification settings', + 'admin.notifications.saved': 'Notification settings saved', + 'admin.notifications.testWebhook': 'Send test webhook', + 'admin.notifications.testWebhookSuccess': 'Test webhook sent successfully', + 'admin.notifications.testWebhookFailed': 'Test webhook failed', + 'admin.notifications.emailPanel.title': 'Email (SMTP)', + 'admin.notifications.webhookPanel.title': 'Webhook', + 'admin.notifications.inappPanel.title': 'In-App', + 'admin.notifications.inappPanel.hint': 'In-app notifications are always active and cannot be disabled globally.', + 'admin.notifications.adminWebhookPanel.title': 'Admin Webhook', + 'admin.notifications.adminWebhookPanel.hint': 'This webhook is used exclusively for admin notifications (e.g. version alerts). It is separate from per-user webhooks and always fires when set.', + 'admin.notifications.adminWebhookPanel.saved': 'Admin webhook URL saved', + 'admin.notifications.adminWebhookPanel.testSuccess': 'Test webhook sent successfully', + 'admin.notifications.adminWebhookPanel.testFailed': 'Test webhook failed', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Admin webhook always fires when a URL is configured', + 'admin.notifications.adminNotificationsHint': 'Configure which channels deliver admin-only notifications (e.g. version alerts).', + 'admin.smtp.title': 'Email & Notifications', + 'admin.smtp.hint': 'SMTP configuration for sending email notifications.', + 'admin.smtp.testButton': 'Send test email', + 'admin.webhook.hint': 'Allow users to configure their own webhook URLs for notifications (Discord, Slack, etc.).', + 'admin.smtp.testSuccess': 'Test email sent successfully', + 'admin.smtp.testFailed': 'Test email failed', + 'settings.notificationsDisabled': 'Notifications are not configured. Ask an admin to enable email or webhook notifications.', + 'settings.notificationsActive': 'Active channel', + 'settings.notificationsManagedByAdmin': 'Notification events are configured by your administrator.', + 'dayplan.icsTooltip': 'Export calendar (ICS)', + 'share.linkTitle': 'Public Link', + 'share.linkHint': 'Create a link anyone can use to view this trip without logging in. Read-only — no editing possible.', + 'share.createLink': 'Create link', + 'share.deleteLink': 'Delete link', + 'share.createError': 'Could not create link', + 'common.copy': 'Copy', + 'common.copied': 'Copied', + 'share.permMap': 'Map & Plan', + 'share.permBookings': 'Bookings', + 'share.permPacking': 'Packing', + 'shared.expired': 'Link expired or invalid', + 'shared.expiredHint': 'This shared trip link is no longer active.', + 'shared.readOnly': 'Read-only shared view', + 'shared.tabPlan': 'Plan', + 'shared.tabBookings': 'Bookings', + 'shared.tabPacking': 'Packing', + 'shared.tabBudget': 'Budget', + 'shared.tabChat': 'Chat', + 'shared.days': 'days', + 'shared.places': 'places', + 'shared.other': 'Other', + 'shared.totalBudget': 'Total Budget', + 'shared.messages': 'messages', + 'shared.sharedVia': 'Shared via', + 'shared.confirmed': 'Confirmed', + 'shared.pending': 'Pending', + 'share.permBudget': 'Budget', + 'share.permCollab': 'Chat', + 'settings.on': 'On', + 'settings.off': 'Off', + 'settings.mcp.title': 'MCP Configuration', + 'settings.mcp.endpoint': 'MCP Endpoint', + 'settings.mcp.clientConfig': 'Client Configuration', + 'settings.mcp.clientConfigHint': 'Replace with an API token from the list below. The path to npx may need to be adjusted for your system (e.g. C:\\PROGRA~1\\nodejs\\npx.cmd on Windows).', + 'settings.mcp.copy': 'Copy', + 'settings.mcp.copied': 'Copied!', + 'settings.mcp.apiTokens': 'API Tokens', + 'settings.mcp.createToken': 'Create New Token', + 'settings.mcp.noTokens': 'No tokens yet. Create one to connect MCP clients.', + 'settings.mcp.tokenCreatedAt': 'Created', + 'settings.mcp.tokenUsedAt': 'Used', + 'settings.mcp.deleteTokenTitle': 'Delete Token', + 'settings.mcp.deleteTokenMessage': 'This token will stop working immediately. Any MCP client using it will lose access.', + 'settings.mcp.modal.createTitle': 'Create API Token', + 'settings.mcp.modal.tokenName': 'Token Name', + 'settings.mcp.modal.tokenNamePlaceholder': 'e.g. Claude Desktop, Work laptop', + 'settings.mcp.modal.creating': 'Creating…', + 'settings.mcp.modal.create': 'Create Token', + 'settings.mcp.modal.createdTitle': 'Token Created', + 'settings.mcp.modal.createdWarning': 'This token will only be shown once. Copy and store it now — it cannot be recovered.', + 'settings.mcp.modal.done': 'Done', + 'settings.mcp.toast.created': 'Token created', + 'settings.mcp.toast.createError': 'Failed to create token', + 'settings.mcp.toast.deleted': 'Token deleted', + 'settings.mcp.toast.deleteError': 'Failed to delete token', + 'settings.account': 'Account', + 'settings.about': 'About', + 'settings.about.reportBug': 'Report a Bug', + 'settings.about.reportBugHint': 'Found an issue? Let us know', + 'settings.about.featureRequest': 'Feature Request', + 'settings.about.featureRequestHint': 'Suggest a new feature', + 'settings.about.wikiHint': 'Documentation & guides', + 'settings.about.description': 'TREK is a self-hosted travel planner that helps you organize your trips from the first idea to the last memory. Day planning, budget, packing lists, photos and much more — all in one place, on your own server.', + 'settings.about.madeWith': 'Made with', + 'settings.about.madeBy': 'by Maurice and a growing open-source community.', + 'settings.username': 'Username', + 'settings.email': 'Email', + 'settings.role': 'Role', + 'settings.roleAdmin': 'Administrator', + 'settings.oidcLinked': 'Linked with', + 'settings.changePassword': 'Change Password', + 'settings.currentPassword': 'Current password', + 'settings.currentPasswordRequired': 'Current password is required', + 'settings.newPassword': 'New password', + 'settings.confirmPassword': 'Confirm new password', + 'settings.updatePassword': 'Update password', + 'settings.passwordRequired': 'Please enter current and new password', + 'settings.passwordTooShort': 'Password must be at least 8 characters', + 'settings.passwordMismatch': 'Passwords do not match', + 'settings.passwordWeak': 'Password must contain uppercase, lowercase, a number, and a special character', + 'settings.passwordChanged': 'Password changed successfully', + 'settings.mustChangePassword': 'You must change your password before you can continue. Please set a new password below.', + 'settings.deleteAccount': 'Delete account', + 'settings.deleteAccountTitle': 'Delete your account?', + 'settings.deleteAccountWarning': 'Your account and all your trips, places, and files will be permanently deleted. This action cannot be undone.', + 'settings.deleteAccountConfirm': 'Delete permanently', + 'settings.deleteBlockedTitle': 'Deletion not possible', + 'settings.deleteBlockedMessage': 'You are the only administrator. Promote another user to admin before deleting your account.', + 'settings.roleUser': 'User', + 'settings.saveProfile': 'Save Profile', + 'settings.toast.mapSaved': 'Map settings saved', + 'settings.toast.keysSaved': 'API keys saved', + 'settings.toast.displaySaved': 'Display settings saved', + 'settings.toast.profileSaved': 'Profile saved', + 'settings.uploadAvatar': 'Upload Profile Picture', + 'settings.removeAvatar': 'Remove Profile Picture', + 'settings.avatarUploaded': 'Profile picture updated', + 'settings.avatarRemoved': 'Profile picture removed', + 'settings.avatarError': 'Upload failed', + 'settings.mfa.title': 'Two-factor authentication (2FA)', + 'settings.mfa.description': 'Adds a second step when you sign in with email and password. Use an authenticator app (Google Authenticator, Authy, etc.).', + 'settings.mfa.requiredByPolicy': 'Your administrator requires two-factor authentication. Set up an authenticator app below before continuing.', + 'settings.mfa.backupTitle': 'Backup codes', + 'settings.mfa.backupDescription': 'Use these one-time backup codes if you lose access to your authenticator app.', + 'settings.mfa.backupWarning': 'Save these codes now. Each code can only be used once.', + 'settings.mfa.backupCopy': 'Copy codes', + 'settings.mfa.backupDownload': 'Download TXT', + 'settings.mfa.backupPrint': 'Print / PDF', + 'settings.mfa.backupCopied': 'Backup codes copied', + 'settings.mfa.enabled': '2FA is enabled on your account.', + 'settings.mfa.disabled': '2FA is not enabled.', + 'settings.mfa.setup': 'Set up authenticator', + 'settings.mfa.scanQr': 'Scan this QR code with your app, or enter the secret manually.', + 'settings.mfa.secretLabel': 'Secret key (manual entry)', + 'settings.mfa.codePlaceholder': '6-digit code', + 'settings.mfa.enable': 'Enable 2FA', + 'settings.mfa.cancelSetup': 'Cancel', + 'settings.mfa.disableTitle': 'Disable 2FA', + 'settings.mfa.disableHint': 'Enter your account password and a current code from your authenticator.', + 'settings.mfa.disable': 'Disable 2FA', + 'settings.mfa.toastEnabled': 'Two-factor authentication enabled', + 'settings.mfa.toastDisabled': 'Two-factor authentication disabled', + 'settings.mfa.demoBlocked': 'Not available in demo mode', + + // Login + 'login.error': 'Login failed. Please check your credentials.', + 'login.tagline': 'Your Trips.\nYour Plan.', + 'login.description': 'Plan trips collaboratively with interactive maps, budgets, and real-time sync.', + 'login.features.maps': 'Interactive Maps', + 'login.features.mapsDesc': 'Google Places, routes & clustering', + 'login.features.realtime': 'Real-Time Sync', + 'login.features.realtimeDesc': 'Plan together via WebSocket', + 'login.features.budget': 'Budget Tracking', + 'login.features.budgetDesc': 'Categories, charts & per-person costs', + 'login.features.collab': 'Collaboration', + 'login.features.collabDesc': 'Multi-user with shared trips', + 'login.features.packing': 'Packing Lists', + 'login.features.packingDesc': 'Categories, progress & suggestions', + 'login.features.bookings': 'Reservations', + 'login.features.bookingsDesc': 'Flights, hotels, restaurants & more', + 'login.features.files': 'Documents', + 'login.features.filesDesc': 'Upload & manage documents', + 'login.features.routes': 'Smart Routes', + 'login.features.routesDesc': 'Auto-optimize & Google Maps export', + 'login.selfHosted': 'Self-hosted \u00B7 Open Source \u00B7 Your data stays yours', + 'login.title': 'Sign In', + 'login.subtitle': 'Welcome back', + 'login.signingIn': 'Signing in…', + 'login.signIn': 'Sign In', + 'login.createAdmin': 'Create Admin Account', + 'login.createAdminHint': 'Set up the first admin account for TREK.', + 'login.setNewPassword': 'Set New Password', + 'login.setNewPasswordHint': 'You must change your password before continuing.', + 'login.createAccount': 'Create Account', + 'login.createAccountHint': 'Register a new account.', + 'login.creating': 'Creating…', + 'login.noAccount': "Don't have an account?", + 'login.hasAccount': 'Already have an account?', + 'login.register': 'Register', + 'login.emailPlaceholder': 'your@email.com', + 'login.username': 'Username', + 'login.oidc.registrationDisabled': 'Registration is disabled. Contact your administrator.', + 'login.oidc.noEmail': 'No email received from provider.', + 'login.oidc.tokenFailed': 'Authentication failed.', + 'login.oidc.invalidState': 'Invalid session. Please try again.', + 'login.demoFailed': 'Demo login failed', + 'login.oidcSignIn': 'Sign in with {name}', + 'login.oidcOnly': 'Password authentication is disabled. Please sign in using your SSO provider.', + 'login.oidcLoggedOut': 'You have been logged out. Sign in again using your SSO provider.', + 'login.demoHint': 'Try the demo — no registration needed', + 'login.mfaTitle': 'Two-factor authentication', + 'login.mfaSubtitle': 'Enter the 6-digit code from your authenticator app.', + 'login.mfaCodeLabel': 'Verification code', + 'login.mfaCodeRequired': 'Enter the code from your authenticator app.', + 'login.mfaHint': 'Open Google Authenticator, Authy, or another TOTP app.', + 'login.mfaBack': '← Back to sign in', + 'login.mfaVerify': 'Verify', + + // Register + 'register.passwordMismatch': 'Passwords do not match', + 'register.passwordTooShort': 'Password must be at least 8 characters', + 'register.failed': 'Registration failed', + 'register.getStarted': 'Get Started', + 'register.subtitle': 'Create an account and start planning your dream trips.', + 'register.feature1': 'Unlimited trip plans', + 'register.feature2': 'Interactive map view', + 'register.feature3': 'Manage places and categories', + 'register.feature4': 'Track reservations', + 'register.feature5': 'Create packing lists', + 'register.feature6': 'Store photos and files', + 'register.createAccount': 'Create Account', + 'register.startPlanning': 'Start your trip planning', + 'register.minChars': 'Min. 6 characters', + 'register.confirmPassword': 'Confirm Password', + 'register.repeatPassword': 'Repeat password', + 'register.registering': 'Registering...', + 'register.register': 'Register', + 'register.hasAccount': 'Already have an account?', + 'register.signIn': 'Sign In', + + // Admin + 'admin.title': 'Administration', + 'admin.subtitle': 'User management and system settings', + 'admin.tabs.users': 'Users', + 'admin.tabs.categories': 'Categories', + 'admin.tabs.backup': 'Backup', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.audit': 'Audit', + 'admin.stats.users': 'Users', + 'admin.stats.trips': 'Trips', + 'admin.stats.places': 'Places', + 'admin.stats.photos': 'Photos', + 'admin.stats.files': 'Files', + 'admin.table.user': 'User', + 'admin.table.email': 'Email', + 'admin.table.role': 'Role', + 'admin.table.created': 'Created', + 'admin.table.lastLogin': 'Last Login', + 'admin.table.actions': 'Actions', + 'admin.you': '(You)', + 'admin.editUser': 'Edit User', + 'admin.newPassword': 'New Password', + 'admin.newPasswordHint': 'Leave empty to keep current password', + 'admin.deleteUser': 'Delete user "{name}"? All trips will be permanently deleted.', + 'admin.deleteUserTitle': 'Delete user', + 'admin.newPasswordPlaceholder': 'Enter new password…', + 'admin.toast.loadError': 'Failed to load admin data', + 'admin.toast.userUpdated': 'User updated', + 'admin.toast.updateError': 'Failed to update', + 'admin.toast.userDeleted': 'User deleted', + 'admin.toast.deleteError': 'Failed to delete', + 'admin.toast.cannotDeleteSelf': 'Cannot delete your own account', + 'admin.toast.userCreated': 'User created', + 'admin.toast.createError': 'Failed to create user', + 'admin.toast.fieldsRequired': 'Username, email and password are required', + 'admin.createUser': 'Create User', + 'admin.invite.title': 'Invite Links', + 'admin.invite.subtitle': 'Create one-time registration links', + 'admin.invite.create': 'Create Link', + 'admin.invite.createAndCopy': 'Create & Copy', + 'admin.invite.empty': 'No invite links created yet', + 'admin.invite.maxUses': 'Max. Uses', + 'admin.invite.expiry': 'Expires after', + 'admin.invite.uses': 'used', + 'admin.invite.expiresAt': 'expires', + 'admin.invite.createdBy': 'by', + 'admin.invite.active': 'Active', + 'admin.invite.expired': 'Expired', + 'admin.invite.usedUp': 'Used up', + 'admin.invite.copied': 'Invite link copied to clipboard', + 'admin.invite.copyLink': 'Copy link', + 'admin.invite.deleted': 'Invite link deleted', + 'admin.invite.createError': 'Failed to create invite link', + 'admin.invite.deleteError': 'Failed to delete invite link', + 'admin.tabs.settings': 'Settings', + 'admin.allowRegistration': 'Allow Registration', + 'admin.allowRegistrationHint': 'New users can register themselves', + 'admin.requireMfa': 'Require two-factor authentication (2FA)', + 'admin.requireMfaHint': 'Users without 2FA must complete setup in Settings before using the app.', + 'admin.apiKeys': 'API Keys', + 'admin.apiKeysHint': 'Optional. Enables extended place data like photos and weather.', + 'admin.mapsKey': 'Google Maps API Key', + 'admin.mapsKeyHint': 'Required for place search. Get at console.cloud.google.com', + 'admin.mapsKeyHintLong': 'Without an API key, OpenStreetMap is used for place search. With a Google API key, photos, ratings, and opening hours can be loaded as well. Get one at console.cloud.google.com.', + 'admin.recommended': 'Recommended', + 'admin.weatherKey': 'OpenWeatherMap API Key', + 'admin.weatherKeyHint': 'For weather data. Free at openweathermap.org', + 'admin.validateKey': 'Test', + 'admin.keyValid': 'Connected', + 'admin.keyInvalid': 'Invalid', + 'admin.keySaved': 'API keys saved', + 'admin.oidcTitle': 'Single Sign-On (OIDC)', + 'admin.oidcSubtitle': 'Allow login via external providers like Google, Apple, Authentik or Keycloak.', + 'admin.oidcDisplayName': 'Display Name', + 'admin.oidcIssuer': 'Issuer URL', + 'admin.oidcIssuerHint': 'The OpenID Connect Issuer URL of the provider. e.g. https://accounts.google.com', + 'admin.oidcSaved': 'OIDC configuration saved', + 'admin.oidcOnlyMode': 'Disable password authentication', + 'admin.oidcOnlyModeHint': 'When enabled, only SSO login is permitted. Password-based login and registration are blocked.', + + // File Types + 'admin.fileTypes': 'Allowed File Types', + 'admin.fileTypesHint': 'Configure which file types users can upload.', + 'admin.fileTypesFormat': 'Comma-separated extensions (e.g. jpg,png,pdf,doc). Use * to allow all types.', + 'admin.fileTypesSaved': 'File type settings saved', + + // Packing Templates & Bag Tracking + 'admin.bagTracking.title': 'Bag Tracking', + 'admin.bagTracking.subtitle': 'Enable weight and bag assignment for packing items', + 'admin.tabs.config': 'Personalization', + 'admin.tabs.templates': 'Packing Templates', + 'admin.packingTemplates.title': 'Packing Templates', + 'admin.packingTemplates.subtitle': 'Create reusable packing lists for your trips', + 'admin.packingTemplates.create': 'New Template', + 'admin.packingTemplates.namePlaceholder': 'Template name (e.g. Beach Holiday)', + 'admin.packingTemplates.empty': 'No templates created yet', + 'admin.packingTemplates.items': 'items', + 'admin.packingTemplates.categories': 'categories', + 'admin.packingTemplates.itemName': 'Item name', + 'admin.packingTemplates.itemCategory': 'Category', + 'admin.packingTemplates.categoryName': 'Category name (e.g. Clothing)', + 'admin.packingTemplates.addCategory': 'Add category', + 'admin.packingTemplates.created': 'Template created', + 'admin.packingTemplates.deleted': 'Template deleted', + 'admin.packingTemplates.loadError': 'Failed to load templates', + 'admin.packingTemplates.createError': 'Failed to create template', + 'admin.packingTemplates.deleteError': 'Failed to delete template', + 'admin.packingTemplates.saveError': 'Failed to save', + + // Addons + 'admin.tabs.addons': 'Addons', + 'admin.addons.title': 'Addons', + 'admin.addons.subtitle': 'Enable or disable features to customize your TREK experience.', + 'admin.addons.catalog.packing.name': 'Lists', + 'admin.addons.catalog.packing.description': 'Packing lists and to-do tasks for your trips', + 'admin.addons.catalog.budget.name': 'Budget', + 'admin.addons.catalog.budget.description': 'Track expenses and plan your trip budget', + 'admin.addons.catalog.documents.name': 'Documents', + 'admin.addons.catalog.documents.description': 'Store and manage travel documents', + 'admin.addons.catalog.vacay.name': 'Vacay', + 'admin.addons.catalog.vacay.description': 'Personal vacation planner with calendar view', + 'admin.addons.catalog.atlas.name': 'Atlas', + 'admin.addons.catalog.atlas.description': 'World map with visited countries and travel stats', + 'admin.addons.catalog.collab.name': 'Collab', + 'admin.addons.catalog.collab.description': 'Real-time notes, polls, and chat for trip planning', + 'admin.addons.catalog.memories.name': 'Photos (Immich)', + 'admin.addons.catalog.memories.description': 'Share trip photos via your Immich instance', + 'admin.addons.catalog.mcp.name': 'MCP', + 'admin.addons.catalog.mcp.description': 'Model Context Protocol for AI assistant integration', + 'admin.addons.subtitleBefore': 'Enable or disable features to customize your ', + 'admin.addons.subtitleAfter': ' experience.', + 'admin.addons.enabled': 'Enabled', + 'admin.addons.disabled': 'Disabled', + 'admin.addons.type.trip': 'Trip', + 'admin.addons.type.global': 'Global', + 'admin.addons.type.integration': 'Integration', + 'admin.addons.tripHint': 'Available as a tab within each trip', + 'admin.addons.globalHint': 'Available as a standalone section in the main navigation', + 'admin.addons.integrationHint': 'Backend services and API integrations with no dedicated page', + 'admin.addons.toast.updated': 'Addon updated', + 'admin.addons.toast.error': 'Failed to update addon', + 'admin.addons.noAddons': 'No addons available', + // Weather info + 'admin.weather.title': 'Weather Data', + 'admin.weather.badge': 'Since March 24, 2026', + 'admin.weather.description': 'TREK uses Open-Meteo as its weather data source. Open-Meteo is a free, open-source weather service — no API key required.', + 'admin.weather.forecast': '16-day forecast', + 'admin.weather.forecastDesc': 'Previously 5 days (OpenWeatherMap)', + 'admin.weather.climate': 'Historical climate data', + 'admin.weather.climateDesc': 'Averages from the last 85 years for days beyond the 16-day forecast', + 'admin.weather.requests': '10,000 requests / day', + 'admin.weather.requestsDesc': 'Free, no API key required', + 'admin.weather.locationHint': 'Weather is based on the first place with coordinates in each day. If no place is assigned to a day, any place from the place list is used as a reference.', + + // GitHub + 'admin.tabs.mcpTokens': 'MCP Tokens', + 'admin.mcpTokens.title': 'MCP Tokens', + 'admin.mcpTokens.subtitle': 'Manage API tokens across all users', + 'admin.mcpTokens.owner': 'Owner', + 'admin.mcpTokens.tokenName': 'Token Name', + 'admin.mcpTokens.created': 'Created', + 'admin.mcpTokens.lastUsed': 'Last Used', + 'admin.mcpTokens.never': 'Never', + 'admin.mcpTokens.empty': 'No MCP tokens have been created yet', + 'admin.mcpTokens.deleteTitle': 'Delete Token', + 'admin.mcpTokens.deleteMessage': 'This will revoke the token immediately. The user will lose MCP access through this token.', + 'admin.mcpTokens.deleteSuccess': 'Token deleted', + 'admin.mcpTokens.deleteError': 'Failed to delete token', + 'admin.mcpTokens.loadError': 'Failed to load tokens', + 'admin.tabs.github': 'GitHub', + + 'admin.audit.subtitle': 'Security-sensitive and administration events (backups, users, MFA, settings).', + 'admin.audit.empty': 'No audit entries yet.', + 'admin.audit.refresh': 'Refresh', + 'admin.audit.loadMore': 'Load more', + 'admin.audit.showing': '{count} loaded · {total} total', + 'admin.audit.col.time': 'Time', + 'admin.audit.col.user': 'User', + 'admin.audit.col.action': 'Action', + 'admin.audit.col.resource': 'Resource', + 'admin.audit.col.ip': 'IP', + 'admin.audit.col.details': 'Details', + 'admin.github.title': 'Release History', + 'admin.github.subtitle': 'Latest updates from {repo}', + 'admin.github.latest': 'Latest', + 'admin.github.prerelease': 'Pre-release', + 'admin.github.showDetails': 'Show details', + 'admin.github.hideDetails': 'Hide details', + 'admin.github.loadMore': 'Load more', + 'admin.github.loading': 'Loading...', + 'admin.github.error': 'Failed to load releases', + 'admin.github.by': 'by', + 'admin.github.support': 'Helps me keep building TREK', + + 'admin.update.available': 'Update available', + 'admin.update.text': 'TREK {version} is available. You are running {current}.', + 'admin.update.button': 'View on GitHub', + 'admin.update.install': 'Install Update', + 'admin.update.confirmTitle': 'Install Update?', + 'admin.update.confirmText': 'TREK will be updated from {current} to {version}. The server will restart automatically afterwards.', + 'admin.update.dataInfo': 'All your data (trips, users, API keys, uploads, Vacay, Atlas, budgets) will be preserved.', + 'admin.update.warning': 'The app will be briefly unavailable during the restart.', + 'admin.update.confirm': 'Update Now', + 'admin.update.installing': 'Updating…', + 'admin.update.success': 'Update installed! Server is restarting…', + 'admin.update.failed': 'Update failed', + 'admin.update.backupHint': 'We recommend creating a backup before updating.', + 'admin.update.backupLink': 'Go to Backup', + 'admin.update.howTo': 'How to Update', + 'admin.update.dockerText': 'Your TREK instance runs in Docker. To update to {version}, run the following commands on your server:', + 'admin.update.reloadHint': 'Please reload the page in a few seconds.', + + // Vacay addon + 'vacay.subtitle': 'Plan and manage vacation days', + 'vacay.settings': 'Settings', + 'vacay.year': 'Year', + 'vacay.addYear': 'Add next year', + 'vacay.addPrevYear': 'Add previous year', + 'vacay.removeYear': 'Remove year', + 'vacay.removeYearConfirm': 'Remove {year}?', + 'vacay.removeYearHint': 'All vacation entries and company holidays for this year will be permanently deleted.', + 'vacay.remove': 'Remove', + 'vacay.persons': 'Persons', + 'vacay.noPersons': 'No persons added', + 'vacay.addPerson': 'Add Person', + 'vacay.editPerson': 'Edit Person', + 'vacay.removePerson': 'Remove Person', + 'vacay.removePersonConfirm': 'Remove {name}?', + 'vacay.removePersonHint': 'All vacation entries for this person will be permanently deleted.', + 'vacay.personName': 'Name', + 'vacay.personNamePlaceholder': 'Enter name', + 'vacay.color': 'Color', + 'vacay.add': 'Add', + 'vacay.legend': 'Legend', + 'vacay.publicHoliday': 'Public Holiday', + 'vacay.companyHoliday': 'Company Holiday', + 'vacay.weekend': 'Weekend', + 'vacay.modeVacation': 'Vacation', + 'vacay.modeCompany': 'Company Holiday', + 'vacay.entitlement': 'Entitlement', + 'vacay.entitlementDays': 'Days', + 'vacay.used': 'Used', + 'vacay.remaining': 'Left', + 'vacay.carriedOver': 'from {year}', + 'vacay.blockWeekends': 'Block Weekends', + 'vacay.blockWeekendsHint': 'Prevent vacation entries on weekend days', + 'vacay.weekendDays': 'Weekend days', + 'vacay.mon': 'Mon', + 'vacay.tue': 'Tue', + 'vacay.wed': 'Wed', + 'vacay.thu': 'Thu', + 'vacay.fri': 'Fri', + 'vacay.sat': 'Sat', + 'vacay.sun': 'Sun', + 'vacay.publicHolidays': 'Public Holidays', + 'vacay.publicHolidaysHint': 'Mark public holidays in the calendar', + 'vacay.selectCountry': 'Select country', + 'vacay.selectRegion': 'Select region (optional)', + 'vacay.addCalendar': 'Add calendar', + 'vacay.calendarLabel': 'Label (optional)', + 'vacay.calendarColor': 'Color', + 'vacay.noCalendars': 'No holiday calendars added yet', + 'vacay.companyHolidays': 'Company Holidays', + 'vacay.companyHolidaysHint': 'Allow marking company-wide holiday days', + 'vacay.companyHolidaysNoDeduct': 'Company holidays do not count towards vacation days.', + 'vacay.carryOver': 'Carry Over', + 'vacay.carryOverHint': 'Automatically carry remaining vacation days into the next year', + 'vacay.sharing': 'Sharing', + 'vacay.sharingHint': 'Share your vacation plan with other TREK users', + 'vacay.owner': 'Owner', + 'vacay.shareEmailPlaceholder': 'Email of TREK user', + 'vacay.shareSuccess': 'Plan shared successfully', + 'vacay.shareError': 'Could not share plan', + 'vacay.dissolve': 'Dissolve Fusion', + 'vacay.dissolveHint': 'Separate calendars again. Your entries will be kept.', + 'vacay.dissolveAction': 'Dissolve', + 'vacay.dissolved': 'Calendar separated', + 'vacay.fusedWith': 'Fused with', + 'vacay.you': 'you', + 'vacay.noData': 'No data', + 'vacay.changeColor': 'Change color', + 'vacay.inviteUser': 'Invite User', + 'vacay.inviteHint': 'Invite another TREK user to share a combined vacation calendar.', + 'vacay.selectUser': 'Select user', + 'vacay.sendInvite': 'Send Invite', + 'vacay.inviteSent': 'Invite sent', + 'vacay.inviteError': 'Could not send invite', + 'vacay.pending': 'pending', + 'vacay.noUsersAvailable': 'No users available', + 'vacay.accept': 'Accept', + 'vacay.decline': 'Decline', + 'vacay.acceptFusion': 'Accept & Fuse', + 'vacay.inviteTitle': 'Fusion Request', + 'vacay.inviteWantsToFuse': 'wants to share a vacation calendar with you.', + 'vacay.fuseInfo1': 'Both of you will see all vacation entries in one shared calendar.', + 'vacay.fuseInfo2': 'Both parties can create and edit entries for each other.', + 'vacay.fuseInfo3': 'Both parties can delete entries and change vacation entitlements.', + 'vacay.fuseInfo4': 'Settings like public holidays and company holidays are shared.', + 'vacay.fuseInfo5': 'The fusion can be dissolved at any time by either party. Your entries will be preserved.', + 'nav.myTrips': 'My Trips', + + // Atlas addon + 'atlas.subtitle': 'Your travel footprint around the world', + 'atlas.countries': 'Countries', + 'atlas.trips': 'Trips', + 'atlas.places': 'Places', + 'atlas.unmark': 'Remove', + 'atlas.confirmMark': 'Mark this country as visited?', + 'atlas.confirmUnmark': 'Remove this country from your visited list?', + 'atlas.confirmUnmarkRegion': 'Remove this region from your visited list?', + 'atlas.markVisited': 'Mark as visited', + 'atlas.markVisitedHint': 'Add this country to your visited list', + 'atlas.markRegionVisitedHint': 'Add this region to your visited list', + 'atlas.addToBucket': 'Add to bucket list', + 'atlas.addPoi': 'Add place', + 'atlas.searchCountry': 'Search a country...', + 'atlas.bucketNamePlaceholder': 'Name (country, city, place...)', + 'atlas.month': 'Month', + 'atlas.year': 'Year', + 'atlas.addToBucketHint': 'Save as a place you want to visit', + 'atlas.bucketWhen': 'When do you plan to visit?', + 'atlas.statsTab': 'Stats', + 'atlas.bucketTab': 'Bucket List', + 'atlas.addBucket': 'Add to bucket list', + 'atlas.bucketNotesPlaceholder': 'Notes (optional)', + 'atlas.bucketEmpty': 'Your bucket list is empty', + 'atlas.bucketEmptyHint': 'Add places you dream of visiting', + 'atlas.days': 'Days', + 'atlas.visitedCountries': 'Visited Countries', + 'atlas.cities': 'Cities', + 'atlas.noData': 'No travel data yet', + 'atlas.noDataHint': 'Create a trip and add places to see your world map', + 'atlas.lastTrip': 'Last trip', + 'atlas.nextTrip': 'Next trip', + 'atlas.daysLeft': 'days left', + 'atlas.streak': 'Streak', + 'atlas.years': 'years', + 'atlas.yearInRow': 'year in a row', + 'atlas.yearsInRow': 'years in a row', + 'atlas.tripIn': 'trip in', + 'atlas.tripsIn': 'trips in', + 'atlas.since': 'since', + 'atlas.europe': 'Europe', + 'atlas.asia': 'Asia', + 'atlas.northAmerica': 'N. America', + 'atlas.southAmerica': 'S. America', + 'atlas.africa': 'Africa', + 'atlas.oceania': 'Oceania', + 'atlas.other': 'Other', + 'atlas.firstVisit': 'First trip', + 'atlas.lastVisitLabel': 'Last trip', + 'atlas.tripSingular': 'Trip', + 'atlas.tripPlural': 'Trips', + 'atlas.placeVisited': 'Place visited', + 'atlas.placesVisited': 'Places visited', + + // Trip Planner + 'trip.tabs.plan': 'Plan', + 'trip.tabs.reservations': 'Bookings', + 'trip.tabs.reservationsShort': 'Book', + 'trip.tabs.packing': 'Packing List', + 'trip.tabs.packingShort': 'Packing', + 'trip.tabs.lists': 'Lists', + 'trip.tabs.listsShort': 'Lists', + 'trip.tabs.budget': 'Budget', + 'trip.tabs.files': 'Files', + 'trip.loading': 'Loading trip...', + 'trip.loadingPhotos': 'Loading place photos...', + 'trip.mobilePlan': 'Plan', + 'trip.mobilePlaces': 'Places', + 'trip.toast.placeUpdated': 'Place updated', + 'trip.toast.placeAdded': 'Place added', + 'trip.toast.placeDeleted': 'Place deleted', + 'trip.toast.selectDay': 'Please select a day first', + 'trip.toast.assignedToDay': 'Place assigned to day', + 'trip.toast.reorderError': 'Failed to reorder', + 'trip.toast.reservationUpdated': 'Reservation updated', + 'trip.toast.reservationAdded': 'Reservation added', + 'trip.toast.deleted': 'Deleted', + 'trip.confirm.deletePlace': 'Are you sure you want to delete this place?', + + // Day Plan Sidebar + 'dayplan.emptyDay': 'No places planned for this day', + 'dayplan.cannotReorderTransport': 'Bookings with a fixed time cannot be reordered', + 'dayplan.confirmRemoveTimeTitle': 'Remove time?', + 'dayplan.confirmRemoveTimeBody': 'This place has a fixed time ({time}). Moving it will remove the time and allow free sorting.', + 'dayplan.confirmRemoveTimeAction': 'Remove time & move', + 'dayplan.cannotDropOnTimed': 'Items cannot be placed between time-bound entries', + 'dayplan.cannotBreakChronology': 'This would break the chronological order of timed items and bookings', + 'dayplan.addNote': 'Add Note', + 'dayplan.editNote': 'Edit Note', + 'dayplan.noteAdd': 'Add Note', + 'dayplan.noteEdit': 'Edit Note', + 'dayplan.noteTitle': 'Note', + 'dayplan.noteSubtitle': 'Daily Note', + 'dayplan.totalCost': 'Total Cost', + 'dayplan.days': 'Days', + 'dayplan.dayN': 'Day {n}', + 'dayplan.calculating': 'Calculating...', + 'dayplan.route': 'Route', + 'dayplan.optimize': 'Optimize', + 'dayplan.optimized': 'Route optimized', + 'dayplan.routeError': 'Failed to calculate route', + 'dayplan.toast.needTwoPlaces': 'At least two places needed for route optimization', + 'dayplan.toast.routeOptimized': 'Route optimized', + 'dayplan.toast.noGeoPlaces': 'No places with coordinates found for route calculation', + 'dayplan.confirmed': 'Confirmed', + 'dayplan.pendingRes': 'Pending', + 'dayplan.pdf': 'PDF', + 'dayplan.pdfTooltip': 'Export day plan as PDF', + 'dayplan.pdfError': 'Failed to export PDF', + + // Places Sidebar + 'places.addPlace': 'Add Place/Activity', + 'places.importGpx': 'GPX', + 'places.gpxImported': '{count} places imported from GPX', + 'places.urlResolved': 'Place imported from URL', + 'places.gpxError': 'GPX import failed', + 'places.importGoogleList': 'Google List', + 'places.googleListHint': 'Paste a shared Google Maps list link to import all places.', + 'places.googleListImported': '{count} places imported from "{list}"', + 'places.googleListError': 'Failed to import Google Maps list', + 'places.viewDetails': 'View Details', + 'places.assignToDay': 'Add to which day?', + 'places.all': 'All', + 'places.unplanned': 'Unplanned', + 'places.search': 'Search places...', + 'places.allCategories': 'All Categories', + 'places.categoriesSelected': 'categories', + 'places.clearFilter': 'Clear filter', + 'places.count': '{count} places', + 'places.countSingular': '1 place', + 'places.allPlanned': 'All places are planned', + 'places.noneFound': 'No places found', + 'places.editPlace': 'Edit Place', + 'places.formName': 'Name', + 'places.formNamePlaceholder': 'e.g. Eiffel Tower', + 'places.formDescription': 'Description', + 'places.formDescriptionPlaceholder': 'Short description...', + 'places.formAddress': 'Address', + 'places.formAddressPlaceholder': 'Street, City, Country', + 'places.formLat': 'Latitude (e.g. 48.8566)', + 'places.formLng': 'Longitude (e.g. 2.3522)', + 'places.formCategory': 'Category', + 'places.noCategory': 'No Category', + 'places.categoryNamePlaceholder': 'Category name', + 'places.formTime': 'Time', + 'places.startTime': 'Start', + 'places.endTime': 'End', + 'places.endTimeBeforeStart': 'End time is before start time', + 'places.timeCollision': 'Time overlap with:', + 'places.formWebsite': 'Website', + 'places.formNotesPlaceholder': 'Personal notes...', + 'places.formReservation': 'Reservation', + 'places.reservationNotesPlaceholder': 'Reservation notes, confirmation number...', + 'places.mapsSearchPlaceholder': 'Search places...', + 'places.mapsSearchError': 'Place search failed.', + 'places.osmHint': 'Using OpenStreetMap search (no photos, opening hours, or ratings). Add a Google API key in settings for full details.', + 'places.osmActive': 'Search via OpenStreetMap (no photos, ratings or opening hours). Add a Google API key in Settings for enhanced data.', + 'places.categoryCreateError': 'Failed to create category', + 'places.nameRequired': 'Please enter a name', + 'places.saveError': 'Failed to save', + // Place Inspector + 'inspector.opened': 'Open', + 'inspector.closed': 'Closed', + 'inspector.openingHours': 'Opening Hours', + 'inspector.showHours': 'Show opening hours', + 'inspector.files': 'Files', + 'inspector.filesCount': '{count} files', + 'inspector.removeFromDay': 'Remove from Day', + 'inspector.addToDay': 'Add to Day', + 'inspector.confirmedRes': 'Confirmed Reservation', + 'inspector.pendingRes': 'Pending Reservation', + 'inspector.google': 'Open in Google Maps', + 'inspector.website': 'Open Website', + 'inspector.addRes': 'Reservation', + 'inspector.editRes': 'Edit Reservation', + 'inspector.participants': 'Participants', + 'inspector.trackStats': 'Track Stats', + + // Reservations + 'reservations.title': 'Bookings', + 'reservations.empty': 'No reservations yet', + 'reservations.emptyHint': 'Add reservations for flights, hotels and more', + 'reservations.add': 'Add Reservation', + 'reservations.addManual': 'Manual Booking', + 'reservations.placeHint': 'Tip: Reservations are best created directly from a place to link them with your day plan.', + 'reservations.confirmed': 'Confirmed', + 'reservations.pending': 'Pending', + 'reservations.summary': '{confirmed} confirmed, {pending} pending', + 'reservations.fromPlan': 'From Plan', + 'reservations.showFiles': 'Show Files', + 'reservations.editTitle': 'Edit Reservation', + 'reservations.status': 'Status', + 'reservations.datetime': 'Date & Time', + 'reservations.startTime': 'Start time', + 'reservations.endTime': 'End time', + 'reservations.date': 'Date', + 'reservations.time': 'Time', + 'reservations.timeAlt': 'Time (alternative, e.g. 19:30)', + 'reservations.notes': 'Notes', + 'reservations.notesPlaceholder': 'Additional notes...', + 'reservations.meta.airline': 'Airline', + 'reservations.meta.flightNumber': 'Flight No.', + 'reservations.meta.from': 'From', + 'reservations.meta.to': 'To', + 'reservations.meta.trainNumber': 'Train No.', + 'reservations.meta.platform': 'Platform', + 'reservations.meta.seat': 'Seat', + 'reservations.meta.checkIn': 'Check-in', + 'reservations.meta.checkOut': 'Check-out', + 'reservations.meta.linkAccommodation': 'Accommodation', + 'reservations.meta.pickAccommodation': 'Link to accommodation', + 'reservations.meta.noAccommodation': 'None', + 'reservations.meta.hotelPlace': 'Accommodation', + 'reservations.meta.pickHotel': 'Select accommodation', + 'reservations.meta.fromDay': 'From', + 'reservations.meta.toDay': 'To', + 'reservations.meta.selectDay': 'Select day', + 'reservations.type.flight': 'Flight', + 'reservations.type.hotel': 'Accommodation', + 'reservations.type.restaurant': 'Restaurant', + 'reservations.type.train': 'Train', + 'reservations.type.car': 'Rental Car', + 'reservations.type.cruise': 'Cruise', + 'reservations.type.event': 'Event', + 'reservations.type.tour': 'Tour', + 'reservations.type.other': 'Other', + 'reservations.confirm.delete': 'Are you sure you want to delete the reservation "{name}"?', + 'reservations.confirm.deleteTitle': 'Delete booking?', + 'reservations.confirm.deleteBody': '"{name}" will be permanently deleted.', + 'reservations.toast.updated': 'Reservation updated', + 'reservations.toast.removed': 'Reservation deleted', + 'reservations.toast.fileUploaded': 'File uploaded', + 'reservations.toast.uploadError': 'Failed to upload', + 'reservations.newTitle': 'New Reservation', + 'reservations.bookingType': 'Booking Type', + 'reservations.titleLabel': 'Title', + 'reservations.titlePlaceholder': 'e.g. Lufthansa LH123, Hotel Adlon, ...', + 'reservations.locationAddress': 'Location / Address', + 'reservations.locationPlaceholder': 'Address, Airport, Hotel...', + 'reservations.confirmationCode': 'Booking Code', + 'reservations.confirmationPlaceholder': 'e.g. ABC12345', + 'reservations.day': 'Day', + 'reservations.noDay': 'No Day', + 'reservations.place': 'Place', + 'reservations.noPlace': 'No Place', + 'reservations.pendingSave': 'will be saved…', + 'reservations.uploading': 'Uploading...', + 'reservations.attachFile': 'Attach file', + 'reservations.linkExisting': 'Link existing file', + 'reservations.toast.saveError': 'Failed to save', + 'reservations.toast.updateError': 'Failed to update', + 'reservations.toast.deleteError': 'Failed to delete', + 'reservations.confirm.remove': 'Remove reservation for "{name}"?', + 'reservations.linkAssignment': 'Link to day assignment', + 'reservations.pickAssignment': 'Select an assignment from your plan...', + 'reservations.noAssignment': 'No link (standalone)', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', + 'reservations.departureDate': 'Departure', + 'reservations.arrivalDate': 'Arrival', + 'reservations.departureTime': 'Dep. time', + 'reservations.arrivalTime': 'Arr. time', + 'reservations.pickupDate': 'Pickup', + 'reservations.returnDate': 'Return', + 'reservations.pickupTime': 'Pickup time', + 'reservations.returnTime': 'Return time', + 'reservations.endDate': 'End date', + 'reservations.meta.departureTimezone': 'Dep. TZ', + 'reservations.meta.arrivalTimezone': 'Arr. TZ', + 'reservations.span.departure': 'Departure', + 'reservations.span.arrival': 'Arrival', + 'reservations.span.inTransit': 'In transit', + 'reservations.span.pickup': 'Pickup', + 'reservations.span.return': 'Return', + 'reservations.span.active': 'Active', + 'reservations.span.start': 'Start', + 'reservations.span.end': 'End', + 'reservations.span.ongoing': 'Ongoing', + 'reservations.validation.endBeforeStart': 'End date/time must be after start date/time', + + // Budget + 'budget.title': 'Budget', + 'budget.exportCsv': 'Export CSV', + 'budget.emptyTitle': 'No budget created yet', + 'budget.emptyText': 'Create categories and entries to plan your travel budget', + 'budget.emptyPlaceholder': 'Enter category name...', + 'budget.createCategory': 'Create Category', + 'budget.category': 'Category', + 'budget.categoryName': 'Category Name', + 'budget.table.name': 'Name', + 'budget.table.total': 'Total', + 'budget.table.persons': 'Persons', + 'budget.table.days': 'Days', + 'budget.table.perPerson': 'Per Person', + 'budget.table.perDay': 'Per Day', + 'budget.table.perPersonDay': 'P. p / Day', + 'budget.table.note': 'Note', + 'budget.table.date': 'Date', + 'budget.newEntry': 'New Entry', + 'budget.defaultEntry': 'New Entry', + 'budget.defaultCategory': 'New Category', + 'budget.total': 'Total', + 'budget.totalBudget': 'Total Budget', + 'budget.byCategory': 'By Category', + 'budget.editTooltip': 'Click to edit', + 'budget.linkedToReservation': 'Linked to a reservation — edit the name there', + 'budget.confirm.deleteCategory': 'Are you sure you want to delete the category "{name}" with {count} entries?', + 'budget.deleteCategory': 'Delete Category', + 'budget.perPerson': 'Per Person', + 'budget.paid': 'Paid', + 'budget.open': 'Open', + 'budget.noMembers': 'No members assigned', + 'budget.settlement': 'Settlement', + 'budget.settlementInfo': 'Click a member avatar on a budget item to mark them green — this means they paid. The settlement then shows who owes whom and how much.', + 'budget.netBalances': 'Net Balances', + + // Files + 'files.title': 'Files', + 'files.count': '{count} files', + 'files.countSingular': '1 file', + 'files.uploaded': '{count} uploaded', + 'files.uploadError': 'Upload failed', + 'files.dropzone': 'Drop files here', + 'files.dropzoneHint': 'or click to browse', + 'files.allowedTypes': 'Images, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Max 50 MB', + 'files.uploading': 'Uploading...', + 'files.filterAll': 'All', + 'files.filterPdf': 'PDFs', + 'files.filterImages': 'Images', + 'files.filterDocs': 'Documents', + 'files.filterCollab': 'Collab Notes', + 'files.sourceCollab': 'From Collab Notes', + 'files.empty': 'No files yet', + 'files.emptyHint': 'Upload files to attach them to your trip', + 'files.openTab': 'Open in new tab', + 'files.confirm.delete': 'Are you sure you want to delete this file?', + 'files.toast.deleted': 'File deleted', + 'files.toast.deleteError': 'Failed to delete file', + 'files.sourcePlan': 'Day Plan', + 'files.sourceBooking': 'Booking', + 'files.attach': 'Attach', + 'files.pasteHint': 'You can also paste images from clipboard (Ctrl+V)', + 'files.trash': 'Trash', + 'files.trashEmpty': 'Trash is empty', + 'files.emptyTrash': 'Empty Trash', + 'files.restore': 'Restore', + 'files.star': 'Star', + 'files.unstar': 'Unstar', + 'files.assign': 'Assign', + 'files.assignTitle': 'Assign File', + 'files.assignPlace': 'Place', + 'files.assignBooking': 'Booking', + 'files.unassigned': 'Unassigned', + 'files.unlink': 'Remove link', + 'files.toast.trashed': 'Moved to trash', + 'files.toast.restored': 'File restored', + 'files.toast.trashEmptied': 'Trash emptied', + 'files.toast.assigned': 'File assigned', + 'files.toast.assignError': 'Assignment failed', + 'files.toast.restoreError': 'Restore failed', + 'files.confirm.permanentDelete': 'Permanently delete this file? This cannot be undone.', + 'files.confirm.emptyTrash': 'Permanently delete all trashed files? This cannot be undone.', + 'files.noteLabel': 'Note', + 'files.notePlaceholder': 'Add a note...', + + // Packing + 'packing.title': 'Packing List', + 'packing.empty': 'Packing list is empty', + 'packing.import': 'Import', + 'packing.importTitle': 'Import Packing List', + 'packing.importHint': 'One item per line. Format: Category, Name, Weight in g (optional), Bag (optional), checked/unchecked (optional)', + 'packing.importPlaceholder': 'Hygiene, Toothbrush\nClothing, T-Shirts, 200\nDocuments, Passport, , Carry-on\nElectronics, Charger, 50, Suitcase, checked', + 'packing.importCsv': 'Load CSV/TXT', + 'packing.importAction': 'Import {count}', + 'packing.importSuccess': '{count} items imported', + 'packing.importError': 'Import failed', + 'packing.importEmpty': 'No items to import', + 'packing.progress': '{packed} of {total} packed ({percent}%)', + 'packing.clearChecked': 'Remove {count} checked', + 'packing.clearCheckedShort': 'Remove {count}', + 'packing.suggestions': 'Suggestions', + 'packing.suggestionsTitle': 'Add Suggestions', + 'packing.allSuggested': 'All suggestions added', + 'packing.allPacked': 'All packed!', + 'packing.addPlaceholder': 'Add new item...', + 'packing.categoryPlaceholder': 'Category...', + 'packing.filterAll': 'All', + 'packing.filterOpen': 'Open', + 'packing.filterDone': 'Done', + 'packing.emptyTitle': 'Packing list is empty', + 'packing.emptyHint': 'Add items or use the suggestions', + 'packing.emptyFiltered': 'No items match this filter', + 'packing.menuRename': 'Rename', + '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...', + 'packing.addCategory': 'Add category', + 'packing.newCategoryPlaceholder': 'Category name (e.g. Clothing)', + 'packing.applyTemplate': 'Apply template', + 'packing.template': 'Template', + 'packing.templateApplied': '{count} items added from template', + 'packing.templateError': 'Failed to apply template', + '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', + 'packing.bagName': 'Bag name...', + 'packing.addBag': 'Add bag', + 'packing.changeCategory': 'Change Category', + 'packing.confirm.clearChecked': 'Are you sure you want to remove {count} checked items?', + 'packing.confirm.deleteCat': 'Are you sure you want to delete the category "{name}" with {count} items?', + 'packing.defaultCategory': 'Other', + 'packing.toast.saveError': 'Failed to save', + 'packing.toast.deleteError': 'Failed to delete', + 'packing.toast.renameError': 'Failed to rename', + 'packing.toast.addError': 'Failed to add', + + // Packing suggestions + 'packing.suggestions.items': [ + { name: 'Passport', category: 'Documents' }, + { name: 'ID Card', category: 'Documents' }, + { name: 'Travel Insurance', category: 'Documents' }, + { name: 'Flight Tickets', category: 'Documents' }, + { name: 'Credit Card', category: 'Finances' }, + { name: 'Cash', category: 'Finances' }, + { name: 'Visa', category: 'Documents' }, + { name: 'T-Shirts', category: 'Clothing' }, + { name: 'Pants', category: 'Clothing' }, + { name: 'Underwear', category: 'Clothing' }, + { name: 'Socks', category: 'Clothing' }, + { name: 'Jacket', category: 'Clothing' }, + { name: 'Sleepwear', category: 'Clothing' }, + { name: 'Swimwear', category: 'Clothing' }, + { name: 'Rain Jacket', category: 'Clothing' }, + { name: 'Comfortable Shoes', category: 'Clothing' }, + { name: 'Toothbrush', category: 'Toiletries' }, + { name: 'Toothpaste', category: 'Toiletries' }, + { name: 'Shampoo', category: 'Toiletries' }, + { name: 'Deodorant', category: 'Toiletries' }, + { name: 'Sunscreen', category: 'Toiletries' }, + { name: 'Razor', category: 'Toiletries' }, + { name: 'Charger', category: 'Electronics' }, + { name: 'Power Bank', category: 'Electronics' }, + { name: 'Headphones', category: 'Electronics' }, + { name: 'Travel Adapter', category: 'Electronics' }, + { name: 'Camera', category: 'Electronics' }, + { name: 'Pain Medication', category: 'Health' }, + { name: 'Band-Aids', category: 'Health' }, + { name: 'Disinfectant', category: 'Health' }, + ], + + // Members / Sharing + 'members.shareTrip': 'Share Trip', + 'members.inviteUser': 'Invite User', + 'members.selectUser': 'Select user…', + 'members.invite': 'Invite', + 'members.allHaveAccess': 'All users already have access.', + 'members.access': 'Access', + 'members.person': 'person', + 'members.persons': 'persons', + 'members.you': 'you', + 'members.owner': 'Owner', + 'members.leaveTrip': 'Leave trip', + 'members.removeAccess': 'Remove access', + 'members.confirmLeave': 'Leave trip? You will lose access.', + 'members.confirmRemove': 'Remove access for this user?', + 'members.loadError': 'Failed to load members', + 'members.added': 'added', + 'members.addError': 'Failed to add', + 'members.removed': 'Member removed', + 'members.removeError': 'Failed to remove', + + // Categories (Admin) + 'categories.title': 'Categories', + 'categories.subtitle': 'Manage categories for places', + 'categories.new': 'New Category', + 'categories.empty': 'No categories yet', + 'categories.namePlaceholder': 'Category name', + 'categories.icon': 'Icon', + 'categories.color': 'Color', + 'categories.customColor': 'Choose custom color', + 'categories.preview': 'Preview', + 'categories.defaultName': 'Category', + 'categories.update': 'Update', + 'categories.create': 'Create', + 'categories.confirm.delete': 'Delete category? Places in this category will not be deleted.', + 'categories.toast.loadError': 'Failed to load categories', + 'categories.toast.nameRequired': 'Please enter a name', + 'categories.toast.updated': 'Category updated', + 'categories.toast.created': 'Category created', + 'categories.toast.saveError': 'Failed to save', + 'categories.toast.deleted': 'Category deleted', + 'categories.toast.deleteError': 'Failed to delete', + + // Backup (Admin) + 'backup.title': 'Data Backup', + 'backup.subtitle': 'Database and all uploaded files', + 'backup.refresh': 'Refresh', + 'backup.upload': 'Upload Backup', + 'backup.uploading': 'Uploading…', + 'backup.create': 'Create Backup', + 'backup.creating': 'Creating…', + 'backup.empty': 'No backups yet', + 'backup.createFirst': 'Create first backup', + 'backup.download': 'Download', + 'backup.restore': 'Restore', + 'backup.confirm.restore': 'Restore backup "{name}"?\n\nAll current data will be replaced with the backup.', + 'backup.confirm.uploadRestore': 'Upload and restore backup file "{name}"?\n\nAll current data will be overwritten.', + 'backup.confirm.delete': 'Delete backup "{name}"?', + 'backup.toast.loadError': 'Failed to load backups', + 'backup.toast.created': 'Backup created successfully', + 'backup.toast.createError': 'Failed to create backup', + 'backup.toast.restored': 'Backup restored. Page will reload…', + 'backup.toast.restoreError': 'Failed to restore', + 'backup.toast.uploadError': 'Failed to upload', + 'backup.toast.deleted': 'Backup deleted', + 'backup.toast.deleteError': 'Failed to delete', + 'backup.toast.downloadError': 'Download failed', + 'backup.toast.settingsSaved': 'Auto-backup settings saved', + 'backup.toast.settingsError': 'Failed to save settings', + 'backup.auto.title': 'Auto-Backup', + 'backup.auto.subtitle': 'Automatic backup on a schedule', + 'backup.auto.enable': 'Enable auto-backup', + 'backup.auto.enableHint': 'Backups will be created automatically on the chosen schedule', + 'backup.auto.interval': 'Interval', + 'backup.auto.hour': 'Run at hour', + 'backup.auto.hourHint': 'Server local time ({format} format)', + 'backup.auto.dayOfWeek': 'Day of week', + 'backup.auto.dayOfMonth': 'Day of month', + 'backup.auto.dayOfMonthHint': 'Limited to 1–28 for compatibility with all months', + 'backup.auto.scheduleSummary': 'Schedule', + 'backup.auto.summaryDaily': 'Every day at {hour}:00', + 'backup.auto.summaryWeekly': 'Every {day} at {hour}:00', + 'backup.auto.summaryMonthly': 'Day {day} of every month at {hour}:00', + 'backup.auto.envLocked': 'Docker', + 'backup.auto.envLockedHint': 'Auto-backup is configured via Docker environment variables. To change these settings, update your docker-compose.yml and restart the container.', + 'backup.auto.copyEnv': 'Copy Docker env vars', + 'backup.auto.envCopied': 'Docker env vars copied to clipboard', + 'backup.auto.keepLabel': 'Delete old backups after', + 'backup.dow.sunday': 'Sun', + 'backup.dow.monday': 'Mon', + 'backup.dow.tuesday': 'Tue', + 'backup.dow.wednesday': 'Wed', + 'backup.dow.thursday': 'Thu', + 'backup.dow.friday': 'Fri', + 'backup.dow.saturday': 'Sat', + 'backup.interval.hourly': 'Hourly', + 'backup.interval.daily': 'Daily', + 'backup.interval.weekly': 'Weekly', + 'backup.interval.monthly': 'Monthly', + 'backup.keep.1day': '1 day', + 'backup.keep.3days': '3 days', + 'backup.keep.7days': '7 days', + 'backup.keep.14days': '14 days', + 'backup.keep.30days': '30 days', + 'backup.keep.forever': 'Keep forever', + + // Photos + 'photos.allDays': 'All Days', + 'photos.noPhotos': 'No photos yet', + 'photos.uploadHint': 'Upload your travel photos', + 'photos.clickToSelect': 'or click to select', + 'photos.linkPlace': 'Link Place', + 'photos.noPlace': 'No Place', + 'photos.uploadN': '{n} photo(s) upload', + + // Backup restore modal + 'backup.restoreConfirmTitle': 'Restore Backup?', + 'backup.restoreWarning': 'All current data (trips, places, users, uploads) will be permanently replaced by the backup. This action cannot be undone.', + 'backup.restoreTip': 'Tip: Create a backup of the current state before restoring.', + 'backup.restoreConfirm': 'Yes, restore', + + // PDF + 'pdf.travelPlan': 'Travel Plan', + 'pdf.planned': 'Planned', + 'pdf.costLabel': 'Cost EUR', + 'pdf.preview': 'PDF Preview', + 'pdf.saveAsPdf': 'Save as PDF', + + // Planner + 'planner.places': 'Places', + 'planner.bookings': 'Bookings', + 'planner.packingList': 'Packing List', + 'planner.documents': 'Documents', + 'planner.dayPlan': 'Day Plan', + 'planner.reservations': 'Reservations', + 'planner.minTwoPlaces': 'At least 2 places with coordinates needed', + 'planner.noGeoPlaces': 'No places with coordinates available', + 'planner.routeCalculated': 'Route calculated', + 'planner.routeCalcFailed': 'Route could not be calculated', + 'planner.routeError': 'Error calculating route', + 'planner.routeOptimized': 'Route optimized', + 'planner.reservationUpdated': 'Reservation updated', + 'planner.reservationAdded': 'Reservation added', + 'planner.confirmDeleteReservation': 'Delete reservation?', + 'planner.reservationDeleted': 'Reservation deleted', + 'planner.days': 'Days', + 'planner.allPlaces': 'All Places', + 'planner.totalPlaces': '{n} places total', + 'planner.noDaysPlanned': 'No days planned yet', + 'planner.editTrip': 'Edit trip \u2192', + 'planner.placeOne': '1 place', + 'planner.placeN': '{n} places', + 'planner.addNote': 'Add note', + 'planner.noEntries': 'No entries for this day', + 'planner.addPlace': 'Add place/activity', + 'planner.addPlaceShort': '+ Add place/activity', + 'planner.resPending': 'Reservation pending · ', + 'planner.resConfirmed': 'Reservation confirmed · ', + 'planner.notePlaceholder': 'Note\u2026', + 'planner.noteTimePlaceholder': 'Time (optional)', + 'planner.noteExamplePlaceholder': 'e.g. S3 at 14:30 from central station, ferry from pier 7, lunch break\u2026', + 'planner.totalCost': 'Total cost', + 'planner.searchPlaces': 'Search places\u2026', + 'planner.allCategories': 'All Categories', + 'planner.noPlacesFound': 'No places found', + 'planner.addFirstPlace': 'Add first place', + 'planner.noReservations': 'No reservations', + 'planner.addFirstReservation': 'Add first reservation', + 'planner.new': 'New', + 'planner.addToDay': '+ Day', + 'planner.calculating': 'Calculating\u2026', + 'planner.route': 'Route', + 'planner.optimize': 'Optimize', + 'planner.openGoogleMaps': 'Open in Google Maps', + 'planner.selectDayHint': 'Select a day from the left list to see the day plan', + 'planner.noPlacesForDay': 'No places for this day yet', + 'planner.addPlacesLink': 'Add places \u2192', + 'planner.minTotal': 'min. total', + 'planner.noReservation': 'No reservation', + 'planner.removeFromDay': 'Remove from day', + 'planner.addToThisDay': 'Add to day', + 'planner.overview': 'Overview', + 'planner.noDays': 'No days yet', + 'planner.editTripToAddDays': 'Edit trip to add days', + 'planner.dayCount': '{n} Days', + 'planner.clickToUnlock': 'Click to unlock', + 'planner.keepPosition': 'Keep position during route optimization', + 'planner.dayDetails': 'Day details', + 'planner.dayN': 'Day {n}', + + // Dashboard Stats + 'stats.countries': 'Countries', + 'stats.cities': 'Cities', + 'stats.trips': 'Trips', + 'stats.places': 'Places', + 'stats.worldProgress': 'World Progress', + 'stats.visited': 'visited', + 'stats.remaining': 'remaining', + 'stats.visitedCountries': 'Visited Countries', + + // Day Detail Panel + 'day.precipProb': 'Rain probability', + 'day.precipitation': 'Precipitation', + 'day.wind': 'Wind', + 'day.sunrise': 'Sunrise', + 'day.sunset': 'Sunset', + 'day.hourlyForecast': 'Hourly Forecast', + 'day.climateHint': 'Historical averages — real forecast available within 16 days of this date.', + 'day.noWeather': 'No weather data available. Add a place with coordinates.', + 'day.overview': 'Daily Overview', + 'day.accommodation': 'Accommodation', + 'day.addAccommodation': 'Add accommodation', + 'day.hotelDayRange': 'Apply to days', + 'day.noPlacesForHotel': 'Add places to your trip first', + 'day.allDays': 'All', + 'day.checkIn': 'Check-in', + 'day.checkOut': 'Check-out', + 'day.confirmation': 'Confirmation', + 'day.editAccommodation': 'Edit accommodation', + 'day.reservations': 'Reservations', + + // Photos / Immich + 'memories.title': 'Photos', + 'memories.notConnected': '{provider_name} not connected', + 'memories.notConnectedHint': 'Connect your {provider_name} instance in Settings to be able add photos to this trip.', + 'memories.notConnectedMultipleHint': 'Connect any of these photo providers: {provider_names} in Settings to be able add photos to this trip.', + 'memories.noDates': 'Add dates to your trip to load photos.', + 'memories.noPhotos': 'No photos found', + 'memories.noPhotosHint': 'No photos found in {provider_name} for this trip\'s date range.', + 'memories.photosFound': 'photos', + 'memories.fromOthers': 'from others', + 'memories.sharePhotos': 'Share photos', + 'memories.sharing': 'Sharing', + 'memories.reviewTitle': 'Review your photos', + 'memories.reviewHint': 'Click photos to exclude them from sharing.', + 'memories.shareCount': 'Share {count} photos', + //------------------------- + //todo section + 'memories.providerUrl': 'Server URL', + 'memories.providerApiKey': 'API Key', + 'memories.providerUsername': 'Username', + 'memories.providerPassword': 'Password', + 'memories.testConnection': 'Test connection', + 'memories.testFirst': 'Test connection first', + 'memories.connected': 'Connected', + 'memories.disconnected': 'Not connected', + 'memories.connectionSuccess': 'Connected to {provider_name}', + 'memories.connectionError': 'Could not connect to {provider_name}', + 'memories.saved': '{provider_name} settings saved', + 'memories.saveError': 'Could not save {provider_name} settings', + //------------------------ + 'memories.addPhotos': 'Add photos', + 'memories.linkAlbum': 'Link Album', + 'memories.selectAlbum': 'Select {provider_name} Album', + 'memories.selectAlbumMultiple': 'Select Album', + 'memories.noAlbums': 'No albums found', + 'memories.syncAlbum': 'Sync album', + 'memories.unlinkAlbum': 'Unlink album', + 'memories.photos': 'photos', + 'memories.selectPhotos': 'Select photos from {provider_name}', + 'memories.selectPhotosMultiple': 'Select Photos', + 'memories.selectHint': 'Tap photos to select them.', + 'memories.selected': 'selected', + 'memories.addSelected': 'Add {count} photos', + 'memories.alreadyAdded': 'Added', + 'memories.private': 'Private', + 'memories.stopSharing': 'Stop sharing', + 'memories.oldest': 'Oldest first', + 'memories.newest': 'Newest first', + 'memories.allLocations': 'All locations', + 'memories.tripDates': 'Trip dates', + 'memories.allPhotos': 'All photos', + 'memories.confirmShareTitle': 'Share with trip members?', + 'memories.confirmShareHint': '{count} photos will be visible to all members of this trip. You can make individual photos private later.', + 'memories.confirmShareButton': 'Share photos', + 'memories.error.loadAlbums': 'Failed to load albums', + 'memories.error.linkAlbum': 'Failed to link album', + 'memories.error.unlinkAlbum': 'Failed to unlink album', + 'memories.error.syncAlbum': 'Failed to sync album', + 'memories.error.loadPhotos': 'Failed to load photos', + 'memories.error.addPhotos': 'Failed to add photos', + 'memories.error.removePhoto': 'Failed to remove photo', + 'memories.error.toggleSharing': 'Failed to update sharing', + + // Collab Addon + 'collab.tabs.chat': 'Chat', + 'collab.tabs.notes': 'Notes', + 'collab.tabs.polls': 'Polls', + 'collab.whatsNext.title': "What's Next", + 'collab.whatsNext.today': 'Today', + 'collab.whatsNext.tomorrow': 'Tomorrow', + 'collab.whatsNext.empty': 'No upcoming activities', + 'collab.whatsNext.until': 'to', + 'collab.whatsNext.emptyHint': 'Activities with times will appear here', + 'collab.chat.send': 'Send', + 'collab.chat.placeholder': 'Type a message...', + 'collab.chat.empty': 'Start the conversation', + 'collab.chat.emptyHint': 'Messages are shared with all trip members', + 'collab.chat.emptyDesc': 'Share ideas, plans, and updates with your travel group', + 'collab.chat.today': 'Today', + 'collab.chat.yesterday': 'Yesterday', + 'collab.chat.deletedMessage': 'deleted a message', + 'collab.chat.reply': 'Reply', + 'collab.chat.loadMore': 'Load older messages', + 'collab.chat.justNow': 'just now', + 'collab.chat.minutesAgo': '{n}m ago', + 'collab.chat.hoursAgo': '{n}h ago', + 'collab.notes.title': 'Notes', + 'collab.notes.new': 'New Note', + 'collab.notes.empty': 'No notes yet', + 'collab.notes.emptyHint': 'Start capturing ideas and plans', + 'collab.notes.all': 'All', + 'collab.notes.titlePlaceholder': 'Note title', + 'collab.notes.contentPlaceholder': 'Write something...', + 'collab.notes.categoryPlaceholder': 'Category', + 'collab.notes.newCategory': 'New category...', + 'collab.notes.category': 'Category', + 'collab.notes.noCategory': 'No category', + 'collab.notes.color': 'Color', + 'collab.notes.save': 'Save', + 'collab.notes.cancel': 'Cancel', + 'collab.notes.edit': 'Edit', + 'collab.notes.delete': 'Delete', + 'collab.notes.pin': 'Pin', + 'collab.notes.unpin': 'Unpin', + 'collab.notes.daysAgo': '{n}d ago', + 'collab.notes.categorySettings': 'Manage Categories', + 'collab.notes.create': 'Create', + 'collab.notes.website': 'Website', + 'collab.notes.websitePlaceholder': 'https://...', + 'collab.notes.attachFiles': 'Attach files', + 'collab.notes.noCategoriesYet': 'No categories yet', + 'collab.notes.emptyDesc': 'Create a note to get started', + 'collab.polls.title': 'Polls', + 'collab.polls.new': 'New Poll', + 'collab.polls.empty': 'No polls yet', + 'collab.polls.emptyHint': 'Ask the group and vote together', + 'collab.polls.question': 'Question', + 'collab.polls.questionPlaceholder': 'What should we do?', + 'collab.polls.addOption': '+ Add option', + 'collab.polls.optionPlaceholder': 'Option {n}', + 'collab.polls.create': 'Create Poll', + 'collab.polls.close': 'Close', + 'collab.polls.closed': 'Closed', + 'collab.polls.votes': '{n} votes', + 'collab.polls.vote': '{n} vote', + 'collab.polls.multipleChoice': 'Multiple choice', + 'collab.polls.multiChoice': 'Multiple choice', + 'collab.polls.deadline': 'Deadline', + 'collab.polls.option': 'Option', + 'collab.polls.options': 'Options', + 'collab.polls.delete': 'Delete', + 'collab.polls.closedSection': 'Closed', + + // Permissions + 'admin.tabs.permissions': 'Permissions', + 'perm.title': 'Permission Settings', + 'perm.subtitle': 'Control who can perform actions across the application', + 'perm.saved': 'Permission settings saved', + 'perm.resetDefaults': 'Reset to defaults', + 'perm.customized': 'customized', + 'perm.level.admin': 'Admin only', + 'perm.level.tripOwner': 'Trip owner', + 'perm.level.tripMember': 'Trip members', + 'perm.level.everybody': 'Everyone', + 'perm.cat.trip': 'Trip Management', + 'perm.cat.members': 'Member Management', + 'perm.cat.files': 'Files', + 'perm.cat.content': 'Content & Schedule', + 'perm.cat.extras': 'Budget, Packing & Collaboration', + 'perm.action.trip_create': 'Create trips', + 'perm.action.trip_edit': 'Edit trip details', + 'perm.action.trip_delete': 'Delete trips', + 'perm.action.trip_archive': 'Archive / unarchive trips', + 'perm.action.trip_cover_upload': 'Upload cover image', + 'perm.action.member_manage': 'Add / remove members', + 'perm.action.file_upload': 'Upload files', + 'perm.action.file_edit': 'Edit file metadata', + 'perm.action.file_delete': 'Delete files', + 'perm.action.place_edit': 'Add / edit / delete places', + 'perm.action.day_edit': 'Edit days, notes & assignments', + 'perm.action.reservation_edit': 'Manage reservations', + 'perm.action.budget_edit': 'Manage budget', + 'perm.action.packing_edit': 'Manage packing lists', + 'perm.action.collab_edit': 'Collaboration (notes, polls, chat)', + 'perm.action.share_manage': 'Manage share links', + 'perm.actionHint.trip_create': 'Who can create new trips', + 'perm.actionHint.trip_edit': 'Who can change trip name, dates, description and currency', + 'perm.actionHint.trip_delete': 'Who can permanently delete a trip', + 'perm.actionHint.trip_archive': 'Who can archive or unarchive a trip', + 'perm.actionHint.trip_cover_upload': 'Who can upload or change the cover image', + 'perm.actionHint.member_manage': 'Who can invite or remove trip members', + 'perm.actionHint.file_upload': 'Who can upload files to a trip', + 'perm.actionHint.file_edit': 'Who can edit file descriptions and links', + 'perm.actionHint.file_delete': 'Who can move files to trash or permanently delete them', + 'perm.actionHint.place_edit': 'Who can add, edit or delete places', + 'perm.actionHint.day_edit': 'Who can edit days, day notes and place assignments', + 'perm.actionHint.reservation_edit': 'Who can create, edit or delete reservations', + 'perm.actionHint.budget_edit': 'Who can create, edit or delete budget items', + 'perm.actionHint.packing_edit': 'Who can manage packing items and bags', + 'perm.actionHint.collab_edit': 'Who can create notes, polls and send messages', + 'perm.actionHint.share_manage': 'Who can create or delete public share links', + + // Undo + 'undo.button': 'Undo', + 'undo.tooltip': 'Undo: {action}', + 'undo.assignPlace': 'Place assigned to day', + 'undo.removeAssignment': 'Place removed from day', + 'undo.reorder': 'Places reordered', + 'undo.optimize': 'Route optimized', + 'undo.deletePlace': 'Place deleted', + 'undo.moveDay': 'Place moved to another day', + 'undo.lock': 'Place lock toggled', + 'undo.importGpx': 'GPX import', + 'undo.importGoogleList': 'Google Maps import', + 'undo.addPlace': 'Place added', + 'undo.done': 'Undone: {action}', + + // Notifications + 'notifications.title': 'Notifications', + 'notifications.markAllRead': 'Mark all read', + 'notifications.deleteAll': 'Delete all', + 'notifications.showAll': 'Show all notifications', + 'notifications.empty': 'No notifications', + 'notifications.emptyDescription': "You're all caught up!", + 'notifications.all': 'All', + 'notifications.unreadOnly': 'Unread', + 'notifications.markRead': 'Mark as read', + 'notifications.markUnread': 'Mark as unread', + 'notifications.delete': 'Delete', + 'notifications.system': 'System', + + // Notification test keys (dev only) + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', + 'notifications.test.title': 'Test notification from {actor}', + 'notifications.test.text': 'This is a simple test notification.', + 'notifications.test.booleanTitle': '{actor} asks for your approval', + 'notifications.test.booleanText': 'This is a test boolean notification. Choose an action below.', + 'notifications.test.accept': 'Approve', + 'notifications.test.decline': 'Decline', + 'notifications.test.navigateTitle': 'Check something out', + 'notifications.test.navigateText': 'This is a test navigate notification.', + 'notifications.test.goThere': 'Go there', + 'notifications.test.adminTitle': 'Admin broadcast', + 'notifications.test.adminText': '{actor} sent a test notification to all admins.', + 'notifications.test.tripTitle': '{actor} posted in your trip', + 'notifications.test.tripText': 'Test notification for trip "{trip}".', + + // Todo + 'todo.subtab.packing': 'Packing List', + 'todo.subtab.todo': 'To-Do', + 'todo.completed': 'completed', + 'todo.filter.all': 'All', + 'todo.filter.open': 'Open', + 'todo.filter.done': 'Done', + 'todo.uncategorized': 'Uncategorized', + 'todo.namePlaceholder': 'Task name', + 'todo.descriptionPlaceholder': 'Description (optional)', + 'todo.unassigned': 'Unassigned', + 'todo.noCategory': 'No category', + 'todo.hasDescription': 'Has description', + 'todo.addItem': 'Add new task...', + 'todo.newCategory': 'Category name', + 'todo.addCategory': 'Add category', + 'todo.newItem': 'New task', + 'todo.empty': 'No tasks yet. Add a task to get started!', + 'todo.filter.my': 'My Tasks', + 'todo.filter.overdue': 'Overdue', + 'todo.sidebar.tasks': 'Tasks', + 'todo.sidebar.categories': 'Categories', + 'todo.detail.title': 'Task', + 'todo.detail.description': 'Description', + 'todo.detail.category': 'Category', + 'todo.detail.dueDate': 'Due date', + 'todo.detail.assignedTo': 'Assigned to', + 'todo.detail.delete': 'Delete', + 'todo.detail.save': 'Save changes', + 'todo.sortByPrio': 'Priority', + 'todo.detail.priority': 'Priority', + 'todo.detail.noPriority': 'None', + 'todo.detail.create': 'Create task', + + // Notifications — dev test events + 'notif.test.title': '[Test] Notification', + 'notif.test.simple.text': 'This is a simple test notification.', + 'notif.test.boolean.text': 'Do you accept this test notification?', + 'notif.test.navigate.text': 'Click below to navigate to the dashboard.', + + // Notifications + 'notif.trip_invite.title': 'Trip Invitation', + 'notif.trip_invite.text': '{actor} invited you to {trip}', + 'notif.booking_change.title': 'Booking Updated', + 'notif.booking_change.text': '{actor} updated a booking in {trip}', + 'notif.trip_reminder.title': 'Trip Reminder', + 'notif.trip_reminder.text': 'Your trip {trip} is coming up soon!', + 'notif.vacay_invite.title': 'Vacay Fusion Invite', + 'notif.vacay_invite.text': '{actor} invited you to fuse vacation plans', + 'notif.photos_shared.title': 'Photos Shared', + 'notif.photos_shared.text': '{actor} shared {count} photo(s) in {trip}', + 'notif.collab_message.title': 'New Message', + 'notif.collab_message.text': '{actor} sent a message in {trip}', + 'notif.packing_tagged.title': 'Packing Assignment', + 'notif.packing_tagged.text': '{actor} assigned you to {category} in {trip}', + 'notif.version_available.title': 'New Version Available', + 'notif.version_available.text': 'TREK {version} is now available', + 'notif.action.view_trip': 'View Trip', + 'notif.action.view_collab': 'View Messages', + 'notif.action.view_packing': 'View Packing', + 'notif.action.view_photos': 'View Photos', + 'notif.action.view_vacay': 'View Vacay', + 'notif.action.view_admin': 'Go to Admin', + 'notif.action.view': 'View', + 'notif.action.accept': 'Accept', + 'notif.action.decline': 'Decline', + 'notif.generic.title': 'Notification', + 'notif.generic.text': 'You have a new notification', + 'notif.dev.unknown_event.title': '[DEV] Unknown Event', + 'notif.dev.unknown_event.text': 'Event type "{event}" is not registered in EVENT_NOTIFICATION_CONFIG', +} + +export default en diff --git a/server/src/services/notifications.ts b/server/src/services/notifications.ts index b4d3a26e..b0468959 100644 --- a/server/src/services/notifications.ts +++ b/server/src/services/notifications.ts @@ -88,6 +88,7 @@ const I18N: Record = { zh: { footer: '您收到此邮件是因为您在 TREK 中启用了通知。', manage: '管理偏好设置', madeWith: 'Made with', openTrek: '打开 TREK' }, 'zh-TW': { footer: '您收到這封郵件是因為您在 TREK 中啟用了通知。', manage: '管理偏好設定', madeWith: 'Made with', openTrek: '開啟 TREK' }, ar: { footer: 'تلقيت هذا لأنك قمت بتفعيل الإشعارات في TREK.', manage: 'إدارة التفضيلات', madeWith: 'Made with', openTrek: 'فتح TREK' }, + id: { footer: 'Anda menerima ini karena Anda telah mengaktifkan notifikasi di TREK.', manage: 'Kelola preferensi di Pengaturan', madeWith: 'Dibuat dengan', openTrek: 'Buka TREK' }, }; // Translated notification texts per event type @@ -234,6 +235,16 @@ const EVENT_TEXTS: Record> = { packing_tagged: p => ({ title: `Pakowanie: ${p.category}`, body: `${p.actor} przypisał Cię do kategorii "${p.category}" w "${p.trip}".` }), version_available: p => ({ title: 'Nowa wersja TREK dostępna', body: `TREK ${p.version} jest teraz dostępny. Odwiedź panel administracyjny, aby zaktualizować.` }), }, + id: { + trip_invite: p => ({ title: `Undangan perjalanan: "${p.trip}"`, body: `${p.actor} mengundang ${p.invitee || 'seorang anggota'} ke perjalanan "${p.trip}".` }), + booking_change: p => ({ title: `Pemesanan baru: ${p.booking}`, body: `${p.actor} menambahkan "${p.booking}" (${p.type}) baru ke "${p.trip}".` }), + trip_reminder: p => ({ title: `Pengingat perjalanan: ${p.trip}`, body: `Perjalanan Anda "${p.trip}" akan segera tiba!` }), + vacay_invite: p => ({ title: 'Undangan Penggabungan Vacay', body: `${p.actor} mengundang Anda untuk menggabungkan rencana liburan. Buka TREK untuk menerima atau menolak.` }), + photos_shared: p => ({ title: `${p.count} foto dibagikan`, body: `${p.actor} membagikan ${p.count} foto di "${p.trip}".` }), + collab_message: p => ({ title: `Pesan baru di "${p.trip}"`, body: `${p.actor}: ${p.preview}` }), + packing_tagged: p => ({ title: `Pengepakan: ${p.category}`, body: `${p.actor} menugaskan Anda ke kategori "${p.category}" di "${p.trip}".` }), + version_available: p => ({ title: 'Versi TREK baru tersedia', body: `TREK ${p.version} sekarang tersedia. Kunjungi panel admin untuk memperbarui.` }), + }, }; // Get localized event text