diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 170a6f5..679cb16 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -168,20 +168,7 @@ const ar: Record = { 'settings.notifyCollabMessage': 'رسائل الدردشة (Collab)', 'settings.notifyPackingTagged': 'قائمة الأمتعة: التعيينات', 'settings.notifyWebhook': 'إشعارات Webhook', - 'settings.notifyVersionAvailable': 'New version available', 'settings.notificationsDisabled': 'الإشعارات غير مكوّنة. اطلب من المسؤول تفعيل إشعارات البريد الإلكتروني أو Webhook.', - 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', - 'settings.webhookUrl.label': 'رابط Webhook', - 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', - 'settings.webhookUrl.hint': 'أدخل رابط Webhook الخاص بـ Discord أو Slack أو المخصص لتلقي الإشعارات.', - 'settings.webhookUrl.save': 'حفظ', - 'settings.webhookUrl.saved': 'تم حفظ رابط Webhook', - 'settings.webhookUrl.test': 'اختبار', - 'settings.webhookUrl.testSuccess': 'تم إرسال Webhook الاختباري بنجاح', - 'settings.webhookUrl.testFailed': 'فشل إرسال Webhook الاختباري', - 'settings.notificationPreferences.inapp': 'In-App', - 'settings.notificationPreferences.webhook': 'Webhook', - 'settings.notificationPreferences.email': 'Email', 'settings.notificationsActive': 'القناة النشطة', 'settings.notificationsManagedByAdmin': 'يتم تكوين أحداث الإشعارات بواسطة المسؤول.', 'admin.notifications.title': 'الإشعارات', @@ -197,21 +184,10 @@ const ar: Record = { 'admin.notifications.testWebhook': 'إرسال webhook تجريبي', 'admin.notifications.testWebhookSuccess': 'تم إرسال webhook التجريبي بنجاح', 'admin.notifications.testWebhookFailed': 'فشل إرسال webhook التجريبي', - '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': 'Webhook المسؤول', - 'admin.notifications.adminWebhookPanel.hint': 'يُستخدم هذا الـ Webhook حصريًا لإشعارات المسؤول (مثل تنبيهات الإصدارات). وهو مستقل عن Webhooks المستخدمين ويُرسل تلقائيًا عند تعيين رابط URL.', - 'admin.notifications.adminWebhookPanel.saved': 'تم حفظ رابط Webhook المسؤول', - 'admin.notifications.adminWebhookPanel.testSuccess': 'تم إرسال Webhook الاختباري بنجاح', - 'admin.notifications.adminWebhookPanel.testFailed': 'فشل إرسال Webhook الاختباري', - 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'يُرسل Webhook المسؤول تلقائيًا عند تعيين رابط URL', - 'admin.notifications.adminNotificationsHint': 'حدد القنوات التي تُسلّم إشعارات المسؤول (مثل تنبيهات الإصدارات). يُرسل الـ Webhook تلقائيًا عند تعيين رابط URL لـ Webhook المسؤول.', 'admin.smtp.title': 'البريد والإشعارات', 'admin.smtp.hint': 'تكوين SMTP لإرسال إشعارات البريد الإلكتروني.', 'admin.smtp.testButton': 'إرسال بريد تجريبي', - 'admin.webhook.hint': 'السماح للمستخدمين بتكوين عناوين URL الخاصة بـ Webhook للإشعارات (Discord، Slack، إلخ).', + 'admin.webhook.hint': 'إرسال الإشعارات إلى webhook خارجي (Discord، Slack، إلخ).', 'admin.smtp.testSuccess': 'تم إرسال البريد التجريبي بنجاح', 'admin.smtp.testFailed': 'فشل إرسال البريد التجريبي', 'dayplan.icsTooltip': 'تصدير التقويم (ICS)', @@ -412,9 +388,6 @@ const ar: Record = { 'admin.tabs.users': 'المستخدمون', 'admin.tabs.categories': 'الفئات', 'admin.tabs.backup': 'النسخ الاحتياطي', - 'admin.tabs.notifications': 'Notifications', - 'admin.tabs.notificationChannels': 'قنوات الإشعارات', - 'admin.tabs.adminNotifications': 'إشعارات المسؤول', 'admin.tabs.audit': 'سجل التدقيق', 'admin.tabs.settings': 'الإعدادات', 'admin.tabs.config': 'الإعدادات', @@ -717,8 +690,10 @@ const ar: Record = { 'atlas.unmark': 'إزالة', 'atlas.confirmMark': 'تعيين هذا البلد كمُزار؟', 'atlas.confirmUnmark': 'إزالة هذا البلد من قائمة المُزارة؟', + 'atlas.confirmUnmarkRegion': 'إزالة هذه المنطقة من قائمة المُزارة؟', 'atlas.markVisited': 'تعيين كمُزار', 'atlas.markVisitedHint': 'إضافة هذا البلد إلى قائمة المُزارة', + 'atlas.markRegionVisitedHint': 'إضافة هذه المنطقة إلى قائمة المُزارة', 'atlas.addToBucket': 'إضافة إلى قائمة الأمنيات', 'atlas.addPoi': 'إضافة مكان', 'atlas.searchCountry': 'ابحث عن دولة...', @@ -965,6 +940,11 @@ const ar: Record = { 'reservations.linkAssignment': 'ربط بخطة اليوم', 'reservations.pickAssignment': 'اختر عنصرًا من خطتك...', 'reservations.noAssignment': 'بلا ربط', + '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': 'المغادرة', 'reservations.arrivalDate': 'الوصول', 'reservations.departureTime': 'وقت المغادرة', @@ -986,11 +966,6 @@ const ar: Record = { 'reservations.span.end': 'النهاية', 'reservations.span.ongoing': 'جارٍ', 'reservations.validation.endBeforeStart': 'يجب أن يكون تاريخ/وقت الانتهاء بعد تاريخ/وقت البدء', - '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.', // Budget 'budget.title': 'الميزانية', @@ -1586,9 +1561,6 @@ const ar: Record = { 'memories.error.toggleSharing': 'فشل تحديث إعدادات المشاركة', 'undo.addPlace': 'تمت إضافة المكان', 'undo.done': 'تم التراجع: {action}', - 'notifications.versionAvailable.title': 'Update Available', - 'notifications.versionAvailable.text': 'TREK {version} is now available.', - 'notifications.versionAvailable.button': 'View Details', 'notifications.test.title': 'إشعار تجريبي من {actor}', 'notifications.test.text': 'هذا إشعار تجريبي بسيط.', 'notifications.test.booleanTitle': 'يطلب منك {actor} الموافقة', @@ -1637,7 +1609,37 @@ const ar: Record = { 'todo.detail.noPriority': 'None', 'todo.sortByPrio': 'Priority', - // Notifications — dev test events + // Notification system (added from feat/notification-system) + 'settings.notifyVersionAvailable': 'New version available', + 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', + 'settings.webhookUrl.label': 'رابط Webhook', + 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', + 'settings.webhookUrl.hint': 'أدخل رابط Webhook الخاص بـ Discord أو Slack أو المخصص لتلقي الإشعارات.', + 'settings.webhookUrl.save': 'حفظ', + 'settings.webhookUrl.saved': 'تم حفظ رابط Webhook', + 'settings.webhookUrl.test': 'اختبار', + 'settings.webhookUrl.testSuccess': 'تم إرسال Webhook الاختباري بنجاح', + 'settings.webhookUrl.testFailed': 'فشل إرسال Webhook الاختباري', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.email': 'Email', + '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': 'Webhook المسؤول', + 'admin.notifications.adminWebhookPanel.hint': 'يُستخدم هذا الـ Webhook حصريًا لإشعارات المسؤول (مثل تنبيهات الإصدارات). وهو مستقل عن Webhooks المستخدمين ويُرسل تلقائيًا عند تعيين رابط URL.', + 'admin.notifications.adminWebhookPanel.saved': 'تم حفظ رابط Webhook المسؤول', + 'admin.notifications.adminWebhookPanel.testSuccess': 'تم إرسال Webhook الاختباري بنجاح', + 'admin.notifications.adminWebhookPanel.testFailed': 'فشل إرسال Webhook الاختباري', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'يُرسل Webhook المسؤول تلقائيًا عند تعيين رابط URL', + 'admin.notifications.adminNotificationsHint': 'حدد القنوات التي تُسلّم إشعارات المسؤول (مثل تنبيهات الإصدارات). يُرسل الـ Webhook تلقائيًا عند تعيين رابط URL لـ Webhook المسؤول.', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.notificationChannels': 'قنوات الإشعارات', + 'admin.tabs.adminNotifications': 'إشعارات المسؤول', + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', '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?', @@ -1674,6 +1676,7 @@ const ar: Record = { '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 ar diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 9d30671..9810678 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -163,20 +163,7 @@ const br: Record = { 'settings.notifyCollabMessage': 'Mensagens de chat (Colab)', 'settings.notifyPackingTagged': 'Lista de mala: atribuições', 'settings.notifyWebhook': 'Notificações webhook', - 'settings.notifyVersionAvailable': 'New version available', 'settings.notificationsDisabled': 'As notificações não estão configuradas. Peça a um administrador para ativar notificações por e-mail ou webhook.', - 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', - 'settings.webhookUrl.label': 'URL do webhook', - 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', - 'settings.webhookUrl.hint': 'Insira a URL do seu webhook do Discord, Slack ou personalizado para receber notificações.', - 'settings.webhookUrl.save': 'Salvar', - 'settings.webhookUrl.saved': 'URL do webhook salva', - 'settings.webhookUrl.test': 'Testar', - 'settings.webhookUrl.testSuccess': 'Webhook de teste enviado com sucesso', - 'settings.webhookUrl.testFailed': 'Falha no webhook de teste', - 'settings.notificationPreferences.inapp': 'In-App', - 'settings.notificationPreferences.webhook': 'Webhook', - 'settings.notificationPreferences.email': 'Email', 'settings.notificationsActive': 'Canal ativo', 'settings.notificationsManagedByAdmin': 'Os eventos de notificação são configurados pelo administrador.', 'admin.notifications.title': 'Notificações', @@ -192,21 +179,10 @@ const br: Record = { 'admin.notifications.testWebhook': 'Enviar webhook de teste', 'admin.notifications.testWebhookSuccess': 'Webhook de teste enviado com sucesso', 'admin.notifications.testWebhookFailed': 'Falha ao enviar webhook de teste', - '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': 'Webhook de admin', - 'admin.notifications.adminWebhookPanel.hint': 'Este webhook é usado exclusivamente para notificações de admin (ex. alertas de versão). É independente dos webhooks de usuários e dispara automaticamente quando uma URL está configurada.', - 'admin.notifications.adminWebhookPanel.saved': 'URL do webhook de admin salva', - 'admin.notifications.adminWebhookPanel.testSuccess': 'Webhook de teste enviado com sucesso', - 'admin.notifications.adminWebhookPanel.testFailed': 'Falha no webhook de teste', - 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'O webhook de admin dispara automaticamente quando uma URL está configurada', - 'admin.notifications.adminNotificationsHint': 'Configure quais canais entregam notificações de admin (ex. alertas de versão). O webhook dispara automaticamente se uma URL de webhook de admin estiver definida.', 'admin.smtp.title': 'E-mail e notificações', 'admin.smtp.hint': 'Configuração SMTP para envio de notificações por e-mail.', 'admin.smtp.testButton': 'Enviar e-mail de teste', - 'admin.webhook.hint': 'Permitir que os usuários configurem suas próprias URLs de webhook para notificações (Discord, Slack, etc.).', + 'admin.webhook.hint': 'Enviar notificações para um webhook externo (Discord, Slack, etc.).', 'admin.smtp.testSuccess': 'E-mail de teste enviado com sucesso', 'admin.smtp.testFailed': 'Falha ao enviar e-mail de teste', 'dayplan.icsTooltip': 'Exportar calendário (ICS)', @@ -407,9 +383,6 @@ const br: Record = { 'admin.tabs.users': 'Usuários', 'admin.tabs.categories': 'Categorias', 'admin.tabs.backup': 'Backup', - 'admin.tabs.notifications': 'Notifications', - 'admin.tabs.notificationChannels': 'Canais de notificação', - 'admin.tabs.adminNotifications': 'Notificações de admin', 'admin.stats.users': 'Usuários', 'admin.stats.trips': 'Viagens', 'admin.stats.places': 'Lugares', @@ -699,8 +672,10 @@ const br: Record = { 'atlas.unmark': 'Remover', 'atlas.confirmMark': 'Marcar este país como visitado?', 'atlas.confirmUnmark': 'Remover este país da lista de visitados?', + 'atlas.confirmUnmarkRegion': 'Remover esta região da lista de visitados?', 'atlas.markVisited': 'Marcar como visitado', 'atlas.markVisitedHint': 'Adicionar este país à lista de visitados', + 'atlas.markRegionVisitedHint': 'Adicionar esta região à lista de visitados', 'atlas.addToBucket': 'Adicionar à lista de desejos', 'atlas.addPoi': 'Adicionar lugar', 'atlas.searchCountry': 'Buscar um país...', @@ -946,6 +921,11 @@ const br: Record = { 'reservations.linkAssignment': 'Vincular à atribuição do dia', 'reservations.pickAssignment': 'Selecione uma atribuição do seu plano...', 'reservations.noAssignment': 'Sem vínculo (avulsa)', + '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': 'Partida', 'reservations.arrivalDate': 'Chegada', 'reservations.departureTime': 'Hora partida', @@ -967,11 +947,6 @@ const br: Record = { 'reservations.span.end': 'Fim', 'reservations.span.ongoing': 'Em andamento', 'reservations.validation.endBeforeStart': 'A data/hora final deve ser posterior à data/hora inicial', - '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.', // Budget 'budget.title': 'Orçamento', @@ -1581,9 +1556,6 @@ const br: Record = { 'memories.error.toggleSharing': 'Falha ao atualizar compartilhamento', 'undo.addPlace': 'Local adicionado', 'undo.done': 'Desfeito: {action}', - 'notifications.versionAvailable.title': 'Update Available', - 'notifications.versionAvailable.text': 'TREK {version} is now available.', - 'notifications.versionAvailable.button': 'View Details', 'notifications.test.title': 'Notificação de teste de {actor}', 'notifications.test.text': 'Esta é uma notificação de teste simples.', 'notifications.test.booleanTitle': '{actor} solicita sua aprovação', @@ -1632,7 +1604,37 @@ const br: Record = { 'todo.detail.noPriority': 'None', 'todo.sortByPrio': 'Priority', - // Notifications — dev test events + // Notification system (added from feat/notification-system) + 'settings.notifyVersionAvailable': 'New version available', + 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', + 'settings.webhookUrl.label': 'URL do webhook', + 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', + 'settings.webhookUrl.hint': 'Insira a URL do seu webhook do Discord, Slack ou personalizado para receber notificações.', + 'settings.webhookUrl.save': 'Salvar', + 'settings.webhookUrl.saved': 'URL do webhook salva', + 'settings.webhookUrl.test': 'Testar', + 'settings.webhookUrl.testSuccess': 'Webhook de teste enviado com sucesso', + 'settings.webhookUrl.testFailed': 'Falha no webhook de teste', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.email': 'Email', + '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': 'Webhook de admin', + 'admin.notifications.adminWebhookPanel.hint': 'Este webhook é usado exclusivamente para notificações de admin (ex. alertas de versão). É independente dos webhooks de usuários e dispara automaticamente quando uma URL está configurada.', + 'admin.notifications.adminWebhookPanel.saved': 'URL do webhook de admin salva', + 'admin.notifications.adminWebhookPanel.testSuccess': 'Webhook de teste enviado com sucesso', + 'admin.notifications.adminWebhookPanel.testFailed': 'Falha no webhook de teste', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'O webhook de admin dispara automaticamente quando uma URL está configurada', + 'admin.notifications.adminNotificationsHint': 'Configure quais canais entregam notificações de admin (ex. alertas de versão). O webhook dispara automaticamente se uma URL de webhook de admin estiver definida.', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.notificationChannels': 'Canais de notificação', + 'admin.tabs.adminNotifications': 'Notificações de admin', + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', '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?', @@ -1669,6 +1671,7 @@ const br: Record = { '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 br diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 51c3849..c234c3c 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -164,20 +164,7 @@ const cs: Record = { 'settings.notifyCollabMessage': 'Zprávy v chatu (Collab)', 'settings.notifyPackingTagged': 'Seznam balení: přiřazení', 'settings.notifyWebhook': 'Webhook oznámení', - 'settings.notifyVersionAvailable': 'New version available', 'settings.notificationsDisabled': 'Oznámení nejsou nakonfigurována. Požádejte správce o aktivaci e-mailových nebo webhookových oznámení.', - 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', - 'settings.webhookUrl.label': 'URL webhooku', - 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', - 'settings.webhookUrl.hint': 'Zadejte URL vašeho Discord, Slack nebo vlastního webhooku pro příjem oznámení.', - 'settings.webhookUrl.save': 'Uložit', - 'settings.webhookUrl.saved': 'URL webhooku uložena', - 'settings.webhookUrl.test': 'Otestovat', - 'settings.webhookUrl.testSuccess': 'Testovací webhook byl úspěšně odeslán', - 'settings.webhookUrl.testFailed': 'Testovací webhook selhal', - 'settings.notificationPreferences.inapp': 'In-App', - 'settings.notificationPreferences.webhook': 'Webhook', - 'settings.notificationPreferences.email': 'Email', 'settings.notificationsActive': 'Aktivní kanál', 'settings.notificationsManagedByAdmin': 'Události oznámení jsou konfigurovány administrátorem.', 'settings.on': 'Zapnuto', @@ -279,21 +266,10 @@ const cs: Record = { 'admin.notifications.testWebhook': 'Odeslat testovací webhook', 'admin.notifications.testWebhookSuccess': 'Testovací webhook úspěšně odeslán', 'admin.notifications.testWebhookFailed': 'Odeslání testovacího webhooku se nezdařilo', - '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': 'Tento webhook se používá výhradně pro admin oznámení (např. upozornění na verze). Je nezávislý na uživatelských webhooků a odesílá automaticky, pokud je nastavena URL.', - 'admin.notifications.adminWebhookPanel.saved': 'URL admin webhooku uložena', - 'admin.notifications.adminWebhookPanel.testSuccess': 'Testovací webhook byl úspěšně odeslán', - 'admin.notifications.adminWebhookPanel.testFailed': 'Testovací webhook selhal', - 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Admin webhook odesílá automaticky, pokud je nastavena URL', - 'admin.notifications.adminNotificationsHint': 'Nastavte, které kanály doručují admin oznámení (např. upozornění na verze). Webhook odesílá automaticky, pokud je nastavena URL admin webhooku.', 'admin.smtp.title': 'E-mail a oznámení', 'admin.smtp.hint': 'Konfigurace SMTP pro odesílání e-mailových oznámení.', 'admin.smtp.testButton': 'Odeslat testovací e-mail', - 'admin.webhook.hint': 'Umožnit uživatelům nakonfigurovat vlastní URL webhooku pro oznámení (Discord, Slack atd.).', + 'admin.webhook.hint': 'Odesílat oznámení na externí webhook (Discord, Slack atd.).', 'admin.smtp.testSuccess': 'Testovací e-mail byl úspěšně odeslán', 'admin.smtp.testFailed': 'Odeslání testovacího e-mailu se nezdařilo', 'dayplan.icsTooltip': 'Exportovat kalendář (ICS)', @@ -407,9 +383,6 @@ const cs: Record = { 'admin.tabs.users': 'Uživatelé', 'admin.tabs.categories': 'Kategorie', 'admin.tabs.backup': 'Zálohování', - 'admin.tabs.notifications': 'Notifications', - 'admin.tabs.notificationChannels': 'Kanály oznámení', - 'admin.tabs.adminNotifications': 'Admin oznámení', 'admin.stats.users': 'Uživatelé', 'admin.stats.trips': 'Cesty', 'admin.stats.places': 'Místa', @@ -716,8 +689,10 @@ const cs: Record = { 'atlas.unmark': 'Odebrat', 'atlas.confirmMark': 'Označit tuto zemi jako navštívenou?', 'atlas.confirmUnmark': 'Odebrat tuto zemi ze seznamu navštívených?', + 'atlas.confirmUnmarkRegion': 'Odebrat tento region ze seznamu navštívených?', 'atlas.markVisited': 'Označit jako navštívené', 'atlas.markVisitedHint': 'Přidat tuto zemi do seznamu navštívených', + 'atlas.markRegionVisitedHint': 'Přidat tento region do seznamu navštívených', 'atlas.addToBucket': 'Přidat do seznamu přání (Bucket list)', 'atlas.addPoi': 'Přidat místo', 'atlas.bucketNamePlaceholder': 'Název (země, město, místo...)', @@ -963,6 +938,11 @@ const cs: Record = { 'reservations.linkAssignment': 'Propojit s přiřazením dne', 'reservations.pickAssignment': 'Vyberte přiřazení z vašeho plánu...', 'reservations.noAssignment': 'Bez propojení (samostatné)', + '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': 'Odlet', 'reservations.arrivalDate': 'Přílet', 'reservations.departureTime': 'Čas odletu', @@ -984,11 +964,6 @@ const cs: Record = { 'reservations.span.end': 'Konec', 'reservations.span.ongoing': 'Probíhá', 'reservations.validation.endBeforeStart': 'Datum/čas konce musí být po datu/čase začátku', - '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.', // Rozpočet (Budget) 'budget.title': 'Rozpočet', @@ -1586,9 +1561,6 @@ const cs: Record = { 'memories.error.toggleSharing': 'Aktualizace sdílení se nezdařila', 'undo.addPlace': 'Místo přidáno', 'undo.done': 'Vráceno zpět: {action}', - 'notifications.versionAvailable.title': 'Update Available', - 'notifications.versionAvailable.text': 'TREK {version} is now available.', - 'notifications.versionAvailable.button': 'View Details', 'notifications.test.title': 'Testovací oznámení od {actor}', 'notifications.test.text': 'Toto je jednoduché testovací oznámení.', 'notifications.test.booleanTitle': '{actor} žádá o vaše schválení', @@ -1637,7 +1609,37 @@ const cs: Record = { 'todo.detail.noPriority': 'None', 'todo.sortByPrio': 'Priority', - // Notifications — dev test events + // Notification system (added from feat/notification-system) + 'settings.notifyVersionAvailable': 'New version available', + 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', + 'settings.webhookUrl.label': 'URL webhooku', + 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', + 'settings.webhookUrl.hint': 'Zadejte URL vašeho Discord, Slack nebo vlastního webhooku pro příjem oznámení.', + 'settings.webhookUrl.save': 'Uložit', + 'settings.webhookUrl.saved': 'URL webhooku uložena', + 'settings.webhookUrl.test': 'Otestovat', + 'settings.webhookUrl.testSuccess': 'Testovací webhook byl úspěšně odeslán', + 'settings.webhookUrl.testFailed': 'Testovací webhook selhal', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.email': 'Email', + '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': 'Tento webhook se používá výhradně pro admin oznámení (např. upozornění na verze). Je nezávislý na uživatelských webhooků a odesílá automaticky, pokud je nastavena URL.', + 'admin.notifications.adminWebhookPanel.saved': 'URL admin webhooku uložena', + 'admin.notifications.adminWebhookPanel.testSuccess': 'Testovací webhook byl úspěšně odeslán', + 'admin.notifications.adminWebhookPanel.testFailed': 'Testovací webhook selhal', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Admin webhook odesílá automaticky, pokud je nastavena URL', + 'admin.notifications.adminNotificationsHint': 'Nastavte, které kanály doručují admin oznámení (např. upozornění na verze). Webhook odesílá automaticky, pokud je nastavena URL admin webhooku.', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.notificationChannels': 'Kanály oznámení', + 'admin.tabs.adminNotifications': 'Admin oznámení', + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', '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?', @@ -1674,6 +1676,7 @@ const cs: Record = { '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 cs diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index f1b3e83..4c028ed 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -163,20 +163,7 @@ const de: Record = { 'settings.notifyCollabMessage': 'Chat-Nachrichten (Collab)', 'settings.notifyPackingTagged': 'Packliste: Zuweisungen', 'settings.notifyWebhook': 'Webhook-Benachrichtigungen', - 'settings.notifyVersionAvailable': 'New version available', 'settings.notificationsDisabled': 'Benachrichtigungen sind nicht konfiguriert. Bitten Sie einen Administrator, E-Mail- oder Webhook-Benachrichtungen zu aktivieren.', - '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': 'Gib deine Discord-, Slack- oder benutzerdefinierte Webhook-URL ein, um Benachrichtigungen zu erhalten.', - 'settings.webhookUrl.save': 'Speichern', - 'settings.webhookUrl.saved': 'Webhook-URL gespeichert', - 'settings.webhookUrl.test': 'Testen', - 'settings.webhookUrl.testSuccess': 'Test-Webhook erfolgreich gesendet', - 'settings.webhookUrl.testFailed': 'Test-Webhook fehlgeschlagen', - 'settings.notificationPreferences.inapp': 'In-App', - 'settings.notificationPreferences.webhook': 'Webhook', - 'settings.notificationPreferences.email': 'Email', 'settings.notificationsActive': 'Aktiver Kanal', 'settings.notificationsManagedByAdmin': 'Benachrichtigungsereignisse werden vom Administrator konfiguriert.', 'admin.notifications.title': 'Benachrichtigungen', @@ -192,21 +179,10 @@ const de: Record = { 'admin.notifications.testWebhook': 'Test-Webhook senden', 'admin.notifications.testWebhookSuccess': 'Test-Webhook erfolgreich gesendet', 'admin.notifications.testWebhookFailed': 'Test-Webhook fehlgeschlagen', - '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': 'Dieser Webhook wird ausschließlich für Admin-Benachrichtigungen verwendet (z. B. Versions-Updates). Er ist unabhängig von den Benutzer-Webhooks und sendet automatisch, wenn eine URL konfiguriert ist.', - 'admin.notifications.adminWebhookPanel.saved': 'Admin-Webhook-URL gespeichert', - 'admin.notifications.adminWebhookPanel.testSuccess': 'Test-Webhook erfolgreich gesendet', - 'admin.notifications.adminWebhookPanel.testFailed': 'Test-Webhook fehlgeschlagen', - 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Admin-Webhook sendet automatisch, wenn eine URL konfiguriert ist', - 'admin.notifications.adminNotificationsHint': 'Konfiguriere, welche Kanäle Admin-Benachrichtigungen liefern (z. B. Versions-Updates). Der Webhook sendet automatisch, wenn eine Admin-Webhook-URL gesetzt ist.', 'admin.smtp.title': 'E-Mail & Benachrichtigungen', 'admin.smtp.hint': 'SMTP-Konfiguration zum Versenden von E-Mail-Benachrichtigungen.', 'admin.smtp.testButton': 'Test-E-Mail senden', - 'admin.webhook.hint': 'Benutzern erlauben, ihre eigenen Webhook-URLs für Benachrichtigungen zu konfigurieren (Discord, Slack usw.).', + 'admin.webhook.hint': 'Benachrichtigungen an einen externen Webhook senden (Discord, Slack usw.).', 'admin.smtp.testSuccess': 'Test-E-Mail erfolgreich gesendet', 'admin.smtp.testFailed': 'Test-E-Mail fehlgeschlagen', 'dayplan.icsTooltip': 'Kalender exportieren (ICS)', @@ -407,9 +383,6 @@ const de: Record = { 'admin.tabs.users': 'Benutzer', 'admin.tabs.categories': 'Kategorien', 'admin.tabs.backup': 'Backup', - 'admin.tabs.notifications': 'Notifications', - 'admin.tabs.notificationChannels': 'Benachrichtigungskanäle', - 'admin.tabs.adminNotifications': 'Admin-Benachrichtigungen', 'admin.tabs.audit': 'Audit-Protokoll', 'admin.stats.users': 'Benutzer', 'admin.stats.trips': 'Reisen', @@ -715,8 +688,10 @@ const de: Record = { 'atlas.unmark': 'Entfernen', 'atlas.confirmMark': 'Dieses Land als besucht markieren?', 'atlas.confirmUnmark': 'Dieses Land von der Liste entfernen?', + 'atlas.confirmUnmarkRegion': 'Diese Region von der Liste entfernen?', 'atlas.markVisited': 'Als besucht markieren', 'atlas.markVisitedHint': 'Dieses Land zur besuchten Liste hinzufügen', + 'atlas.markRegionVisitedHint': 'Diese Region zur besuchten Liste hinzufügen', 'atlas.addToBucket': 'Zur Bucket List', 'atlas.addPoi': 'Ort hinzufügen', 'atlas.searchCountry': 'Land suchen...', @@ -962,6 +937,11 @@ const de: Record = { 'reservations.linkAssignment': 'Mit Tagesplanung verknüpfen', 'reservations.pickAssignment': 'Zuordnung aus dem Plan wählen...', 'reservations.noAssignment': 'Keine Verknüpfung', + 'reservations.price': 'Preis', + 'reservations.budgetCategory': 'Budgetkategorie', + 'reservations.budgetCategoryPlaceholder': 'z.B. Transport, Unterkunft', + 'reservations.budgetCategoryAuto': 'Auto (aus Buchungstyp)', + 'reservations.budgetHint': 'Beim Speichern wird automatisch ein Budgeteintrag erstellt.', 'reservations.departureDate': 'Abflug', 'reservations.arrivalDate': 'Ankunft', 'reservations.departureTime': 'Abflugzeit', @@ -983,11 +963,6 @@ const de: Record = { 'reservations.span.end': 'Ende', 'reservations.span.ongoing': 'Laufend', 'reservations.validation.endBeforeStart': 'Enddatum/-zeit muss nach dem Startdatum/-zeit liegen', - 'reservations.price': 'Preis', - 'reservations.budgetCategory': 'Budgetkategorie', - 'reservations.budgetCategoryPlaceholder': 'z.B. Transport, Unterkunft', - 'reservations.budgetCategoryAuto': 'Auto (aus Buchungstyp)', - 'reservations.budgetHint': 'Beim Speichern wird automatisch ein Budgeteintrag erstellt.', // Budget 'budget.title': 'Budget', @@ -1583,9 +1558,6 @@ const de: Record = { 'memories.error.toggleSharing': 'Freigabe konnte nicht aktualisiert werden', 'undo.addPlace': 'Ort hinzugefügt', 'undo.done': 'Rückgängig gemacht: {action}', - 'notifications.versionAvailable.title': 'Update Available', - 'notifications.versionAvailable.text': 'TREK {version} is now available.', - 'notifications.versionAvailable.button': 'View Details', 'notifications.test.title': 'Testbenachrichtigung von {actor}', 'notifications.test.text': 'Dies ist eine einfache Testbenachrichtigung.', 'notifications.test.booleanTitle': '{actor} bittet um Ihre Zustimmung', @@ -1634,7 +1606,37 @@ const de: Record = { 'todo.detail.noPriority': 'Keine', 'todo.detail.create': 'Aufgabe erstellen', - // Notifications — dev test events + // Notification system (added from feat/notification-system) + 'settings.notifyVersionAvailable': 'New version available', + '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': 'Gib deine Discord-, Slack- oder benutzerdefinierte Webhook-URL ein, um Benachrichtigungen zu erhalten.', + 'settings.webhookUrl.save': 'Speichern', + 'settings.webhookUrl.saved': 'Webhook-URL gespeichert', + 'settings.webhookUrl.test': 'Testen', + 'settings.webhookUrl.testSuccess': 'Test-Webhook erfolgreich gesendet', + 'settings.webhookUrl.testFailed': 'Test-Webhook fehlgeschlagen', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.email': 'Email', + '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': 'Dieser Webhook wird ausschließlich für Admin-Benachrichtigungen verwendet (z. B. Versions-Updates). Er ist unabhängig von den Benutzer-Webhooks und sendet automatisch, wenn eine URL konfiguriert ist.', + 'admin.notifications.adminWebhookPanel.saved': 'Admin-Webhook-URL gespeichert', + 'admin.notifications.adminWebhookPanel.testSuccess': 'Test-Webhook erfolgreich gesendet', + 'admin.notifications.adminWebhookPanel.testFailed': 'Test-Webhook fehlgeschlagen', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Admin-Webhook sendet automatisch, wenn eine URL konfiguriert ist', + 'admin.notifications.adminNotificationsHint': 'Konfiguriere, welche Kanäle Admin-Benachrichtigungen liefern (z. B. Versions-Updates). Der Webhook sendet automatisch, wenn eine Admin-Webhook-URL gesetzt ist.', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.notificationChannels': 'Benachrichtigungskanäle', + 'admin.tabs.adminNotifications': 'Admin-Benachrichtigungen', + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', '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?', @@ -1671,6 +1673,7 @@ const de: Record = { '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 de diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 0ee61e5..fdc4276 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -709,8 +709,10 @@ const en: Record = { '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...', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index c04f524..6655691 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -164,20 +164,7 @@ const es: Record = { 'settings.notifyCollabMessage': 'Mensajes de chat (Collab)', 'settings.notifyPackingTagged': 'Lista de equipaje: asignaciones', 'settings.notifyWebhook': 'Notificaciones webhook', - 'settings.notifyVersionAvailable': 'New version available', 'settings.notificationsDisabled': 'Las notificaciones no están configuradas. Pida a un administrador que active las notificaciones por correo o webhook.', - 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', - 'settings.webhookUrl.label': 'URL del webhook', - 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', - 'settings.webhookUrl.hint': 'Introduce tu URL de webhook de Discord, Slack o personalizada para recibir notificaciones.', - 'settings.webhookUrl.save': 'Guardar', - 'settings.webhookUrl.saved': 'URL del webhook guardada', - 'settings.webhookUrl.test': 'Probar', - 'settings.webhookUrl.testSuccess': 'Webhook de prueba enviado correctamente', - 'settings.webhookUrl.testFailed': 'Error al enviar el webhook de prueba', - 'settings.notificationPreferences.inapp': 'In-App', - 'settings.notificationPreferences.webhook': 'Webhook', - 'settings.notificationPreferences.email': 'Email', 'settings.notificationsActive': 'Canal activo', 'settings.notificationsManagedByAdmin': 'Los eventos de notificación son configurados por el administrador.', 'admin.notifications.title': 'Notificaciones', @@ -193,21 +180,10 @@ const es: Record = { 'admin.notifications.testWebhook': 'Enviar webhook de prueba', 'admin.notifications.testWebhookSuccess': 'Webhook de prueba enviado correctamente', 'admin.notifications.testWebhookFailed': 'Error al enviar webhook de prueba', - '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': 'Webhook de admin', - 'admin.notifications.adminWebhookPanel.hint': 'Este webhook se usa exclusivamente para notificaciones de admin (ej. alertas de versión). Es independiente de los webhooks de usuario y se activa automáticamente si hay una URL configurada.', - 'admin.notifications.adminWebhookPanel.saved': 'URL del webhook de admin guardada', - 'admin.notifications.adminWebhookPanel.testSuccess': 'Webhook de prueba enviado correctamente', - 'admin.notifications.adminWebhookPanel.testFailed': 'Error al enviar el webhook de prueba', - 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'El webhook de admin se activa automáticamente si hay una URL configurada', - 'admin.notifications.adminNotificationsHint': 'Configura qué canales entregan notificaciones de admin (ej. alertas de versión). El webhook se activa automáticamente si hay una URL de webhook de admin configurada.', 'admin.smtp.title': 'Correo y notificaciones', 'admin.smtp.hint': 'Configuración SMTP para el envío de notificaciones por correo.', 'admin.smtp.testButton': 'Enviar correo de prueba', - 'admin.webhook.hint': 'Permitir a los usuarios configurar sus propias URLs de webhook para notificaciones (Discord, Slack, etc.).', + 'admin.webhook.hint': 'Enviar notificaciones a un webhook externo (Discord, Slack, etc.).', 'admin.smtp.testSuccess': 'Correo de prueba enviado correctamente', 'admin.smtp.testFailed': 'Error al enviar correo de prueba', 'dayplan.icsTooltip': 'Exportar calendario (ICS)', @@ -405,9 +381,6 @@ const es: Record = { 'admin.tabs.users': 'Usuarios', 'admin.tabs.categories': 'Categorías', 'admin.tabs.backup': 'Copia de seguridad', - 'admin.tabs.notifications': 'Notifications', - 'admin.tabs.notificationChannels': 'Canales de notificación', - 'admin.tabs.adminNotifications': 'Notificaciones de admin', 'admin.tabs.audit': 'Registro de auditoría', 'admin.stats.users': 'Usuarios', 'admin.stats.trips': 'Viajes', @@ -727,8 +700,10 @@ const es: Record = { 'atlas.unmark': 'Eliminar', 'atlas.confirmMark': '¿Marcar este país como visitado?', 'atlas.confirmUnmark': '¿Eliminar este país de tu lista de visitados?', + 'atlas.confirmUnmarkRegion': '¿Eliminar esta región de tu lista de visitados?', 'atlas.markVisited': 'Marcar como visitado', 'atlas.markVisitedHint': 'Añadir este país a tu lista de visitados', + 'atlas.markRegionVisitedHint': 'Añadir esta región a tu lista de visitados', 'atlas.addToBucket': 'Añadir a lista de deseos', 'atlas.addPoi': 'Añadir lugar', 'atlas.searchCountry': 'Buscar un país...', @@ -922,6 +897,11 @@ const es: Record = { 'reservations.linkAssignment': 'Vincular a una asignación del día', 'reservations.pickAssignment': 'Selecciona una asignación de tu plan...', 'reservations.noAssignment': 'Sin vínculo (independiente)', + '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': 'Salida', 'reservations.arrivalDate': 'Llegada', 'reservations.departureTime': 'Hora salida', @@ -1489,11 +1469,6 @@ const es: Record = { 'reservations.meta.fromDay': 'Desde', 'reservations.meta.toDay': 'Hasta', 'reservations.meta.selectDay': 'Seleccionar día', - '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.', // OIDC-only mode (2.6.2) 'admin.oidcOnlyMode': 'Desactivar autenticación por contraseña', @@ -1588,9 +1563,6 @@ const es: Record = { 'memories.error.toggleSharing': 'Error al actualizar el uso compartido', 'undo.addPlace': 'Lugar agregado', 'undo.done': 'Deshecho: {action}', - 'notifications.versionAvailable.title': 'Update Available', - 'notifications.versionAvailable.text': 'TREK {version} is now available.', - 'notifications.versionAvailable.button': 'View Details', 'notifications.test.title': 'Notificación de prueba de {actor}', 'notifications.test.text': 'Esta es una notificación de prueba simple.', 'notifications.test.booleanTitle': '{actor} solicita tu aprobación', @@ -1639,7 +1611,37 @@ const es: Record = { 'todo.detail.noPriority': 'None', 'todo.sortByPrio': 'Priority', - // Notifications — dev test events + // Notification system (added from feat/notification-system) + 'settings.notifyVersionAvailable': 'New version available', + 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', + 'settings.webhookUrl.label': 'URL del webhook', + 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', + 'settings.webhookUrl.hint': 'Introduce tu URL de webhook de Discord, Slack o personalizada para recibir notificaciones.', + 'settings.webhookUrl.save': 'Guardar', + 'settings.webhookUrl.saved': 'URL del webhook guardada', + 'settings.webhookUrl.test': 'Probar', + 'settings.webhookUrl.testSuccess': 'Webhook de prueba enviado correctamente', + 'settings.webhookUrl.testFailed': 'Error al enviar el webhook de prueba', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.email': 'Email', + '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': 'Webhook de admin', + 'admin.notifications.adminWebhookPanel.hint': 'Este webhook se usa exclusivamente para notificaciones de admin (ej. alertas de versión). Es independiente de los webhooks de usuario y se activa automáticamente si hay una URL configurada.', + 'admin.notifications.adminWebhookPanel.saved': 'URL del webhook de admin guardada', + 'admin.notifications.adminWebhookPanel.testSuccess': 'Webhook de prueba enviado correctamente', + 'admin.notifications.adminWebhookPanel.testFailed': 'Error al enviar el webhook de prueba', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'El webhook de admin se activa automáticamente si hay una URL configurada', + 'admin.notifications.adminNotificationsHint': 'Configura qué canales entregan notificaciones de admin (ej. alertas de versión). El webhook se activa automáticamente si hay una URL de webhook de admin configurada.', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.notificationChannels': 'Canales de notificación', + 'admin.tabs.adminNotifications': 'Notificaciones de admin', + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', '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?', @@ -1676,6 +1678,7 @@ const es: Record = { '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 es diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index ff6d337..281f0ee 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -163,20 +163,7 @@ const fr: Record = { 'settings.notifyCollabMessage': 'Messages de chat (Collab)', 'settings.notifyPackingTagged': 'Liste de bagages : attributions', 'settings.notifyWebhook': 'Notifications webhook', - 'settings.notifyVersionAvailable': 'New version available', 'settings.notificationsDisabled': 'Les notifications ne sont pas configurées. Demandez à un administrateur d\'activer les notifications par e-mail ou webhook.', - 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', - 'settings.webhookUrl.label': 'URL du webhook', - 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', - 'settings.webhookUrl.hint': 'Entrez votre URL de webhook Discord, Slack ou personnalisée pour recevoir des notifications.', - 'settings.webhookUrl.save': 'Enregistrer', - 'settings.webhookUrl.saved': 'URL du webhook enregistrée', - 'settings.webhookUrl.test': 'Tester', - 'settings.webhookUrl.testSuccess': 'Webhook de test envoyé avec succès', - 'settings.webhookUrl.testFailed': 'Échec du webhook de test', - 'settings.notificationPreferences.inapp': 'In-App', - 'settings.notificationPreferences.webhook': 'Webhook', - 'settings.notificationPreferences.email': 'Email', 'settings.notificationsActive': 'Canal actif', 'settings.notificationsManagedByAdmin': 'Les événements de notification sont configurés par votre administrateur.', 'admin.notifications.title': 'Notifications', @@ -192,21 +179,10 @@ const fr: Record = { 'admin.notifications.testWebhook': 'Envoyer un webhook de test', 'admin.notifications.testWebhookSuccess': 'Webhook de test envoyé avec succès', 'admin.notifications.testWebhookFailed': 'Échec du webhook de test', - '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': 'Webhook admin', - 'admin.notifications.adminWebhookPanel.hint': 'Ce webhook est utilisé exclusivement pour les notifications admin (ex. alertes de version). Il est séparé des webhooks utilisateur et s\'active automatiquement si une URL est configurée.', - 'admin.notifications.adminWebhookPanel.saved': 'URL du webhook admin enregistrée', - 'admin.notifications.adminWebhookPanel.testSuccess': 'Webhook de test envoyé avec succès', - 'admin.notifications.adminWebhookPanel.testFailed': 'Échec du webhook de test', - 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Le webhook admin s\'active automatiquement si une URL est configurée', - 'admin.notifications.adminNotificationsHint': 'Configurez quels canaux envoient les notifications admin (ex. alertes de version). Le webhook s\'active automatiquement si une URL webhook admin est définie.', 'admin.smtp.title': 'E-mail et notifications', 'admin.smtp.hint': 'Configuration SMTP pour l\'envoi des notifications par e-mail.', 'admin.smtp.testButton': 'Envoyer un e-mail de test', - 'admin.webhook.hint': 'Permettre aux utilisateurs de configurer leurs propres URL de webhook pour les notifications (Discord, Slack, etc.).', + 'admin.webhook.hint': 'Envoyer des notifications vers un webhook externe (Discord, Slack, etc.).', 'admin.smtp.testSuccess': 'E-mail de test envoyé avec succès', 'admin.smtp.testFailed': 'Échec de l\'e-mail de test', 'dayplan.icsTooltip': 'Exporter le calendrier (ICS)', @@ -407,9 +383,6 @@ const fr: Record = { 'admin.tabs.users': 'Utilisateurs', 'admin.tabs.categories': 'Catégories', 'admin.tabs.backup': 'Sauvegarde', - 'admin.tabs.notifications': 'Notifications', - 'admin.tabs.notificationChannels': 'Canaux de notification', - 'admin.tabs.adminNotifications': 'Notifications admin', 'admin.stats.users': 'Utilisateurs', 'admin.stats.trips': 'Voyages', 'admin.stats.places': 'Lieux', @@ -750,8 +723,10 @@ const fr: Record = { 'atlas.unmark': 'Retirer', 'atlas.confirmMark': 'Marquer ce pays comme visité ?', 'atlas.confirmUnmark': 'Retirer ce pays de votre liste ?', + 'atlas.confirmUnmarkRegion': 'Retirer cette région de votre liste ?', 'atlas.markVisited': 'Marquer comme visité', 'atlas.markVisitedHint': 'Ajouter ce pays à votre liste de visités', + 'atlas.markRegionVisitedHint': 'Ajouter cette région à votre liste de visités', 'atlas.addToBucket': 'Ajouter à la bucket list', 'atlas.addPoi': 'Ajouter un lieu', 'atlas.searchCountry': 'Rechercher un pays…', @@ -961,6 +936,11 @@ const fr: Record = { 'reservations.linkAssignment': 'Lier à l\'affectation du jour', 'reservations.pickAssignment': 'Sélectionnez une affectation de votre plan…', 'reservations.noAssignment': 'Aucun lien (autonome)', + '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': 'Départ', 'reservations.arrivalDate': 'Arrivée', 'reservations.departureTime': 'Heure dép.', @@ -982,11 +962,6 @@ const fr: Record = { 'reservations.span.end': 'Fin', 'reservations.span.ongoing': 'En cours', 'reservations.validation.endBeforeStart': 'La date/heure de fin doit être postérieure à la date/heure de début', - '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.', // Budget 'budget.title': 'Budget', @@ -1582,9 +1557,6 @@ const fr: Record = { 'memories.error.toggleSharing': 'Impossible de mettre à jour le partage', 'undo.addPlace': 'Lieu ajouté', 'undo.done': 'Annulé : {action}', - 'notifications.versionAvailable.title': 'Update Available', - 'notifications.versionAvailable.text': 'TREK {version} is now available.', - 'notifications.versionAvailable.button': 'View Details', 'notifications.test.title': 'Notification test de {actor}', 'notifications.test.text': 'Ceci est une simple notification de test.', 'notifications.test.booleanTitle': '{actor} demande votre approbation', @@ -1633,7 +1605,37 @@ const fr: Record = { 'todo.detail.noPriority': 'None', 'todo.sortByPrio': 'Priority', - // Notifications — dev test events + // Notification system (added from feat/notification-system) + 'settings.notifyVersionAvailable': 'New version available', + 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', + 'settings.webhookUrl.label': 'URL du webhook', + 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', + 'settings.webhookUrl.hint': 'Entrez votre URL de webhook Discord, Slack ou personnalisée pour recevoir des notifications.', + 'settings.webhookUrl.save': 'Enregistrer', + 'settings.webhookUrl.saved': 'URL du webhook enregistrée', + 'settings.webhookUrl.test': 'Tester', + 'settings.webhookUrl.testSuccess': 'Webhook de test envoyé avec succès', + 'settings.webhookUrl.testFailed': 'Échec du webhook de test', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.email': 'Email', + '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': 'Webhook admin', + 'admin.notifications.adminWebhookPanel.hint': 'Ce webhook est utilisé exclusivement pour les notifications admin (ex. alertes de version). Il est séparé des webhooks utilisateur et s\'active automatiquement si une URL est configurée.', + 'admin.notifications.adminWebhookPanel.saved': 'URL du webhook admin enregistrée', + 'admin.notifications.adminWebhookPanel.testSuccess': 'Webhook de test envoyé avec succès', + 'admin.notifications.adminWebhookPanel.testFailed': 'Échec du webhook de test', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Le webhook admin s\'active automatiquement si une URL est configurée', + 'admin.notifications.adminNotificationsHint': 'Configurez quels canaux envoient les notifications admin (ex. alertes de version). Le webhook s\'active automatiquement si une URL webhook admin est définie.', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.notificationChannels': 'Canaux de notification', + 'admin.tabs.adminNotifications': 'Notifications admin', + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', '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?', @@ -1670,6 +1672,7 @@ const fr: Record = { '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 fr diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index beabb08..58c2007 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -163,20 +163,7 @@ const hu: Record = { 'settings.notifyCollabMessage': 'Csevegés üzenetek (Collab)', 'settings.notifyPackingTagged': 'Csomagolási lista: hozzárendelések', 'settings.notifyWebhook': 'Webhook értesítések', - 'settings.notifyVersionAvailable': 'New version available', 'settings.notificationsDisabled': 'Az értesítések nincsenek beállítva. Kérje meg a rendszergazdát, hogy engedélyezze az e-mail vagy webhook értesítéseket.', - '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': 'Adja meg a Discord, Slack vagy egyéni webhook URL-jét az értesítések fogadásához.', - 'settings.webhookUrl.save': 'Mentés', - 'settings.webhookUrl.saved': 'Webhook URL mentve', - 'settings.webhookUrl.test': 'Teszt', - 'settings.webhookUrl.testSuccess': 'Teszt webhook sikeresen elküldve', - 'settings.webhookUrl.testFailed': 'Teszt webhook sikertelen', - 'settings.notificationPreferences.inapp': 'In-App', - 'settings.notificationPreferences.webhook': 'Webhook', - 'settings.notificationPreferences.email': 'Email', 'settings.notificationsActive': 'Aktív csatorna', 'settings.notificationsManagedByAdmin': 'Az értesítési eseményeket az adminisztrátor konfigurálja.', 'settings.on': 'Be', @@ -279,21 +266,10 @@ const hu: Record = { 'admin.notifications.testWebhook': 'Teszt webhook küldése', 'admin.notifications.testWebhookSuccess': 'Teszt webhook sikeresen elküldve', 'admin.notifications.testWebhookFailed': 'Teszt webhook küldése sikertelen', - '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': 'Ez a webhook kizárólag admin értesítésekhez használatos (pl. verziófrissítési figyelmeztetések). Független a felhasználói webhookoktól, és automatikusan küld, ha URL van beállítva.', - 'admin.notifications.adminWebhookPanel.saved': 'Admin webhook URL mentve', - 'admin.notifications.adminWebhookPanel.testSuccess': 'Teszt webhook sikeresen elküldve', - 'admin.notifications.adminWebhookPanel.testFailed': 'Teszt webhook sikertelen', - 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Az admin webhook automatikusan küld, ha URL van beállítva', - 'admin.notifications.adminNotificationsHint': 'Állítsa be, hogy mely csatornák szállítsák az admin értesítéseket (pl. verziófrissítési figyelmeztetések). A webhook automatikusan küld, ha admin webhook URL van megadva.', 'admin.smtp.title': 'E-mail és értesítések', 'admin.smtp.hint': 'SMTP konfiguráció e-mail értesítések küldéséhez.', 'admin.smtp.testButton': 'Teszt e-mail küldése', - 'admin.webhook.hint': 'Engedje meg a felhasználóknak, hogy saját webhook URL-t konfiguráljanak az értesítésekhez (Discord, Slack stb.).', + 'admin.webhook.hint': 'Értesítések küldése külső webhookra (Discord, Slack stb.).', 'admin.smtp.testSuccess': 'Teszt e-mail sikeresen elküldve', 'admin.smtp.testFailed': 'Teszt e-mail küldése sikertelen', 'dayplan.icsTooltip': 'Naptár exportálása (ICS)', @@ -407,9 +383,6 @@ const hu: Record = { 'admin.tabs.users': 'Felhasználók', 'admin.tabs.categories': 'Kategóriák', 'admin.tabs.backup': 'Biztonsági mentés', - 'admin.tabs.notifications': 'Notifications', - 'admin.tabs.notificationChannels': 'Értesítési csatornák', - 'admin.tabs.adminNotifications': 'Admin értesítések', 'admin.stats.users': 'Felhasználók', 'admin.stats.trips': 'Utazások', 'admin.stats.places': 'Helyek', @@ -715,8 +688,10 @@ const hu: Record = { 'atlas.unmark': 'Eltávolítás', 'atlas.confirmMark': 'Megjelölöd ezt az országot meglátogatottként?', 'atlas.confirmUnmark': 'Eltávolítod ezt az országot a meglátogatottak listájáról?', + 'atlas.confirmUnmarkRegion': 'Eltávolítod ezt a régiót a meglátogatottak listájáról?', 'atlas.markVisited': 'Megjelölés meglátogatottként', 'atlas.markVisitedHint': 'Ország hozzáadása a meglátogatottak listájához', + 'atlas.markRegionVisitedHint': 'Régió hozzáadása a meglátogatottak listájához', 'atlas.addToBucket': 'Hozzáadás a bakancslistához', 'atlas.addPoi': 'Hely hozzáadása', 'atlas.searchCountry': 'Ország keresése...', @@ -962,6 +937,11 @@ const hu: Record = { 'reservations.linkAssignment': 'Összekapcsolás napi tervvel', 'reservations.pickAssignment': 'Válassz hozzárendelést a tervedből...', 'reservations.noAssignment': 'Nincs összekapcsolás (önálló)', + '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': 'Indulás', 'reservations.arrivalDate': 'Érkezés', 'reservations.departureTime': 'Indulási idő', @@ -983,11 +963,6 @@ const hu: Record = { 'reservations.span.end': 'Vége', 'reservations.span.ongoing': 'Folyamatban', 'reservations.validation.endBeforeStart': 'A befejezés dátuma/időpontja a kezdés utáni kell legyen', - '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.', // Költségvetés 'budget.title': 'Költségvetés', @@ -1583,9 +1558,6 @@ const hu: Record = { 'memories.error.toggleSharing': 'A megosztás frissítése sikertelen', 'undo.addPlace': 'Hely hozzáadva', 'undo.done': 'Visszavonva: {action}', - 'notifications.versionAvailable.title': 'Update Available', - 'notifications.versionAvailable.text': 'TREK {version} is now available.', - 'notifications.versionAvailable.button': 'View Details', 'notifications.test.title': 'Teszt értesítés {actor} részéről', 'notifications.test.text': 'Ez egy egyszerű teszt értesítés.', 'notifications.test.booleanTitle': '{actor} jóváhagyásodat kéri', @@ -1634,7 +1606,37 @@ const hu: Record = { 'todo.detail.noPriority': 'None', 'todo.sortByPrio': 'Priority', - // Notifications — dev test events + // Notification system (added from feat/notification-system) + 'settings.notifyVersionAvailable': 'New version available', + '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': 'Adja meg a Discord, Slack vagy egyéni webhook URL-jét az értesítések fogadásához.', + 'settings.webhookUrl.save': 'Mentés', + 'settings.webhookUrl.saved': 'Webhook URL mentve', + 'settings.webhookUrl.test': 'Teszt', + 'settings.webhookUrl.testSuccess': 'Teszt webhook sikeresen elküldve', + 'settings.webhookUrl.testFailed': 'Teszt webhook sikertelen', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.email': 'Email', + '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': 'Ez a webhook kizárólag admin értesítésekhez használatos (pl. verziófrissítési figyelmeztetések). Független a felhasználói webhookoktól, és automatikusan küld, ha URL van beállítva.', + 'admin.notifications.adminWebhookPanel.saved': 'Admin webhook URL mentve', + 'admin.notifications.adminWebhookPanel.testSuccess': 'Teszt webhook sikeresen elküldve', + 'admin.notifications.adminWebhookPanel.testFailed': 'Teszt webhook sikertelen', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Az admin webhook automatikusan küld, ha URL van beállítva', + 'admin.notifications.adminNotificationsHint': 'Állítsa be, hogy mely csatornák szállítsák az admin értesítéseket (pl. verziófrissítési figyelmeztetések). A webhook automatikusan küld, ha admin webhook URL van megadva.', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.notificationChannels': 'Értesítési csatornák', + 'admin.tabs.adminNotifications': 'Admin értesítések', + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', '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?', @@ -1671,6 +1673,7 @@ const hu: Record = { '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 hu diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index 86bd060..f20112c 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -163,20 +163,7 @@ const it: Record = { 'settings.notifyCollabMessage': 'Messaggi chat (Collab)', 'settings.notifyPackingTagged': 'Lista valigia: assegnazioni', 'settings.notifyWebhook': 'Notifiche webhook', - 'settings.notifyVersionAvailable': 'New version available', 'settings.notificationsDisabled': 'Le notifiche non sono configurate. Chiedi a un amministratore di abilitare le notifiche e-mail o webhook.', - 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', - 'settings.webhookUrl.label': 'URL webhook', - 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', - 'settings.webhookUrl.hint': 'Inserisci il tuo URL webhook Discord, Slack o personalizzato per ricevere notifiche.', - 'settings.webhookUrl.save': 'Salva', - 'settings.webhookUrl.saved': 'URL webhook salvato', - 'settings.webhookUrl.test': 'Test', - 'settings.webhookUrl.testSuccess': 'Webhook di test inviato con successo', - 'settings.webhookUrl.testFailed': 'Invio webhook di test fallito', - 'settings.notificationPreferences.inapp': 'In-App', - 'settings.notificationPreferences.webhook': 'Webhook', - 'settings.notificationPreferences.email': 'Email', 'settings.notificationsActive': 'Canale attivo', 'settings.notificationsManagedByAdmin': 'Gli eventi di notifica sono configurati dall\'amministratore.', 'settings.on': 'On', @@ -279,21 +266,10 @@ const it: Record = { 'admin.notifications.testWebhook': 'Invia webhook di test', 'admin.notifications.testWebhookSuccess': 'Webhook di test inviato con successo', 'admin.notifications.testWebhookFailed': 'Invio webhook di test fallito', - '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': 'Webhook admin', - 'admin.notifications.adminWebhookPanel.hint': 'Questo webhook viene usato esclusivamente per le notifiche admin (es. avvisi di versione). È separato dai webhook utente e si attiva automaticamente quando è configurato un URL.', - 'admin.notifications.adminWebhookPanel.saved': 'URL webhook admin salvato', - 'admin.notifications.adminWebhookPanel.testSuccess': 'Webhook di test inviato con successo', - 'admin.notifications.adminWebhookPanel.testFailed': 'Invio webhook di test fallito', - 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Il webhook admin si attiva automaticamente quando è configurato un URL', - 'admin.notifications.adminNotificationsHint': 'Configura quali canali consegnano le notifiche admin (es. avvisi di versione). Il webhook si attiva automaticamente se è impostato un URL webhook admin.', 'admin.smtp.title': 'Email e notifiche', 'admin.smtp.hint': 'Configurazione SMTP per l\'invio delle notifiche via e-mail.', 'admin.smtp.testButton': 'Invia email di prova', - 'admin.webhook.hint': 'Consenti agli utenti di configurare i propri URL webhook per le notifiche (Discord, Slack, ecc.).', + 'admin.webhook.hint': 'Invia notifiche a un webhook esterno (Discord, Slack, ecc.).', 'admin.smtp.testSuccess': 'Email di prova inviata con successo', 'admin.smtp.testFailed': 'Invio email di prova fallito', 'dayplan.icsTooltip': 'Esporta calendario (ICS)', @@ -407,9 +383,6 @@ const it: Record = { 'admin.tabs.users': 'Utenti', 'admin.tabs.categories': 'Categorie', 'admin.tabs.backup': 'Backup', - 'admin.tabs.notifications': 'Notifications', - 'admin.tabs.notificationChannels': 'Canali di notifica', - 'admin.tabs.adminNotifications': 'Notifiche admin', 'admin.stats.users': 'Utenti', 'admin.stats.trips': 'Viaggi', 'admin.stats.places': 'Luoghi', @@ -715,8 +688,10 @@ const it: Record = { 'atlas.unmark': 'Rimuovi', 'atlas.confirmMark': 'Segnare questo paese come visitato?', 'atlas.confirmUnmark': 'Rimuovere questo paese dalla tua lista dei visitati?', + 'atlas.confirmUnmarkRegion': 'Rimuovere questa regione dalla tua lista dei visitati?', 'atlas.markVisited': 'Segna come visitato', 'atlas.markVisitedHint': 'Aggiungi questo paese alla tua lista dei visitati', + 'atlas.markRegionVisitedHint': 'Aggiungi questa regione alla tua lista dei visitati', 'atlas.addToBucket': 'Aggiungi alla lista desideri', 'atlas.addPoi': 'Aggiungi luogo', 'atlas.bucketNamePlaceholder': 'Nome (paese, città, luogo...)', @@ -962,6 +937,11 @@ const it: Record = { 'reservations.linkAssignment': 'Collega all\'assegnazione del giorno', 'reservations.pickAssignment': 'Seleziona un\'assegnazione dal tuo programma...', 'reservations.noAssignment': 'Nessun collegamento (autonomo)', + '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': 'Partenza', 'reservations.arrivalDate': 'Arrivo', 'reservations.departureTime': 'Ora part.', @@ -983,11 +963,6 @@ const it: Record = { 'reservations.span.end': 'Fine', 'reservations.span.ongoing': 'In corso', 'reservations.validation.endBeforeStart': 'La data/ora di fine deve essere successiva alla data/ora di inizio', - '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.', // Budget 'budget.title': 'Budget', @@ -1583,9 +1558,6 @@ const it: Record = { 'memories.error.addPhotos': 'Aggiunta foto non riuscita', 'memories.error.removePhoto': 'Rimozione foto non riuscita', 'memories.error.toggleSharing': 'Aggiornamento condivisione non riuscito', - 'notifications.versionAvailable.title': 'Update Available', - 'notifications.versionAvailable.text': 'TREK {version} is now available.', - 'notifications.versionAvailable.button': 'View Details', 'notifications.test.title': 'Notifica di test da {actor}', 'notifications.test.text': 'Questa è una semplice notifica di test.', 'notifications.test.booleanTitle': '{actor} richiede la tua approvazione', @@ -1634,7 +1606,37 @@ const it: Record = { 'todo.detail.noPriority': 'None', 'todo.sortByPrio': 'Priority', - // Notifications — dev test events + // Notification system (added from feat/notification-system) + 'settings.notifyVersionAvailable': 'New version available', + 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', + 'settings.webhookUrl.label': 'URL webhook', + 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', + 'settings.webhookUrl.hint': 'Inserisci il tuo URL webhook Discord, Slack o personalizzato per ricevere notifiche.', + 'settings.webhookUrl.save': 'Salva', + 'settings.webhookUrl.saved': 'URL webhook salvato', + 'settings.webhookUrl.test': 'Test', + 'settings.webhookUrl.testSuccess': 'Webhook di test inviato con successo', + 'settings.webhookUrl.testFailed': 'Invio webhook di test fallito', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.email': 'Email', + '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': 'Webhook admin', + 'admin.notifications.adminWebhookPanel.hint': 'Questo webhook viene usato esclusivamente per le notifiche admin (es. avvisi di versione). È separato dai webhook utente e si attiva automaticamente quando è configurato un URL.', + 'admin.notifications.adminWebhookPanel.saved': 'URL webhook admin salvato', + 'admin.notifications.adminWebhookPanel.testSuccess': 'Webhook di test inviato con successo', + 'admin.notifications.adminWebhookPanel.testFailed': 'Invio webhook di test fallito', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Il webhook admin si attiva automaticamente quando è configurato un URL', + 'admin.notifications.adminNotificationsHint': 'Configura quali canali consegnano le notifiche admin (es. avvisi di versione). Il webhook si attiva automaticamente se è impostato un URL webhook admin.', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.notificationChannels': 'Canali di notifica', + 'admin.tabs.adminNotifications': 'Notifiche admin', + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', '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?', @@ -1671,6 +1673,7 @@ const it: Record = { '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 it diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index d7a875c..3944396 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -163,20 +163,7 @@ const nl: Record = { 'settings.notifyCollabMessage': 'Chatberichten (Collab)', 'settings.notifyPackingTagged': 'Paklijst: toewijzingen', 'settings.notifyWebhook': 'Webhook-meldingen', - 'settings.notifyVersionAvailable': 'New version available', 'settings.notificationsDisabled': 'Meldingen zijn niet geconfigureerd. Vraag een beheerder om e-mail- of webhookmeldingen in te schakelen.', - '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': 'Voer je Discord-, Slack- of aangepaste webhook-URL in om meldingen te ontvangen.', - 'settings.webhookUrl.save': 'Opslaan', - 'settings.webhookUrl.saved': 'Webhook-URL opgeslagen', - 'settings.webhookUrl.test': 'Testen', - 'settings.webhookUrl.testSuccess': 'Test-webhook succesvol verzonden', - 'settings.webhookUrl.testFailed': 'Test-webhook mislukt', - 'settings.notificationPreferences.inapp': 'In-App', - 'settings.notificationPreferences.webhook': 'Webhook', - 'settings.notificationPreferences.email': 'Email', 'settings.notificationsActive': 'Actief kanaal', 'settings.notificationsManagedByAdmin': 'Meldingsgebeurtenissen worden geconfigureerd door je beheerder.', 'admin.notifications.title': 'Meldingen', @@ -192,21 +179,10 @@ const nl: Record = { 'admin.notifications.testWebhook': 'Testwebhook verzenden', 'admin.notifications.testWebhookSuccess': 'Testwebhook succesvol verzonden', 'admin.notifications.testWebhookFailed': 'Testwebhook mislukt', - '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': 'Deze webhook wordt uitsluitend gebruikt voor admin-meldingen (bijv. versie-updates). Hij staat los van gebruikerswebhooks en verstuurt automatisch als er een URL is ingesteld.', - 'admin.notifications.adminWebhookPanel.saved': 'Admin-webhook-URL opgeslagen', - 'admin.notifications.adminWebhookPanel.testSuccess': 'Test-webhook succesvol verzonden', - 'admin.notifications.adminWebhookPanel.testFailed': 'Test-webhook mislukt', - 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Admin-webhook verstuurt automatisch als er een URL is ingesteld', - 'admin.notifications.adminNotificationsHint': 'Stel in via welke kanalen admin-meldingen worden bezorgd (bijv. versie-updates). De webhook verstuurt automatisch als er een admin-webhook-URL is ingesteld.', 'admin.smtp.title': 'E-mail en meldingen', 'admin.smtp.hint': 'SMTP-configuratie voor het verzenden van e-mailmeldingen.', 'admin.smtp.testButton': 'Test-e-mail verzenden', - 'admin.webhook.hint': 'Gebruikers toestaan hun eigen webhook-URLs voor meldingen te configureren (Discord, Slack, enz.).', + 'admin.webhook.hint': 'Meldingen verzenden naar een externe webhook (Discord, Slack, enz.).', 'admin.smtp.testSuccess': 'Test-e-mail succesvol verzonden', 'admin.smtp.testFailed': 'Test-e-mail mislukt', 'dayplan.icsTooltip': 'Kalender exporteren (ICS)', @@ -407,9 +383,6 @@ const nl: Record = { 'admin.tabs.users': 'Gebruikers', 'admin.tabs.categories': 'Categorieën', 'admin.tabs.backup': 'Back-up', - 'admin.tabs.notifications': 'Notifications', - 'admin.tabs.notificationChannels': 'Meldingskanalen', - 'admin.tabs.adminNotifications': 'Admin-meldingen', 'admin.tabs.audit': 'Auditlog', 'admin.stats.users': 'Gebruikers', 'admin.stats.trips': 'Reizen', @@ -750,8 +723,10 @@ const nl: Record = { 'atlas.unmark': 'Verwijderen', 'atlas.confirmMark': 'Dit land als bezocht markeren?', 'atlas.confirmUnmark': 'Dit land van je bezochte lijst verwijderen?', + 'atlas.confirmUnmarkRegion': 'Deze regio van je bezochte lijst verwijderen?', 'atlas.markVisited': 'Markeren als bezocht', 'atlas.markVisitedHint': 'Dit land toevoegen aan je bezochte lijst', + 'atlas.markRegionVisitedHint': 'Deze regio toevoegen aan je bezochte lijst', 'atlas.addToBucket': 'Aan bucket list toevoegen', 'atlas.addPoi': 'Plaats toevoegen', 'atlas.searchCountry': 'Zoek een land...', @@ -961,6 +936,11 @@ const nl: Record = { 'reservations.linkAssignment': 'Koppelen aan dagtoewijzing', 'reservations.pickAssignment': 'Selecteer een toewijzing uit je plan...', 'reservations.noAssignment': 'Geen koppeling (zelfstandig)', + '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': 'Vertrek', 'reservations.arrivalDate': 'Aankomst', 'reservations.departureTime': 'Vertrektijd', @@ -982,11 +962,6 @@ const nl: Record = { 'reservations.span.end': 'Einde', 'reservations.span.ongoing': 'Lopend', 'reservations.validation.endBeforeStart': 'Einddatum/-tijd moet na de startdatum/-tijd liggen', - '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.', // Budget 'budget.title': 'Budget', @@ -1582,9 +1557,6 @@ const nl: Record = { 'memories.error.toggleSharing': 'Delen bijwerken mislukt', 'undo.addPlace': 'Locatie toegevoegd', 'undo.done': 'Ongedaan gemaakt: {action}', - 'notifications.versionAvailable.title': 'Update Available', - 'notifications.versionAvailable.text': 'TREK {version} is now available.', - 'notifications.versionAvailable.button': 'View Details', 'notifications.test.title': 'Testmelding van {actor}', 'notifications.test.text': 'Dit is een eenvoudige testmelding.', 'notifications.test.booleanTitle': '{actor} vraagt om uw goedkeuring', @@ -1633,7 +1605,37 @@ const nl: Record = { 'todo.detail.noPriority': 'None', 'todo.sortByPrio': 'Priority', - // Notifications — dev test events + // Notification system (added from feat/notification-system) + 'settings.notifyVersionAvailable': 'New version available', + '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': 'Voer je Discord-, Slack- of aangepaste webhook-URL in om meldingen te ontvangen.', + 'settings.webhookUrl.save': 'Opslaan', + 'settings.webhookUrl.saved': 'Webhook-URL opgeslagen', + 'settings.webhookUrl.test': 'Testen', + 'settings.webhookUrl.testSuccess': 'Test-webhook succesvol verzonden', + 'settings.webhookUrl.testFailed': 'Test-webhook mislukt', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.email': 'Email', + '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': 'Deze webhook wordt uitsluitend gebruikt voor admin-meldingen (bijv. versie-updates). Hij staat los van gebruikerswebhooks en verstuurt automatisch als er een URL is ingesteld.', + 'admin.notifications.adminWebhookPanel.saved': 'Admin-webhook-URL opgeslagen', + 'admin.notifications.adminWebhookPanel.testSuccess': 'Test-webhook succesvol verzonden', + 'admin.notifications.adminWebhookPanel.testFailed': 'Test-webhook mislukt', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Admin-webhook verstuurt automatisch als er een URL is ingesteld', + 'admin.notifications.adminNotificationsHint': 'Stel in via welke kanalen admin-meldingen worden bezorgd (bijv. versie-updates). De webhook verstuurt automatisch als er een admin-webhook-URL is ingesteld.', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.notificationChannels': 'Meldingskanalen', + 'admin.tabs.adminNotifications': 'Admin-meldingen', + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', '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?', @@ -1670,6 +1672,7 @@ const nl: Record = { '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 nl diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index e10ebdc..e8f65bd 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -655,8 +655,10 @@ const pl: Record = { 'atlas.unmark': 'Usuń', 'atlas.confirmMark': 'Oznaczyć ten kraj jako odwiedzony?', 'atlas.confirmUnmark': 'Usunąć ten kraj z listy odwiedzonych?', + 'atlas.confirmUnmarkRegion': 'Usunąć ten region z listy odwiedzonych?', 'atlas.markVisited': 'Oznacz jako odwiedzony', 'atlas.markVisitedHint': 'Dodaj ten kraj do listy odwiedzonych', + 'atlas.markRegionVisitedHint': 'Dodaj ten region do listy odwiedzonych', 'atlas.addToBucket': 'Dodaj do listy marzeń', 'atlas.addPoi': 'Dodaj miejsce', 'atlas.bucketNamePlaceholder': 'Nazwa (kraj, miasto, miejsce...)', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 45fea9d..25927c3 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -163,20 +163,7 @@ const ru: Record = { 'settings.notifyCollabMessage': 'Сообщения чата (Collab)', 'settings.notifyPackingTagged': 'Список вещей: назначения', 'settings.notifyWebhook': 'Webhook-уведомления', - 'settings.notifyVersionAvailable': 'New version available', 'settings.notificationsDisabled': 'Уведомления не настроены. Попросите администратора включить уведомления по электронной почте или webhook.', - 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', - 'settings.webhookUrl.label': 'URL вебхука', - 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', - 'settings.webhookUrl.hint': 'Введите URL вашего вебхука Discord, Slack или пользовательского для получения уведомлений.', - 'settings.webhookUrl.save': 'Сохранить', - 'settings.webhookUrl.saved': 'URL вебхука сохранён', - 'settings.webhookUrl.test': 'Тест', - 'settings.webhookUrl.testSuccess': 'Тестовый вебхук успешно отправлен', - 'settings.webhookUrl.testFailed': 'Ошибка тестового вебхука', - 'settings.notificationPreferences.inapp': 'In-App', - 'settings.notificationPreferences.webhook': 'Webhook', - 'settings.notificationPreferences.email': 'Email', 'settings.notificationsActive': 'Активный канал', 'settings.notificationsManagedByAdmin': 'События уведомлений настраиваются администратором.', 'admin.notifications.title': 'Уведомления', @@ -192,21 +179,10 @@ const ru: Record = { 'admin.notifications.testWebhook': 'Отправить тестовый вебхук', 'admin.notifications.testWebhookSuccess': 'Тестовый вебхук успешно отправлен', 'admin.notifications.testWebhookFailed': 'Ошибка отправки тестового вебхука', - '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.notifications.adminWebhookPanel.hint': 'Этот вебхук используется исключительно для уведомлений администратора (например, оповещения о версиях). Он независим от пользовательских вебхуков и отправляется автоматически при наличии URL.', - 'admin.notifications.adminWebhookPanel.saved': 'URL вебхука администратора сохранён', - 'admin.notifications.adminWebhookPanel.testSuccess': 'Тестовый вебхук успешно отправлен', - 'admin.notifications.adminWebhookPanel.testFailed': 'Ошибка тестового вебхука', - 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Вебхук администратора отправляется автоматически при наличии URL', - 'admin.notifications.adminNotificationsHint': 'Настройте, какие каналы доставляют уведомления администратора (например, оповещения о версиях). Вебхук отправляется автоматически, если задан URL вебхука администратора.', 'admin.smtp.title': 'Почта и уведомления', 'admin.smtp.hint': 'Конфигурация SMTP для отправки уведомлений по электронной почте.', 'admin.smtp.testButton': 'Отправить тестовое письмо', - 'admin.webhook.hint': 'Разрешить пользователям настраивать собственные URL вебхуков для уведомлений (Discord, Slack и т.д.).', + 'admin.webhook.hint': 'Отправлять уведомления через внешний webhook (Discord, Slack и т.д.).', 'admin.smtp.testSuccess': 'Тестовое письмо успешно отправлено', 'admin.smtp.testFailed': 'Ошибка отправки тестового письма', 'dayplan.icsTooltip': 'Экспорт календаря (ICS)', @@ -407,9 +383,6 @@ const ru: Record = { 'admin.tabs.users': 'Пользователи', 'admin.tabs.categories': 'Категории', 'admin.tabs.backup': 'Резервная копия', - 'admin.tabs.notifications': 'Notifications', - 'admin.tabs.notificationChannels': 'Каналы уведомлений', - 'admin.tabs.adminNotifications': 'Уведомления администратора', 'admin.tabs.audit': 'Журнал аудита', 'admin.stats.users': 'Пользователи', 'admin.stats.trips': 'Поездки', @@ -750,8 +723,10 @@ const ru: Record = { 'atlas.unmark': 'Удалить', 'atlas.confirmMark': 'Отметить эту страну как посещённую?', 'atlas.confirmUnmark': 'Удалить эту страну из списка посещённых?', + 'atlas.confirmUnmarkRegion': 'Удалить этот регион из списка посещённых?', 'atlas.markVisited': 'Отметить как посещённую', 'atlas.markVisitedHint': 'Добавить эту страну в список посещённых', + 'atlas.markRegionVisitedHint': 'Добавить этот регион в список посещённых', 'atlas.addToBucket': 'В список желаний', 'atlas.addPoi': 'Добавить место', 'atlas.searchCountry': 'Поиск страны...', @@ -961,6 +936,11 @@ const ru: Record = { 'reservations.linkAssignment': 'Привязать к назначению дня', 'reservations.pickAssignment': 'Выберите назначение из вашего плана...', 'reservations.noAssignment': 'Без привязки (самостоятельное)', + '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': 'Вылет', 'reservations.arrivalDate': 'Прилёт', 'reservations.departureTime': 'Время вылета', @@ -982,11 +962,6 @@ const ru: Record = { 'reservations.span.end': 'Конец', 'reservations.span.ongoing': 'Продолжается', 'reservations.validation.endBeforeStart': 'Дата/время окончания должны быть позже даты/времени начала', - '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.', // Budget 'budget.title': 'Бюджет', @@ -1582,9 +1557,6 @@ const ru: Record = { 'memories.error.toggleSharing': 'Не удалось обновить настройки доступа', 'undo.addPlace': 'Место добавлено', 'undo.done': 'Отменено: {action}', - 'notifications.versionAvailable.title': 'Update Available', - 'notifications.versionAvailable.text': 'TREK {version} is now available.', - 'notifications.versionAvailable.button': 'View Details', 'notifications.test.title': 'Тестовое уведомление от {actor}', 'notifications.test.text': 'Это простое тестовое уведомление.', 'notifications.test.booleanTitle': '{actor} запрашивает подтверждение', @@ -1633,7 +1605,37 @@ const ru: Record = { 'todo.detail.noPriority': 'None', 'todo.sortByPrio': 'Priority', - // Notifications — dev test events + // Notification system (added from feat/notification-system) + 'settings.notifyVersionAvailable': 'New version available', + 'settings.notificationPreferences.noChannels': 'No notification channels are configured. Ask an admin to set up email or webhook notifications.', + 'settings.webhookUrl.label': 'URL вебхука', + 'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...', + 'settings.webhookUrl.hint': 'Введите URL вашего вебхука Discord, Slack или пользовательского для получения уведомлений.', + 'settings.webhookUrl.save': 'Сохранить', + 'settings.webhookUrl.saved': 'URL вебхука сохранён', + 'settings.webhookUrl.test': 'Тест', + 'settings.webhookUrl.testSuccess': 'Тестовый вебхук успешно отправлен', + 'settings.webhookUrl.testFailed': 'Ошибка тестового вебхука', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.email': 'Email', + '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.notifications.adminWebhookPanel.hint': 'Этот вебхук используется исключительно для уведомлений администратора (например, оповещения о версиях). Он независим от пользовательских вебхуков и отправляется автоматически при наличии URL.', + 'admin.notifications.adminWebhookPanel.saved': 'URL вебхука администратора сохранён', + 'admin.notifications.adminWebhookPanel.testSuccess': 'Тестовый вебхук успешно отправлен', + 'admin.notifications.adminWebhookPanel.testFailed': 'Ошибка тестового вебхука', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': 'Вебхук администратора отправляется автоматически при наличии URL', + 'admin.notifications.adminNotificationsHint': 'Настройте, какие каналы доставляют уведомления администратора (например, оповещения о версиях). Вебхук отправляется автоматически, если задан URL вебхука администратора.', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.notificationChannels': 'Каналы уведомлений', + 'admin.tabs.adminNotifications': 'Уведомления администратора', + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', '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?', @@ -1670,6 +1672,7 @@ const ru: Record = { '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 ru diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 7722efe..58f4e68 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -163,20 +163,7 @@ const zh: Record = { 'settings.notifyCollabMessage': '聊天消息 (Collab)', 'settings.notifyPackingTagged': '行李清单:分配', 'settings.notifyWebhook': 'Webhook 通知', - 'settings.notifyVersionAvailable': 'New version available', 'settings.notificationsDisabled': '通知尚未配置。请联系管理员启用电子邮件或 Webhook 通知。', - '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': '输入您的 Discord、Slack 或自定义 Webhook URL 以接收通知。', - 'settings.webhookUrl.save': '保存', - 'settings.webhookUrl.saved': 'Webhook URL 已保存', - 'settings.webhookUrl.test': '测试', - 'settings.webhookUrl.testSuccess': '测试 Webhook 发送成功', - 'settings.webhookUrl.testFailed': '测试 Webhook 失败', - 'settings.notificationPreferences.inapp': 'In-App', - 'settings.notificationPreferences.webhook': 'Webhook', - 'settings.notificationPreferences.email': 'Email', 'settings.notificationsActive': '活跃频道', 'settings.notificationsManagedByAdmin': '通知事件由管理员配置。', 'admin.notifications.title': '通知', @@ -192,21 +179,10 @@ const zh: Record = { 'admin.notifications.testWebhook': '发送测试 Webhook', 'admin.notifications.testWebhookSuccess': '测试 Webhook 发送成功', 'admin.notifications.testWebhookFailed': '测试 Webhook 发送失败', - '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': '管理员 Webhook', - 'admin.notifications.adminWebhookPanel.hint': '此 Webhook 专用于管理员通知(如版本更新提醒)。它与用户 Webhook 相互独立,配置 URL 后自动触发。', - 'admin.notifications.adminWebhookPanel.saved': '管理员 Webhook URL 已保存', - 'admin.notifications.adminWebhookPanel.testSuccess': '测试 Webhook 发送成功', - 'admin.notifications.adminWebhookPanel.testFailed': '测试 Webhook 失败', - 'admin.notifications.adminWebhookPanel.alwaysOnHint': '配置 URL 后管理员 Webhook 自动触发', - 'admin.notifications.adminNotificationsHint': '配置哪些渠道发送管理员通知(如版本更新提醒)。设置管理员 Webhook URL 后,Webhook 将自动触发。', 'admin.smtp.title': '邮件与通知', 'admin.smtp.hint': '用于发送电子邮件通知的 SMTP 配置。', 'admin.smtp.testButton': '发送测试邮件', - 'admin.webhook.hint': '允许用户为通知配置自己的 Webhook URL(Discord、Slack 等)。', + 'admin.webhook.hint': '向外部 Webhook 发送通知(Discord、Slack 等)。', 'admin.smtp.testSuccess': '测试邮件发送成功', 'admin.smtp.testFailed': '测试邮件发送失败', 'dayplan.icsTooltip': '导出日历 (ICS)', @@ -407,9 +383,6 @@ const zh: Record = { 'admin.tabs.users': '用户', 'admin.tabs.categories': '分类', 'admin.tabs.backup': '备份', - 'admin.tabs.notifications': 'Notifications', - 'admin.tabs.notificationChannels': '通知渠道', - 'admin.tabs.adminNotifications': '管理员通知', 'admin.tabs.audit': '审计日志', 'admin.stats.users': '用户', 'admin.stats.trips': '旅行', @@ -750,8 +723,10 @@ const zh: Record = { 'atlas.unmark': '移除', 'atlas.confirmMark': '将此国家标记为已访问?', 'atlas.confirmUnmark': '从已访问列表中移除此国家?', + 'atlas.confirmUnmarkRegion': '从已访问列表中移除此地区?', 'atlas.markVisited': '标记为已访问', 'atlas.markVisitedHint': '将此国家添加到已访问列表', + 'atlas.markRegionVisitedHint': '将此地区添加到已访问列表', 'atlas.addToBucket': '添加到心愿单', 'atlas.addPoi': '添加地点', 'atlas.searchCountry': '搜索国家...', @@ -961,6 +936,11 @@ const zh: Record = { 'reservations.linkAssignment': '关联日程分配', 'reservations.pickAssignment': '从计划中选择一个分配...', 'reservations.noAssignment': '无关联(独立)', + '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': '出发', 'reservations.arrivalDate': '到达', 'reservations.departureTime': '出发时间', @@ -982,11 +962,6 @@ const zh: Record = { 'reservations.span.end': '结束', 'reservations.span.ongoing': '进行中', 'reservations.validation.endBeforeStart': '结束日期/时间必须晚于开始日期/时间', - '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.', // Budget 'budget.title': '预算', @@ -1582,9 +1557,6 @@ const zh: Record = { 'memories.error.toggleSharing': '更新共享设置失败', 'undo.addPlace': '地点已添加', 'undo.done': '已撤销:{action}', - 'notifications.versionAvailable.title': 'Update Available', - 'notifications.versionAvailable.text': 'TREK {version} is now available.', - 'notifications.versionAvailable.button': 'View Details', 'notifications.test.title': '来自 {actor} 的测试通知', 'notifications.test.text': '这是一条简单的测试通知。', 'notifications.test.booleanTitle': '{actor} 请求您的审批', @@ -1633,7 +1605,37 @@ const zh: Record = { 'todo.detail.noPriority': 'None', 'todo.sortByPrio': 'Priority', - // Notifications — dev test events + // Notification system (added from feat/notification-system) + 'settings.notifyVersionAvailable': 'New version available', + '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': '输入您的 Discord、Slack 或自定义 Webhook URL 以接收通知。', + 'settings.webhookUrl.save': '保存', + 'settings.webhookUrl.saved': 'Webhook URL 已保存', + 'settings.webhookUrl.test': '测试', + 'settings.webhookUrl.testSuccess': '测试 Webhook 发送成功', + 'settings.webhookUrl.testFailed': '测试 Webhook 失败', + 'settings.notificationPreferences.inapp': 'In-App', + 'settings.notificationPreferences.webhook': 'Webhook', + 'settings.notificationPreferences.email': 'Email', + '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': '管理员 Webhook', + 'admin.notifications.adminWebhookPanel.hint': '此 Webhook 专用于管理员通知(如版本更新提醒)。它与用户 Webhook 相互独立,配置 URL 后自动触发。', + 'admin.notifications.adminWebhookPanel.saved': '管理员 Webhook URL 已保存', + 'admin.notifications.adminWebhookPanel.testSuccess': '测试 Webhook 发送成功', + 'admin.notifications.adminWebhookPanel.testFailed': '测试 Webhook 失败', + 'admin.notifications.adminWebhookPanel.alwaysOnHint': '配置 URL 后管理员 Webhook 自动触发', + 'admin.notifications.adminNotificationsHint': '配置哪些渠道发送管理员通知(如版本更新提醒)。设置管理员 Webhook URL 后,Webhook 将自动触发。', + 'admin.tabs.notifications': 'Notifications', + 'admin.tabs.notificationChannels': '通知渠道', + 'admin.tabs.adminNotifications': '管理员通知', + 'notifications.versionAvailable.title': 'Update Available', + 'notifications.versionAvailable.text': 'TREK {version} is now available.', + 'notifications.versionAvailable.button': 'View Details', '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?', @@ -1670,6 +1672,7 @@ const zh: Record = { '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 zh diff --git a/client/src/pages/AtlasPage.tsx b/client/src/pages/AtlasPage.tsx index 8106238..ddff4ce 100644 --- a/client/src/pages/AtlasPage.tsx +++ b/client/src/pages/AtlasPage.tsx @@ -154,7 +154,16 @@ export default function AtlasPage(): React.ReactElement { const [selectedCountry, setSelectedCountry] = useState(null) const [countryDetail, setCountryDetail] = useState(null) const [geoData, setGeoData] = useState(null) - const [confirmAction, setConfirmAction] = useState<{ type: 'mark' | 'unmark' | 'choose' | 'bucket'; code: string; name: string } | null>(null) + const [visitedRegions, setVisitedRegions] = useState>({}) + const regionLayerRef = useRef(null) + const regionGeoCache = useRef>({}) + const [showRegions, setShowRegions] = useState(false) + const [regionGeoLoaded, setRegionGeoLoaded] = useState(0) + const regionTooltipRef = useRef(null) + const loadCountryDetailRef = useRef<(code: string) => void>(() => {}) + const handleMarkCountryRef = useRef<(code: string, name: string) => void>(() => {}) + const setConfirmActionRef = useRef(() => {}) + const [confirmAction, setConfirmAction] = useState<{ type: 'mark' | 'unmark' | 'choose' | 'bucket' | 'choose-region' | 'unmark-region'; code: string; name: string; regionCode?: string; countryName?: string } | null>(null) const [bucketMonth, setBucketMonth] = useState(0) const [bucketYear, setBucketYear] = useState(0) @@ -221,6 +230,41 @@ export default function AtlasPage(): React.ReactElement { .catch(() => {}) }, []) + // Load visited regions (geocoded from places/trips) — once on mount + useEffect(() => { + apiClient.get(`/addons/atlas/regions?_t=${Date.now()}`) + .then(r => setVisitedRegions(r.data?.regions || {})) + .catch(() => {}) + }, []) + + // Load admin-1 GeoJSON for countries visible in the current viewport + const loadRegionsForViewportRef = useRef<() => void>(() => {}) + const loadRegionsForViewport = (): void => { + if (!mapInstance.current) return + const bounds = mapInstance.current.getBounds() + const toLoad: string[] = [] + for (const [code, layer] of Object.entries(country_layer_by_a2_ref.current)) { + if (regionGeoCache.current[code]) continue + try { + if (bounds.intersects((layer as any).getBounds())) toLoad.push(code) + } catch {} + } + if (!toLoad.length) return + apiClient.get(`/addons/atlas/regions/geo?countries=${toLoad.join(',')}`) + .then(geoRes => { + const geo = geoRes.data + if (!geo?.features) return + let added = false + for (const c of toLoad) { + const features = geo.features.filter((f: any) => f.properties?.iso_a2?.toUpperCase() === c) + if (features.length > 0) { regionGeoCache.current[c] = { type: 'FeatureCollection', features }; added = true } + } + if (added) setRegionGeoLoaded(v => v + 1) + }) + .catch(() => {}) + } + loadRegionsForViewportRef.current = loadRegionsForViewport + // Initialize map — runs after loading is done and mapRef is available useEffect(() => { if (loading || !mapRef.current) return @@ -230,7 +274,7 @@ export default function AtlasPage(): React.ReactElement { center: [25, 0], zoom: 3, minZoom: 3, - maxZoom: 7, + maxZoom: 10, zoomControl: false, attributionControl: false, maxBounds: [[-90, -220], [90, 220]], @@ -246,7 +290,7 @@ export default function AtlasPage(): React.ReactElement { : 'https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png' L.tileLayer(tileUrl, { - maxZoom: 8, + maxZoom: 10, keepBuffer: 25, updateWhenZooming: true, updateWhenIdle: false, @@ -257,14 +301,49 @@ export default function AtlasPage(): React.ReactElement { // Preload adjacent zoom level tiles L.tileLayer(tileUrl, { - maxZoom: 8, + maxZoom: 10, keepBuffer: 10, opacity: 0, tileSize: 256, crossOrigin: true, }).addTo(map) + // Custom pane for region layer — above overlay (z-index 400) + map.createPane('regionPane') + map.getPane('regionPane')!.style.zIndex = '401' + mapInstance.current = map + + // Zoom-based region switching + map.on('zoomend', () => { + const z = map.getZoom() + const shouldShow = z >= 5 + setShowRegions(shouldShow) + const overlayPane = map.getPane('overlayPane') + if (overlayPane) { + overlayPane.style.opacity = shouldShow ? '0.35' : '1' + overlayPane.style.pointerEvents = shouldShow ? 'none' : 'auto' + } + if (shouldShow) { + // Re-add region layer if it was removed while zoomed out + if (regionLayerRef.current && !map.hasLayer(regionLayerRef.current)) { + regionLayerRef.current.addTo(map) + } + loadRegionsForViewportRef.current() + } else { + // Physically remove region layer so its SVG paths can't intercept events + if (regionTooltipRef.current) regionTooltipRef.current.style.display = 'none' + if (regionLayerRef.current && map.hasLayer(regionLayerRef.current)) { + regionLayerRef.current.resetStyle() + regionLayerRef.current.removeFrom(map) + } + } + }) + + map.on('moveend', () => { + if (map.getZoom() >= 6) loadRegionsForViewportRef.current() + }) + return () => { map.remove(); mapInstance.current = null } }, [dark, loading]) @@ -339,10 +418,7 @@ export default function AtlasPage(): React.ReactElement { }) layer.on('click', () => { if (c.placeCount === 0 && c.tripCount === 0) { - // Manually marked only — show unmark popup handleUnmarkCountry(c.code) - } else { - loadCountryDetail(c.code) } }) layer.on('mouseover', (e) => { @@ -379,9 +455,153 @@ export default function AtlasPage(): React.ReactElement { mapInstance.current.setView(currentCenter, currentZoom, { animate: false }) }, [geoData, data, dark]) + // Render sub-national region layer (zoom >= 5) + useEffect(() => { + if (!mapInstance.current) return + + // Remove existing region layer + if (regionLayerRef.current) { + mapInstance.current.removeLayer(regionLayerRef.current) + regionLayerRef.current = null + } + + if (Object.keys(regionGeoCache.current).length === 0) return + + // Build set of visited region codes first + const visitedRegionCodes = new Set() + const visitedRegionNames = new Set() + const regionPlaceCounts: Record = {} + for (const [, regions] of Object.entries(visitedRegions)) { + for (const r of regions) { + visitedRegionCodes.add(r.code) + visitedRegionNames.add(r.name.toLowerCase()) + regionPlaceCounts[r.code] = r.placeCount + regionPlaceCounts[r.name.toLowerCase()] = r.placeCount + } + } + + // Match feature by ISO code OR region name + const isVisitedFeature = (f: any) => { + if (visitedRegionCodes.has(f.properties?.iso_3166_2)) return true + const name = (f.properties?.name || '').toLowerCase() + if (visitedRegionNames.has(name)) return true + // Fuzzy: check if any visited name is contained in feature name or vice versa + for (const vn of visitedRegionNames) { + if (name.includes(vn) || vn.includes(name)) return true + } + return false + } + + // Include ALL region features — visited ones get colored fill, unvisited get outline only + const allFeatures: any[] = [] + for (const geo of Object.values(regionGeoCache.current)) { + for (const f of geo.features) { + allFeatures.push(f) + } + } + if (allFeatures.length === 0) return + + // Use same colors as country layer + const VISITED_COLORS = ['#6366f1','#ec4899','#14b8a6','#f97316','#8b5cf6','#ef4444','#3b82f6','#22c55e','#06b6d4','#f43f5e','#a855f7','#10b981','#0ea5e9','#e11d48','#0d9488','#7c3aed','#2563eb','#dc2626','#059669','#d946ef'] + const countryA3Set = data ? data.countries.map(c => A2_TO_A3[c.code]).filter(Boolean) : [] + const countryColorMap: Record = {} + countryA3Set.forEach((a3, i) => { countryColorMap[a3] = VISITED_COLORS[i % VISITED_COLORS.length] }) + // Map country A2 code to country color + const a2ColorMap: Record = {} + if (data) data.countries.forEach(c => { if (A2_TO_A3[c.code] && countryColorMap[A2_TO_A3[c.code]]) a2ColorMap[c.code] = countryColorMap[A2_TO_A3[c.code]] }) + + const mergedGeo = { type: 'FeatureCollection', features: allFeatures } + + const svgRenderer = L.svg({ pane: 'regionPane' }) + + regionLayerRef.current = L.geoJSON(mergedGeo as any, { + renderer: svgRenderer, + interactive: true, + pane: 'regionPane', + style: (feature) => { + const countryA2 = (feature?.properties?.iso_a2 || '').toUpperCase() + const visited = isVisitedFeature(feature) + return visited ? { + fillColor: a2ColorMap[countryA2] || '#6366f1', + fillOpacity: 0.85, + color: dark ? '#888' : '#64748b', + weight: 1.2, + } : { + fillColor: dark ? '#ffffff' : '#000000', + fillOpacity: 0.03, + color: dark ? '#555' : '#94a3b8', + weight: 1, + } + }, + onEachFeature: (feature, layer) => { + const regionName = feature?.properties?.name || '' + const countryName = feature?.properties?.admin || '' + const regionCode = feature?.properties?.iso_3166_2 || '' + const countryA2 = (feature?.properties?.iso_a2 || '').toUpperCase() + const visited = isVisitedFeature(feature) + const count = regionPlaceCounts[regionCode] || regionPlaceCounts[regionName.toLowerCase()] || 0 + layer.on('click', () => { + if (!countryA2) return + if (visited) { + const regionEntry = visitedRegions[countryA2]?.find(r => r.code === regionCode) + if (regionEntry?.manuallyMarked) { + setConfirmActionRef.current({ + type: 'unmark-region', + code: countryA2, + name: regionName, + regionCode, + countryName, + }) + } else { + loadCountryDetailRef.current(countryA2) + } + } else { + setConfirmActionRef.current({ + type: 'choose-region', + code: countryA2, // country A2 code — used for flag display + name: regionName, // region name — shown as heading + regionCode, + countryName, + }) + } + }) + layer.on('mouseover', (e: any) => { + e.target.setStyle(visited + ? { fillOpacity: 0.95, weight: 2, color: dark ? '#818cf8' : '#4f46e5' } + : { fillOpacity: 0.15, fillColor: dark ? '#818cf8' : '#4f46e5', weight: 1.5, color: dark ? '#818cf8' : '#4f46e5' } + ) + const tt = regionTooltipRef.current + if (tt) { + tt.style.display = 'block' + tt.style.left = e.originalEvent.clientX + 12 + 'px' + tt.style.top = e.originalEvent.clientY - 10 + 'px' + tt.innerHTML = visited + ? `
${regionName}
${countryName}
${count} ${count === 1 ? 'place' : 'places'}
` + : `
${regionName}
${countryName}
` + } + }) + layer.on('mousemove', (e: any) => { + const tt = regionTooltipRef.current + if (tt) { tt.style.left = e.originalEvent.clientX + 12 + 'px'; tt.style.top = e.originalEvent.clientY - 10 + 'px' } + }) + layer.on('mouseout', (e: any) => { + regionLayerRef.current?.resetStyle(e.target) + const tt = regionTooltipRef.current + if (tt) tt.style.display = 'none' + }) + }, + }) + // Only add to map if currently in region mode — otherwise hold it ready for when user zooms in + if (mapInstance.current.getZoom() >= 6) { + regionLayerRef.current.addTo(mapInstance.current) + } + }, [regionGeoLoaded, visitedRegions, dark, t]) + const handleMarkCountry = (code: string, name: string): void => { setConfirmAction({ type: 'choose', code, name }) } + handleMarkCountryRef.current = handleMarkCountry + setConfirmActionRef.current = setConfirmAction const handleUnmarkCountry = (code: string): void => { const country = data?.countries.find(c => c.code === code) @@ -435,6 +655,12 @@ export default function AtlasPage(): React.ReactElement { stats: { ...prev.stats, totalCountries: Math.max(0, prev.stats.totalCountries - 1) }, } }) + setVisitedRegions(prev => { + if (!prev[code]) return prev + const next = { ...prev } + delete next[code] + return next + }) } } @@ -512,6 +738,7 @@ export default function AtlasPage(): React.ReactElement { setCountryDetail(r.data) } catch { /* */ } } + loadCountryDetailRef.current = loadCountryDetail const stats = data?.stats || { totalTrips: 0, totalPlaces: 0, totalCountries: 0, totalDays: 0 } const countries = data?.countries || [] @@ -533,6 +760,18 @@ export default function AtlasPage(): React.ReactElement {
{/* Map */}
+ + {/* Region tooltip (custom, always on top, ref-controlled to avoid re-renders) */} +
)} + {confirmAction.type === 'choose-region' && ( +
+ {confirmAction.countryName && ( +

{confirmAction.countryName}

+ )} + + +
+ )} + {confirmAction.type === 'unmark' && ( <>

{t('atlas.confirmUnmark')}

@@ -785,6 +1068,51 @@ export default function AtlasPage(): React.ReactElement { )} + {confirmAction.type === 'unmark-region' && ( + <> + {confirmAction.countryName && ( +

{confirmAction.countryName}

+ )} +

{t('atlas.confirmUnmarkRegion')}

+
+ + +
+ + )} + {confirmAction.type === 'bucket' && ( <>

{t('atlas.bucketWhen')}

@@ -815,7 +1143,7 @@ export default function AtlasPage(): React.ReactElement {
- diff --git a/server/src/db/migrations.ts b/server/src/db/migrations.ts index b4919be..f2148b5 100644 --- a/server/src/db/migrations.ts +++ b/server/src/db/migrations.ts @@ -550,7 +550,34 @@ function runMigrations(db: Database.Database): void { ); `); }, - // Migration 69: Normalized per-user per-channel notification preferences + // Migration 69: Place region cache for sub-national Atlas regions + () => { + db.exec(` + CREATE TABLE IF NOT EXISTS place_regions ( + place_id INTEGER PRIMARY KEY REFERENCES places(id) ON DELETE CASCADE, + country_code TEXT NOT NULL, + region_code TEXT NOT NULL, + region_name TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_place_regions_country ON place_regions(country_code); + CREATE INDEX IF NOT EXISTS idx_place_regions_region ON place_regions(region_code); + `); + }, + () => { + db.exec(` + CREATE TABLE IF NOT EXISTS visited_regions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + region_code TEXT NOT NULL, + region_name TEXT NOT NULL, + country_code TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, region_code) + ); + CREATE INDEX IF NOT EXISTS idx_visited_regions_country ON visited_regions(country_code); + `); + }, + // Migration 71: Normalized per-user per-channel notification preferences () => { db.exec(` CREATE TABLE IF NOT EXISTS notification_channel_preferences ( @@ -605,7 +632,7 @@ function runMigrations(db: Database.Database): void { SELECT 'notification_channels', value FROM app_settings WHERE key = 'notification_channel'; `); }, - // Migration 70: Drop the old notification_preferences table (data migrated to notification_channel_preferences in migration 69) + // Migration 72: Drop the old notification_preferences table (data migrated to notification_channel_preferences in migration 71) () => { db.exec('DROP TABLE IF EXISTS notification_preferences;'); }, diff --git a/server/src/routes/atlas.ts b/server/src/routes/atlas.ts index 9929c3d..d5af8d8 100644 --- a/server/src/routes/atlas.ts +++ b/server/src/routes/atlas.ts @@ -6,6 +6,10 @@ import { getCountryPlaces, markCountryVisited, unmarkCountryVisited, + markRegionVisited, + unmarkRegionVisited, + getVisitedRegions, + getRegionGeo, listBucketList, createBucketItem, updateBucketItem, @@ -21,6 +25,21 @@ router.get('/stats', async (req: Request, res: Response) => { res.json(data); }); +router.get('/regions', async (req: Request, res: Response) => { + const userId = (req as AuthRequest).user.id; + res.setHeader('Cache-Control', 'no-cache, no-store'); + const data = await getVisitedRegions(userId); + res.json(data); +}); + +router.get('/regions/geo', async (req: Request, res: Response) => { + const countries = (req.query.countries as string || '').split(',').filter(Boolean); + if (countries.length === 0) return res.json({ type: 'FeatureCollection', features: [] }); + const geo = await getRegionGeo(countries); + res.setHeader('Cache-Control', 'public, max-age=86400'); + res.json(geo); +}); + router.get('/country/:code', (req: Request, res: Response) => { const userId = (req as AuthRequest).user.id; const code = req.params.code.toUpperCase(); @@ -39,6 +58,20 @@ router.delete('/country/:code/mark', (req: Request, res: Response) => { res.json({ success: true }); }); +router.post('/region/:code/mark', (req: Request, res: Response) => { + const userId = (req as AuthRequest).user.id; + const { name, country_code } = req.body; + if (!name || !country_code) return res.status(400).json({ error: 'name and country_code are required' }); + markRegionVisited(userId, req.params.code.toUpperCase(), name, country_code.toUpperCase()); + res.json({ success: true }); +}); + +router.delete('/region/:code/mark', (req: Request, res: Response) => { + const userId = (req as AuthRequest).user.id; + unmarkRegionVisited(userId, req.params.code.toUpperCase()); + res.json({ success: true }); +}); + // ── Bucket List ───────────────────────────────────────────────────────────── router.get('/bucket-list', (req: Request, res: Response) => { diff --git a/server/src/services/atlasService.ts b/server/src/services/atlasService.ts index 11a317b..93779dd 100644 --- a/server/src/services/atlasService.ts +++ b/server/src/services/atlasService.ts @@ -2,6 +2,38 @@ import fetch from 'node-fetch'; import { db } from '../db/database'; import { Trip, Place } from '../types'; +// ── Admin-1 GeoJSON cache (sub-national regions) ───────────────────────── + +let admin1GeoCache: any = null; +let admin1GeoLoading: Promise | null = null; + +async function loadAdmin1Geo(): Promise { + if (admin1GeoCache) return admin1GeoCache; + if (admin1GeoLoading) return admin1GeoLoading; + admin1GeoLoading = fetch( + 'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_10m_admin_1_states_provinces.geojson', + { headers: { 'User-Agent': 'TREK Travel Planner' } } + ).then(r => r.json()).then(geo => { + admin1GeoCache = geo; + admin1GeoLoading = null; + console.log(`[Atlas] Cached admin-1 GeoJSON: ${geo.features?.length || 0} features`); + return geo; + }).catch(err => { + admin1GeoLoading = null; + console.error('[Atlas] Failed to load admin-1 GeoJSON:', err); + return null; + }); + return admin1GeoLoading; +} + +export async function getRegionGeo(countryCodes: string[]): Promise { + const geo = await loadAdmin1Geo(); + if (!geo) return { type: 'FeatureCollection', features: [] }; + const codes = new Set(countryCodes.map(c => c.toUpperCase())); + const features = geo.features.filter((f: any) => codes.has(f.properties?.iso_a2?.toUpperCase())); + return { type: 'FeatureCollection', features }; +} + // ── Geocode cache ─────────────────────────────────────────────────────────── const geocodeCache = new Map(); @@ -339,6 +371,126 @@ export function markCountryVisited(userId: number, code: string): void { export function unmarkCountryVisited(userId: number, code: string): void { db.prepare('DELETE FROM visited_countries WHERE user_id = ? AND country_code = ?').run(userId, code); + db.prepare('DELETE FROM visited_regions WHERE user_id = ? AND country_code = ?').run(userId, code); +} + +// ── Mark / unmark region ──────────────────────────────────────────────────── + +export function listManuallyVisitedRegions(userId: number): { region_code: string; region_name: string; country_code: string }[] { + return db.prepare( + 'SELECT region_code, region_name, country_code FROM visited_regions WHERE user_id = ? ORDER BY created_at DESC' + ).all(userId) as { region_code: string; region_name: string; country_code: string }[]; +} + +export function markRegionVisited(userId: number, regionCode: string, regionName: string, countryCode: string): void { + db.prepare('INSERT OR IGNORE INTO visited_regions (user_id, region_code, region_name, country_code) VALUES (?, ?, ?, ?)').run(userId, regionCode, regionName, countryCode); + // Auto-mark parent country if not already visited + db.prepare('INSERT OR IGNORE INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(userId, countryCode); +} + +export function unmarkRegionVisited(userId: number, regionCode: string): void { + const region = db.prepare('SELECT country_code FROM visited_regions WHERE user_id = ? AND region_code = ?').get(userId, regionCode) as { country_code: string } | undefined; + db.prepare('DELETE FROM visited_regions WHERE user_id = ? AND region_code = ?').run(userId, regionCode); + if (region) { + const remaining = db.prepare('SELECT COUNT(*) as count FROM visited_regions WHERE user_id = ? AND country_code = ?').get(userId, region.country_code) as { count: number }; + if (remaining.count === 0) { + db.prepare('DELETE FROM visited_countries WHERE user_id = ? AND country_code = ?').run(userId, region.country_code); + } + } +} + +// ── Sub-national region resolution ──────────────────────────────────────── + +interface RegionInfo { country_code: string; region_code: string; region_name: string } + +const regionCache = new Map(); + +async function reverseGeocodeRegion(lat: number, lng: number): Promise { + const key = roundKey(lat, lng); + if (regionCache.has(key)) return regionCache.get(key)!; + try { + const res = await fetch( + `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&zoom=8&accept-language=en`, + { headers: { 'User-Agent': 'TREK Travel Planner' } } + ); + if (!res.ok) return null; + const data = await res.json() as { address?: Record }; + const countryCode = data.address?.country_code?.toUpperCase() || null; + // Try finest ISO level first (lvl6 = departments/provinces), then lvl5, then lvl4 (states/regions) + let regionCode = data.address?.['ISO3166-2-lvl6'] || data.address?.['ISO3166-2-lvl5'] || data.address?.['ISO3166-2-lvl4'] || null; + // Normalize: FR-75C → FR-75 (strip trailing letter suffixes for GeoJSON compatibility) + if (regionCode && /^[A-Z]{2}-\d+[A-Z]$/i.test(regionCode)) { + regionCode = regionCode.replace(/[A-Z]$/i, ''); + } + const regionName = data.address?.county || data.address?.state || data.address?.province || data.address?.region || data.address?.city || null; + if (!countryCode || !regionName) { regionCache.set(key, null); return null; } + const info: RegionInfo = { + country_code: countryCode, + region_code: regionCode || `${countryCode}-${regionName.substring(0, 3).toUpperCase()}`, + region_name: regionName, + }; + regionCache.set(key, info); + return info; + } catch { + return null; + } +} + +export async function getVisitedRegions(userId: number): Promise<{ regions: Record }> { + const trips = getUserTrips(userId); + const tripIds = trips.map(t => t.id); + const places = getPlacesForTrips(tripIds); + + // Check DB cache first + const placeIds = places.filter(p => p.lat && p.lng).map(p => p.id); + const cached = placeIds.length > 0 + ? db.prepare(`SELECT * FROM place_regions WHERE place_id IN (${placeIds.map(() => '?').join(',')})`).all(...placeIds) as { place_id: number; country_code: string; region_code: string; region_name: string }[] + : []; + const cachedMap = new Map(cached.map(c => [c.place_id, c])); + + // Resolve uncached places (rate-limited to avoid hammering Nominatim) + const uncached = places.filter(p => p.lat && p.lng && !cachedMap.has(p.id)); + const insertStmt = db.prepare('INSERT OR REPLACE INTO place_regions (place_id, country_code, region_code, region_name) VALUES (?, ?, ?, ?)'); + + for (const place of uncached) { + const info = await reverseGeocodeRegion(place.lat!, place.lng!); + if (info) { + insertStmt.run(place.id, info.country_code, info.region_code, info.region_name); + cachedMap.set(place.id, { place_id: place.id, ...info }); + } + // Nominatim rate limit: 1 req/sec + if (uncached.indexOf(place) < uncached.length - 1) { + await new Promise(r => setTimeout(r, 1100)); + } + } + + // Group by country → regions with place counts + const regionMap: Record> = {}; + for (const [, entry] of cachedMap) { + if (!regionMap[entry.country_code]) regionMap[entry.country_code] = new Map(); + const existing = regionMap[entry.country_code].get(entry.region_code); + if (existing) { + existing.placeCount++; + } else { + regionMap[entry.country_code].set(entry.region_code, { code: entry.region_code, name: entry.region_name, placeCount: 1 }); + } + } + + const result: Record = {}; + for (const [country, regions] of Object.entries(regionMap)) { + result[country] = [...regions.values()]; + } + + // Merge manually marked regions + const manualRegions = listManuallyVisitedRegions(userId); + for (const r of manualRegions) { + if (!result[r.country_code]) result[r.country_code] = []; + if (!result[r.country_code].find(x => x.code === r.region_code)) { + result[r.country_code].push({ code: r.region_code, name: r.region_name, placeCount: 0, manuallyMarked: true }); + } + } + + return { regions: result }; } // ── Bucket list CRUD ────────────────────────────────────────────────────────