fix(i18n): sweep activity-type translations + smart notification fallback

The admin notification bell and dashboard "Recent Activities" panel were
showing raw snake_case keys ("event_published") or the generic
"Systemaktivität: <type>" fallback for ~65 activity types — most of them
from the CRM and Accounting modules added since #555. Users with German
locale saw the gap most visibly because the English placeholder leaked
through.

Three pieces:

1. notifications.service.ts — smart `default:` branch. Instead of falling
   straight to the systemActivity template, derive the camelCase i18n key
   from the snake_case type, try resolving `admin.notificationMessages.<camelCase>`
   directly with the full metadata spread as params, and only drop to the
   legacy template when no specific translation exists. This means every
   future activity type just needs an i18n entry — no per-type switch
   case to add.

2. en.json + de.json — added 65 missing `admin.notificationMessages.*`
   bell entries and 58 missing `admin.activities.*` dashboard entries
   across both locales. Covers Contracts (13), Quotes (7), Invoices /
   Storno (12), Monthly billing (5), Expenses (4), Hours (5), Incoming
   invoices (6), Customers (1), Admin user mgmt (3), and 9 misc /
   legacy types (bulk_archive_completed, email_resent, email_queue_flushed,
   email_template_created, event_duplicated, feedback_deleted,
   feedback_moderated, feedback_settings_updated, word_filter_added).
   Both locales finish symmetrical (149 activities / 136 notifications
   each, vs. 91 / 71 before).

3. admin.service.ts `formatActivityMessage` messages dict — added the
   same 58 English-only entries as a last-resort fallback for the
   dashboard when i18n itself fails to load. Keeps the surface
   resilient against bundle-load issues.

Metadata field names in the new translations match what the backend
writes via `logActivity()` — `{{contractNumber}}`, `{{quoteNumber}}`,
`{{invoiceNumber}}`, `{{username}}`, `{{template_key}}`,
`{{source_event_name}}`, `{{word}}` — verified against the call sites
in contractService, quoteService, invoiceService, userManagementService,
expenseService, adminEvents, adminEmail, adminFeedback.

DE wording authored natively; tone matches the existing terse,
maintainer-voice style of the rest of the file.
This commit is contained in:
Paul Nothaft
2026-06-18 21:10:12 +02:00
parent 9df2502547
commit bc8d3330bb
4 changed files with 413 additions and 30 deletions
+72
View File
@@ -445,6 +445,78 @@ export const adminService = {
'event_type_updated': `Event type updated: ${md.name || ''}`,
'event_type_deleted': `Event type deleted: ${md.name || ''}`,
'event_types_reordered': 'Event types reordered',
// CRM — Contracts.
'contract_created': `Contract created: ${md.contractNumber || ''}`,
'contract_created_from_quote': `Contract created from quote: ${md.contractNumber || ''}`,
'contract_updated': `Contract updated: ${md.contractNumber || ''}`,
'contract_sent': `Contract sent: ${md.contractNumber || ''}`,
'contract_resent_signed': `Signed contract resent: ${md.contractNumber || ''}`,
'contract_signed_by_customer': `Contract signed by customer: ${md.contractNumber || ''}`,
'contract_signed_pdf_uploaded': `Signed contract PDF uploaded: ${md.contractNumber || ''}`,
'contract_signatures_restamped': `Contract signatures re-stamped: ${md.contractNumber || ''}`,
'contract_cancelled': `Contract cancelled: ${md.contractNumber || ''}`,
'contract_converted_to_event': `Contract converted to event: ${md.contractNumber || ''}`,
'contract_converted_to_empty_event': `Contract converted to empty event: ${md.contractNumber || ''}`,
'contract_converted_to_invoices': `Contract converted to invoices: ${md.contractNumber || ''}`,
'contract_converted_to_empty_invoice': `Contract converted to empty invoice: ${md.contractNumber || ''}`,
// CRM — Quotes.
'quote_created': `Quote created: ${md.quoteNumber || ''}`,
'quote_sent': `Quote sent: ${md.quoteNumber || ''}`,
'quote_updated': `Quote updated: ${md.quoteNumber || ''}`,
'quote_accepted_by_admin': `Quote accepted: ${md.quoteNumber || ''}`,
'quote_declined_by_admin': `Quote declined: ${md.quoteNumber || ''}`,
'quote_converted': `Quote converted: ${md.quoteNumber || ''}`,
'quote_converted_invoices_only': `Quote converted to invoices: ${md.quoteNumber || ''}`,
// CRM — Invoices / Storno.
'invoice_created': `Invoice created: ${md.invoiceNumber || ''}`,
'invoice_sent': `Invoice sent: ${md.invoiceNumber || ''}`,
'invoice_scheduled': `Invoice scheduled: ${md.invoiceNumber || ''}`,
'invoice_cancelled': `Invoice cancelled: ${md.invoiceNumber || ''}`,
'invoice_cancelled_via_storno': `Invoice cancelled via Storno: ${md.invoiceNumber || ''}`,
'invoice_reissued': `Invoice reissued: ${md.invoiceNumber || ''}`,
'invoice_paid_admin_notified': `Invoice marked paid: ${md.invoiceNumber || ''}`,
'invoice_payment_check_recorded': `Payment-check recorded for invoice: ${md.invoiceNumber || ''}`,
'invoice_payment_check_sent': `Payment-check sent for invoice: ${md.invoiceNumber || ''}`,
'invoice_released_for_delivery': `Invoice released for delivery: ${md.invoiceNumber || ''}`,
'invoice_reminder_sent': `Invoice reminder sent: ${md.invoiceNumber || ''}`,
'storno_sent': `Storno sent: ${md.invoiceNumber || ''}`,
// CRM — Monthly billing.
'monthly_bill_issued': 'Monthly bill issued for customer',
'monthly_bill_skipped_empty': 'Monthly bill skipped (no entries)',
'monthly_bill_triggered_manually': 'Monthly bill triggered manually',
'monthly_billing_items_queued': 'Monthly billing items queued',
'installment_plan_updated': 'Installment plan updated',
// Accounting — Expenses + Hours + Incoming invoices.
'expense_created': 'Expense created',
'expense_updated': 'Expense updated',
'expense_paid': 'Expense marked paid',
'expense_invoiced': 'Expense invoiced',
'hour_entry_logged': 'Hour entry logged',
'hour_entry_updated': 'Hour entry updated',
'hour_entry_deleted': 'Hour entry deleted',
'hour_entry_logged_to_monthly_draft': 'Hour entry logged to monthly draft',
'hour_entries_billed': 'Hour entries billed to customer',
'incoming_invoice_captured': 'Incoming invoice captured',
'incoming_invoice_categorized': 'Incoming invoice categorised',
'incoming_invoice_updated': 'Incoming invoice updated',
'incoming_invoice_rebilled': 'Incoming invoice re-billed to customer',
'incoming_invoice_supplier_payment': 'Supplier payment recorded',
'incoming_mail_config_updated': 'Incoming mail configuration updated',
// Customers + Admin user mgmt.
'customer_created_passive': `Passive customer created: ${md.email || ''}`,
'admin_user_activated': `Admin user activated: ${md.username || ''}`,
'admin_user_deleted': `Admin user deleted: ${md.username || ''}`,
'admin_password_reset': `Admin password reset: ${md.username || ''}`,
// Misc / legacy.
'bulk_archive_completed': `Bulk archive completed: ${md.count || 0} events archived`,
'email_queue_flushed': 'Email queue flushed',
'email_resent': `Creation email resent for ${activity.eventName || ''}`,
'email_template_created': `Email template created: ${md.template_key || ''}`,
'event_duplicated': `Event duplicated from ${md.source_event_name || ''}`,
'feedback_deleted': 'Feedback deleted',
'feedback_moderated': 'Feedback moderated',
'feedback_settings_updated': `Feedback settings updated for ${activity.eventName || ''}`,
'word_filter_added': `Word filter added: ${md.word || ''}`,
};
return messages[activity.type] || activity.type;
+17 -2
View File
@@ -288,12 +288,27 @@ export const notificationsService = {
slug: notification.metadata.slug,
});
default:
// Log unknown notification types for debugging
default: {
// Smart fallback: try to resolve admin.notificationMessages.<camelCase>
// directly before giving up to the systemActivity template. Most CRM
// and accounting activity types follow a stable shape (a doc number /
// customer email / count interpolated into a short sentence), so we
// add their translation entries to the locale files and rely on this
// default to pick them up — no per-type switch case needed.
const camelCase = notification.type.replace(/_(\w)/g, (_match, c) => c.toUpperCase());
const key = `admin.notificationMessages.${camelCase}`;
const translated = t(key, {
eventName: notification.eventName,
actorName: notification.actorName,
...notification.metadata,
});
if (translated && translated !== key) return translated as string;
// Truly unknown — log and render the legacy systemActivity template.
console.warn('Unknown notification type:', notification.type, notification);
return notification.metadata.message || t('admin.notificationMessages.systemActivity', {
type: notification.type.replace(/_/g, ' ')
});
}
}
},