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:
@@ -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;
|
||||
|
||||
@@ -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, ' ')
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user