Parcourir la source

Say the audit log's details in the reader's language (#258)

The Details column stored a finished English sentence. It was composed once,
at 115 call sites, and saved as characters, so nothing downstream knew what
its words meant. A Norwegian workshop read a Norwegian action label with an
English sentence beside it, on every row, and no amount of translating could
change that after the fact.

Each site now names a key and passes its values, and the page assembles the
sentence when it renders. Every enum that used to print raw goes through an
ICU select, so a status, a channel, a role and a treatment type all read as
words rather than as column values.

117 sentences in twelve languages. The count-carrying ones are real ICU
plurals, which matters most where English has nothing to teach: Polish,
Russian and Lithuanian each pick a different form for 1, 3, 11 and 21.

Nothing needed a migration. The key and its values ride in metadata, the
English is still rendered from the same catalogue and written to the row, so
the audit search and webhook subscribers see what they always did, and rows
written before this keep their sentence as the fallback.

A test compiles all 1404 strings. It found one that would have thrown on the
page in every language: ICU select labels cannot contain a hyphen, and the
service statuses in-progress and waiting-parts both do.
Bernt Christian Egeland il y a 1 mois
Parent
commit
7a18e3314c
63 fichiers modifiés avec 1809 ajouts et 183 suppressions
  1. 119 0
      messages/de/audit.json
  2. 119 0
      messages/en/audit.json
  3. 119 0
      messages/es/audit.json
  4. 119 0
      messages/fr/audit.json
  5. 119 0
      messages/it/audit.json
  6. 119 0
      messages/lt/audit.json
  7. 119 0
      messages/nb/audit.json
  8. 119 0
      messages/nl/audit.json
  9. 119 0
      messages/pl/audit.json
  10. 119 0
      messages/pt-BR/audit.json
  11. 119 0
      messages/ru/audit.json
  12. 119 0
      messages/tr/audit.json
  13. 6 28
      src/__tests__/features/tire-hotel/audit-entries.test.ts
  14. 137 0
      src/__tests__/i18n/audit-summaries.test.ts
  15. 24 3
      src/app/(authenticated)/audit-log/audit-log-client.tsx
  16. 3 3
      src/features/billing/Actions/recurringInvoiceActions.ts
  17. 3 3
      src/features/custom-fields/Actions/customFieldActions.ts
  18. 4 4
      src/features/customers/Actions/customerActions.ts
  19. 3 3
      src/features/email/Actions/emailActions.ts
  20. 5 5
      src/features/inspections/Actions/inspectionActions.ts
  21. 1 1
      src/features/inspections/Actions/packageActions.ts
  22. 1 1
      src/features/inspections/Actions/quoteRequestActions.ts
  23. 5 5
      src/features/inspections/Actions/templateActions.ts
  24. 4 4
      src/features/inventory/Actions/inventoryActions.ts
  25. 3 3
      src/features/labor-presets/Actions/laborPresetActions.ts
  26. 1 1
      src/features/onboarding/Actions/checklistActions.ts
  27. 2 2
      src/features/payments/Actions/paymentActions.ts
  28. 5 5
      src/features/quotes/Actions/quoteActions.ts
  29. 5 5
      src/features/scheduled-messages/Actions/scheduledMessageActions.ts
  30. 3 3
      src/features/settings/Actions/applyTaxRateToExisting.ts
  31. 1 1
      src/features/settings/Actions/deleteContent.ts
  32. 2 2
      src/features/settings/Actions/invoiceLayoutActions.ts
  33. 1 1
      src/features/sms/Actions/smsActions.ts
  34. 1 1
      src/features/status-reports/Actions/createStatusReport.ts
  35. 1 1
      src/features/status-reports/Actions/deleteStatusReport.ts
  36. 1 1
      src/features/status-reports/Actions/sendStatusReport.ts
  37. 2 2
      src/features/subscription/Actions/subscriptionActions.ts
  38. 1 1
      src/features/team/Actions/cancelInvitation.ts
  39. 1 1
      src/features/team/Actions/createNewOrganization.ts
  40. 1 1
      src/features/team/Actions/createRole.ts
  41. 1 1
      src/features/team/Actions/deleteRole.ts
  42. 1 1
      src/features/team/Actions/sendInvitation.ts
  43. 3 3
      src/features/team/Actions/teamActions.ts
  44. 1 1
      src/features/team/Actions/updateRole.ts
  45. 1 1
      src/features/telegram/Actions/telegramActions.ts
  46. 2 3
      src/features/tire-hotel/Actions/attachmentActions.ts
  47. 1 1
      src/features/tire-hotel/Actions/messageCustomerActions.ts
  48. 11 8
      src/features/tire-hotel/Actions/storageActions.ts
  49. 4 4
      src/features/tire-hotel/Actions/tireJobActions.ts
  50. 18 9
      src/features/tire-hotel/Actions/tireSetActions.ts
  51. 3 4
      src/features/tire-hotel/Actions/treatmentActions.ts
  52. 0 29
      src/features/tire-hotel/Lib/auditText.ts
  53. 1 1
      src/features/vehicles/Actions/archiveVehicle.ts
  54. 2 2
      src/features/vehicles/Actions/createDraftServiceRecord.ts
  55. 4 4
      src/features/vehicles/Actions/findingActions.ts
  56. 2 2
      src/features/vehicles/Actions/noteActions.ts
  57. 2 2
      src/features/vehicles/Actions/reminderActions.ts
  58. 9 4
      src/features/vehicles/Actions/serviceActions.ts
  59. 1 1
      src/features/vehicles/Actions/unarchiveVehicle.ts
  60. 9 3
      src/features/vehicles/Actions/vehicleActions.ts
  61. 1 1
      src/features/webhooks/Actions/webhookActions.ts
  62. 3 3
      src/features/workboard/Actions/technicianActions.ts
  63. 74 4
      src/lib/audit.ts

+ 119 - 0
messages/de/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Reifensatz ausgemustert",
     "tire_set_dispose": "Reifensatz ausgemustert",
     "tire_set_attach": "Dateien zu einem Reifensatz hinzugefügt",
     "tire_set_attach": "Dateien zu einem Reifensatz hinzugefügt",
     "tire_set_detach": "Datei von einem Reifensatz entfernt"
     "tire_set_detach": "Datei von einem Reifensatz entfernt"
+  },
+  "summary": {
+    "customField_create": "Benutzerdefiniertes Feld „{name}“ erstellt",
+    "customField_delete": "Benutzerdefiniertes Feld „{name}“ gelöscht",
+    "customField_update": "Benutzerdefiniertes Feld „{name}“ aktualisiert",
+    "customer_backfillNumbers": "Kundennummern an {count, plural, one {# Kunden} other {# Kunden}} vergeben",
+    "customer_create": "Kunde {name} angelegt",
+    "customer_delete": "Kunde {id} gelöscht",
+    "customer_update": "Kunde {id} aktualisiert",
+    "email_sendInspection": "Prüfbericht per E-Mail an {recipient} gesendet",
+    "email_sendInvoice": "Rechnung per E-Mail an {recipient} gesendet",
+    "email_sendQuote": "Angebot per E-Mail an {recipient} gesendet",
+    "finding_create": "Befund „{description}“ am Fahrzeug {vehicleId} angelegt",
+    "finding_delete": "Befund {id} gelöscht",
+    "finding_resolve": "Befund „{description}“ erledigt",
+    "finding_update": "Befund „{description}“ aktualisiert",
+    "inspectionTemplate_create": "Prüfvorlage „{name}“ erstellt",
+    "inspectionTemplate_createFromPreset": "Prüfvorlage „{name}“ aus einer Voreinstellung erstellt",
+    "inspectionTemplate_delete": "Prüfvorlage „{name}“ gelöscht",
+    "inspectionTemplate_duplicate": "Prüfvorlage nach „{name}“ dupliziert",
+    "inspectionTemplate_import": "{count, plural, one {# Prüfvorlage} other {# Prüfvorlagen}} aus einem Paket importiert",
+    "inspectionTemplate_update": "Prüfvorlage „{name}“ aktualisiert",
+    "inspection_complete": "Prüfung {id} abgeschlossen",
+    "inspection_create": "Prüfung {id} angelegt",
+    "inspection_delete": "Prüfung {id} gelöscht",
+    "inspection_reopen": "Prüfung {id} wieder geöffnet",
+    "inventory_bulkDelete": "{count, plural, one {# Lagerteil} other {# Lagerteile}} im Stapel gelöscht",
+    "inventory_create": "Lagerteil „{name}“ angelegt",
+    "inventory_delete": "Lagerteil {id} gelöscht",
+    "inventory_update": "Lagerteil {id} aktualisiert",
+    "labor_preset_create": "Arbeitsvorlage „{name}“ erstellt",
+    "labor_preset_delete": "Arbeitsvorlage {id} gelöscht",
+    "labor_preset_update": "Arbeitsvorlage {id} aktualisiert",
+    "note_create": "Notiz am Fahrzeug {vehicleId} angelegt",
+    "note_delete": "Notiz {id} gelöscht",
+    "organization_create": "Organisation „{name}“ angelegt",
+    "organization_sampleDataRemove": "Beispieldaten aus der Einrichtung entfernt",
+    "payment_create": "Zahlung {amount} zum Auftrag {serviceRecordId} erfasst",
+    "payment_delete": "Zahlung {id} gelöscht",
+    "quoteRequest_update": "Status der Angebotsanfrage geändert auf {status, select, pending {offen} approved {genehmigt} rejected {abgelehnt} completed {abgeschlossen} other {{status}}}",
+    "quote_convert": "Angebot {ref} in Auftrag {serviceRecordId} umgewandelt",
+    "quote_create": "Angebot {ref} erstellt",
+    "quote_delete": "Angebot {ref} gelöscht",
+    "quote_status": "Angebotsstatus geändert auf {status, select, draft {Entwurf} sent {gesendet} accepted {angenommen} rejected {abgelehnt} expired {abgelaufen} converted {umgewandelt} other {{status}}}",
+    "quote_update": "Angebot {ref} aktualisiert",
+    "recurringInvoice_create": "Wiederkehrende Rechnung „{title}“ erstellt",
+    "recurringInvoice_delete": "Wiederkehrende Rechnung {id} gelöscht",
+    "recurringInvoice_update": "Wiederkehrende Rechnung „{title}“ aktualisiert",
+    "reminder_create": "Erinnerung „{title}“ erstellt",
+    "reminder_delete": "Erinnerung {id} gelöscht",
+    "role_create": "Rolle „{name}“ erstellt",
+    "role_delete": "Rolle „{name}“ gelöscht",
+    "role_update": "Rolle „{name}“ aktualisiert",
+    "scheduled_message_cancel": "Geplante Nachricht abgebrochen",
+    "scheduled_message_create": "{channel, select, email {E-Mail} sms {SMS} telegram {Telegram-Nachricht} in_app {Hinweis in der App} other {{channel}}} für {sendAt} geplant",
+    "scheduled_message_delete": "Geplante Nachricht gelöscht",
+    "scheduled_message_send_now": "Geplante {channel, select, email {E-Mail} sms {SMS} telegram {Telegram-Nachricht} in_app {Hinweis in der App} other {{channel}}} vorzeitig gesendet",
+    "scheduled_message_update": "Geplante {channel, select, email {E-Mail} sms {SMS} telegram {Telegram-Nachricht} in_app {Hinweis in der App} other {{channel}}} aktualisiert",
+    "service_create": "Auftrag {ref} angelegt",
+    "service_createCounterSale": "Thekenverkauf {ref} angelegt",
+    "service_createDraft": "Auftragsentwurf {ref} angelegt",
+    "service_createFromInspection": "Arbeitsauftrag {ref} aus einer Prüfung angelegt",
+    "service_delete": "Auftrag {ref} gelöscht",
+    "service_status": "Auftragsstatus geändert auf {status, select, pending {offen} scheduled {geplant} in_progress {in Arbeit} waiting_parts {wartet auf Teile} ready {fertig} completed {abgeschlossen} cancelled {storniert} other {{status}}}",
+    "service_update": "Auftrag {ref} aktualisiert",
+    "settings_applyTaxRate": "{rate} % Steuer auf {records, plural, one {# Auftrag} other {# Aufträge}} und {quotes, plural, one {# Angebot} other {# Angebote}} angewendet",
+    "settings_convertToExclusive": "{records, plural, one {# Auftrag} other {# Aufträge}} und {quotes, plural, one {# Angebot} other {# Angebote}} auf Nettopreise umgestellt",
+    "settings_convertToInclusive": "{records, plural, one {# Auftrag} other {# Aufträge}} und {quotes, plural, one {# Angebot} other {# Angebote}} auf Bruttopreise umgestellt",
+    "settings_deleteContent": "Alle Daten gelöscht: {items}",
+    "settings_updateInvoiceLayout": "Rechnungslayout aktualisiert",
+    "settings_updateQuoteLayout": "Angebotslayout aktualisiert",
+    "sms_send": "SMS an {name} ({phone}) gesendet",
+    "statusReport_create": "Statusbericht zum Auftrag {serviceRecordId} erstellt",
+    "statusReport_delete": "Statusbericht gelöscht",
+    "statusReport_send": "Statusbericht über {channels} gesendet",
+    "subscription_cancel": "Abonnement zum Ende der Laufzeit gekündigt",
+    "subscription_resume": "Abonnement fortgesetzt",
+    "team_cancelInvitation": "Einladung für {email} zurückgezogen",
+    "team_removeMember": "Teammitglied {id} entfernt",
+    "team_sendInvitation": "{email} als {role, select, owner {Inhaber} admin {Administrator} member {Mitglied} other {{role}}} eingeladen",
+    "team_updateRole": "Rolle eines Mitglieds geändert auf {role, select, owner {Inhaber} admin {Administrator} member {Mitglied} other {{role}}}",
+    "technician_create": "Mechaniker „{name}“ angelegt",
+    "technician_delete": "Mechaniker {id} gelöscht",
+    "technician_update": "Mechaniker {id} aktualisiert",
+    "telegram_send": "Telegram-Nachricht an {name} gesendet",
+    "tire_location_bulk_create": "{count, plural, one {# Reifenlagerplatz} other {# Reifenlagerplätze}} angelegt",
+    "tire_location_create": "Reifenlagerplatz {code} angelegt",
+    "tire_location_delete": "Reifenlagerplatz {code} gelöscht",
+    "tire_location_update": "Reifenlagerplatz {code} aktualisiert",
+    "tire_measurement_create": "{count, plural, one {# Messung} other {# Messungen}} am Reifensatz {ref} erfasst",
+    "tire_set_add_to_work_order": "Reifensatz {ref} zum Arbeitsauftrag {jobRef} hinzugefügt",
+    "tire_set_attach": "{count, plural, one {# Datei} other {# Dateien}} zum Reifensatz {ref} hinzugefügt",
+    "tire_set_check_in": "Reifensatz {ref} auf {code} eingelagert",
+    "tire_set_check_out": "Reifensatz {ref} ausgegeben",
+    "tire_set_create_quote": "Angebot {ref} aus einem Reifensatz erstellt",
+    "tire_set_create_work_order": "Arbeitsauftrag {ref} aus einem Reifensatz erstellt",
+    "tire_set_delete": "Reifensatz {ref} gelöscht",
+    "tire_set_detach": "{fileName} von einem Reifensatz entfernt",
+    "tire_set_dispose": "Reifensatz {ref} entsorgt",
+    "tire_set_message_customer": "Kunden über Reifensatz {ref} per {channel, select, email {E-Mail} sms {SMS} telegram {Telegram} in_app {Hinweis in der App} other {{channel}}} benachrichtigt",
+    "tire_set_relocate": "Reifensatz {ref} von {from} nach {to} umgeräumt",
+    "tire_set_relocateUnassigned": "Reifensatz {ref} auf {to} gestellt",
+    "tire_set_return": "Reifensatz {ref} wieder eingelagert, auf {code}",
+    "tire_set_unlink_work_order": "Reifensatz {ref} vom Arbeitsauftrag {jobRef} getrennt",
+    "tire_set_update": "Reifensatz {ref} aktualisiert",
+    "tire_treatment_complete_all": "{count, plural, one {# Vorbereitungsarbeit} other {# Vorbereitungsarbeiten}} am Reifensatz {ref} abgeschlossen",
+    "tire_treatment_mark": "{type, select, wash_tires {Reifenwäsche} wash_rims {Felgenwäsche} balance {Auswuchten} tpms_service {RDKS-Service} new_valves {neue Ventile} repair {Reparatur} other {{type}}} am Reifensatz {ref} als {status, select, done {erledigt} pending {offen} skipped {übersprungen} other {{status}}} markiert",
+    "tire_treatment_update": "Vorbereitung am Reifensatz {ref} aktualisiert",
+    "tire_warehouse_archive": "Reifenlager {name} archiviert",
+    "tire_warehouse_create": "Reifenlager {name} angelegt",
+    "tire_warehouse_delete": "Reifenlager {name} gelöscht",
+    "tire_warehouse_update": "Reifenlager {name} aktualisiert",
+    "vehicle_archive": "Fahrzeug {id} archiviert",
+    "vehicle_create": "Fahrzeug {year} {make} {model} angelegt",
+    "vehicle_delete": "Fahrzeug {name} gelöscht",
+    "vehicle_unarchive": "Fahrzeug {id} aus dem Archiv geholt",
+    "vehicle_update": "Fahrzeug {name} aktualisiert, geändert: {fields}",
+    "vehicle_updateNoChanges": "Fahrzeug {name} aktualisiert, nichts geändert",
+    "webhook_create": "Webhook {name} erstellt"
   }
   }
 }
 }

+ 119 - 0
messages/en/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Wrote off a tire set",
     "tire_set_dispose": "Wrote off a tire set",
     "tire_set_attach": "Added files to a tire set",
     "tire_set_attach": "Added files to a tire set",
     "tire_set_detach": "Removed a file from a tire set"
     "tire_set_detach": "Removed a file from a tire set"
+  },
+  "summary": {
+    "customField_create": "Created custom field \"{name}\"",
+    "customField_delete": "Deleted custom field \"{name}\"",
+    "customField_update": "Updated custom field \"{name}\"",
+    "customer_backfillNumbers": "Assigned customer numbers to {count, plural, one {# customer} other {# customers}}",
+    "customer_create": "Created customer {name}",
+    "customer_delete": "Deleted customer {id}",
+    "customer_update": "Updated customer {id}",
+    "email_sendInspection": "Sent inspection email to {recipient}",
+    "email_sendInvoice": "Sent invoice email to {recipient}",
+    "email_sendQuote": "Sent quote email to {recipient}",
+    "finding_create": "Created finding \"{description}\" on vehicle {vehicleId}",
+    "finding_delete": "Deleted finding {id}",
+    "finding_resolve": "Resolved finding \"{description}\"",
+    "finding_update": "Updated finding \"{description}\"",
+    "inspectionTemplate_create": "Created inspection template \"{name}\"",
+    "inspectionTemplate_createFromPreset": "Created inspection template \"{name}\" from a preset",
+    "inspectionTemplate_delete": "Deleted inspection template \"{name}\"",
+    "inspectionTemplate_duplicate": "Duplicated inspection template into \"{name}\"",
+    "inspectionTemplate_import": "Imported {count, plural, one {# inspection template} other {# inspection templates}} from a package",
+    "inspectionTemplate_update": "Updated inspection template \"{name}\"",
+    "inspection_complete": "Completed inspection {id}",
+    "inspection_create": "Created inspection {id}",
+    "inspection_delete": "Deleted inspection {id}",
+    "inspection_reopen": "Reopened inspection {id}",
+    "inventory_bulkDelete": "Bulk deleted {count, plural, one {# inventory part} other {# inventory parts}}",
+    "inventory_create": "Created inventory part \"{name}\"",
+    "inventory_delete": "Deleted inventory part {id}",
+    "inventory_update": "Updated inventory part {id}",
+    "labor_preset_create": "Created labor preset \"{name}\"",
+    "labor_preset_delete": "Deleted labor preset {id}",
+    "labor_preset_update": "Updated labor preset {id}",
+    "note_create": "Created note on vehicle {vehicleId}",
+    "note_delete": "Deleted note {id}",
+    "organization_create": "Created organization \"{name}\"",
+    "organization_sampleDataRemove": "Removed onboarding sample data",
+    "payment_create": "Recorded payment {amount} for service {serviceRecordId}",
+    "payment_delete": "Deleted payment {id}",
+    "quoteRequest_update": "Updated quote request status to {status, select, pending {pending} approved {approved} rejected {rejected} completed {completed} other {{status}}}",
+    "quote_convert": "Converted quote {ref} to service record {serviceRecordId}",
+    "quote_create": "Created quote {ref}",
+    "quote_delete": "Deleted quote {ref}",
+    "quote_status": "Changed quote status to {status, select, draft {draft} sent {sent} accepted {accepted} rejected {rejected} expired {expired} converted {converted} other {{status}}}",
+    "quote_update": "Updated quote {ref}",
+    "recurringInvoice_create": "Created recurring invoice \"{title}\"",
+    "recurringInvoice_delete": "Deleted recurring invoice {id}",
+    "recurringInvoice_update": "Updated recurring invoice \"{title}\"",
+    "reminder_create": "Created reminder \"{title}\"",
+    "reminder_delete": "Deleted reminder {id}",
+    "role_create": "Created role \"{name}\"",
+    "role_delete": "Deleted role \"{name}\"",
+    "role_update": "Updated role \"{name}\"",
+    "scheduled_message_cancel": "Cancelled a scheduled message",
+    "scheduled_message_create": "Scheduled a {channel, select, email {email} sms {text message} telegram {Telegram message} in_app {in-app note} other {{channel}}} for {sendAt}",
+    "scheduled_message_delete": "Deleted a scheduled message",
+    "scheduled_message_send_now": "Sent a scheduled {channel, select, email {email} sms {text message} telegram {Telegram message} in_app {in-app note} other {{channel}}} ahead of time",
+    "scheduled_message_update": "Updated a scheduled {channel, select, email {email} sms {text message} telegram {Telegram message} in_app {in-app note} other {{channel}}}",
+    "service_create": "Created service record {ref}",
+    "service_createCounterSale": "Created counter sale {ref}",
+    "service_createDraft": "Created draft service record {ref}",
+    "service_createFromInspection": "Created work order {ref} from an inspection",
+    "service_delete": "Deleted service record {ref}",
+    "service_status": "Changed service record status to {status, select, pending {pending} scheduled {scheduled} in_progress {in progress} waiting_parts {waiting for parts} ready {ready} completed {completed} cancelled {cancelled} other {{status}}}",
+    "service_update": "Updated service record {ref}",
+    "settings_applyTaxRate": "Applied {rate}% tax to {records, plural, one {# service record} other {# service records}} and {quotes, plural, one {# quote} other {# quotes}}",
+    "settings_convertToExclusive": "Converted {records, plural, one {# service record} other {# service records}} and {quotes, plural, one {# quote} other {# quotes}} to tax-exclusive prices",
+    "settings_convertToInclusive": "Converted {records, plural, one {# service record} other {# service records}} and {quotes, plural, one {# quote} other {# quotes}} to tax-inclusive prices",
+    "settings_deleteContent": "Deleted all data: {items}",
+    "settings_updateInvoiceLayout": "Updated the invoice layout",
+    "settings_updateQuoteLayout": "Updated the quote layout",
+    "sms_send": "Sent a text message to {name} ({phone})",
+    "statusReport_create": "Created a status report for service {serviceRecordId}",
+    "statusReport_delete": "Deleted a status report",
+    "statusReport_send": "Sent a status report via {channels}",
+    "subscription_cancel": "Cancelled the subscription, effective at the end of the period",
+    "subscription_resume": "Resumed the subscription",
+    "team_cancelInvitation": "Cancelled the invitation for {email}",
+    "team_removeMember": "Removed team member {id}",
+    "team_sendInvitation": "Invited {email} as {role, select, owner {owner} admin {admin} member {member} other {{role}}}",
+    "team_updateRole": "Changed a member's role to {role, select, owner {owner} admin {admin} member {member} other {{role}}}",
+    "technician_create": "Created technician \"{name}\"",
+    "technician_delete": "Deleted technician {id}",
+    "technician_update": "Updated technician {id}",
+    "telegram_send": "Sent a Telegram message to {name}",
+    "tire_location_bulk_create": "Created {count, plural, one {# tire storage location} other {# tire storage locations}}",
+    "tire_location_create": "Created tire storage location {code}",
+    "tire_location_delete": "Deleted tire storage location {code}",
+    "tire_location_update": "Updated tire storage location {code}",
+    "tire_measurement_create": "Recorded {count, plural, one {# reading} other {# readings}} on tire set {ref}",
+    "tire_set_add_to_work_order": "Added tire set {ref} to work order {jobRef}",
+    "tire_set_attach": "Added {count, plural, one {# file} other {# files}} to tire set {ref}",
+    "tire_set_check_in": "Checked in tire set {ref} to {code}",
+    "tire_set_check_out": "Checked out tire set {ref}",
+    "tire_set_create_quote": "Created quote {ref} from a tire set",
+    "tire_set_create_work_order": "Created work order {ref} from a tire set",
+    "tire_set_delete": "Deleted tire set {ref}",
+    "tire_set_detach": "Removed {fileName} from a tire set",
+    "tire_set_dispose": "Wrote off tire set {ref}",
+    "tire_set_message_customer": "Messaged the customer about tire set {ref} by {channel, select, email {email} sms {text message} telegram {Telegram} in_app {in-app note} other {{channel}}}",
+    "tire_set_relocate": "Moved tire set {ref} from {from} to {to}",
+    "tire_set_relocateUnassigned": "Moved tire set {ref} onto {to}",
+    "tire_set_return": "Stored tire set {ref} again, on {code}",
+    "tire_set_unlink_work_order": "Unlinked tire set {ref} from work order {jobRef}",
+    "tire_set_update": "Updated tire set {ref}",
+    "tire_treatment_complete_all": "Completed {count, plural, one {# prep job} other {# prep jobs}} on tire set {ref}",
+    "tire_treatment_mark": "Marked {type, select, wash_tires {the tire wash} wash_rims {the rim wash} balance {balancing} tpms_service {the TPMS service} new_valves {new valves} repair {the repair} other {{type}}} as {status, select, done {done} pending {still to do} skipped {skipped} other {{status}}} on tire set {ref}",
+    "tire_treatment_update": "Updated prep work on tire set {ref}",
+    "tire_warehouse_archive": "Archived tire warehouse {name}",
+    "tire_warehouse_create": "Created tire warehouse {name}",
+    "tire_warehouse_delete": "Deleted tire warehouse {name}",
+    "tire_warehouse_update": "Updated tire warehouse {name}",
+    "vehicle_archive": "Archived vehicle {id}",
+    "vehicle_create": "Created vehicle {year} {make} {model}",
+    "vehicle_delete": "Deleted vehicle {name}",
+    "vehicle_unarchive": "Unarchived vehicle {id}",
+    "vehicle_update": "Updated vehicle {name}, changed: {fields}",
+    "vehicle_updateNoChanges": "Updated vehicle {name}, nothing changed",
+    "webhook_create": "Created webhook {name}"
   }
   }
 }
 }

+ 119 - 0
messages/es/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Juego de neumáticos dado de baja",
     "tire_set_dispose": "Juego de neumáticos dado de baja",
     "tire_set_attach": "Archivos añadidos a un juego de neumáticos",
     "tire_set_attach": "Archivos añadidos a un juego de neumáticos",
     "tire_set_detach": "Archivo quitado de un juego de neumáticos"
     "tire_set_detach": "Archivo quitado de un juego de neumáticos"
+  },
+  "summary": {
+    "customField_create": "Campo personalizado «{name}» creado",
+    "customField_delete": "Campo personalizado «{name}» eliminado",
+    "customField_update": "Campo personalizado «{name}» actualizado",
+    "customer_backfillNumbers": "Números de cliente asignados a {count, plural, one {# cliente} other {# clientes}}",
+    "customer_create": "Cliente {name} creado",
+    "customer_delete": "Cliente {id} eliminado",
+    "customer_update": "Cliente {id} actualizado",
+    "email_sendInspection": "Inspección enviada por correo a {recipient}",
+    "email_sendInvoice": "Factura enviada por correo a {recipient}",
+    "email_sendQuote": "Presupuesto enviado por correo a {recipient}",
+    "finding_create": "Hallazgo «{description}» creado en el vehículo {vehicleId}",
+    "finding_delete": "Hallazgo {id} eliminado",
+    "finding_resolve": "Hallazgo «{description}» resuelto",
+    "finding_update": "Hallazgo «{description}» actualizado",
+    "inspectionTemplate_create": "Plantilla de inspección «{name}» creada",
+    "inspectionTemplate_createFromPreset": "Plantilla de inspección «{name}» creada a partir de un preajuste",
+    "inspectionTemplate_delete": "Plantilla de inspección «{name}» eliminada",
+    "inspectionTemplate_duplicate": "Plantilla de inspección duplicada como «{name}»",
+    "inspectionTemplate_import": "{count, plural, one {# plantilla de inspección importada} other {# plantillas de inspección importadas}} desde un paquete",
+    "inspectionTemplate_update": "Plantilla de inspección «{name}» actualizada",
+    "inspection_complete": "Inspección {id} completada",
+    "inspection_create": "Inspección {id} creada",
+    "inspection_delete": "Inspección {id} eliminada",
+    "inspection_reopen": "Inspección {id} reabierta",
+    "inventory_bulkDelete": "{count, plural, one {# pieza de inventario eliminada} other {# piezas de inventario eliminadas}} en bloque",
+    "inventory_create": "Pieza de inventario «{name}» creada",
+    "inventory_delete": "Pieza de inventario {id} eliminada",
+    "inventory_update": "Pieza de inventario {id} actualizada",
+    "labor_preset_create": "Preajuste de mano de obra «{name}» creado",
+    "labor_preset_delete": "Preajuste de mano de obra {id} eliminado",
+    "labor_preset_update": "Preajuste de mano de obra {id} actualizado",
+    "note_create": "Nota creada en el vehículo {vehicleId}",
+    "note_delete": "Nota {id} eliminada",
+    "organization_create": "Organización «{name}» creada",
+    "organization_sampleDataRemove": "Datos de ejemplo de la puesta en marcha eliminados",
+    "payment_create": "Pago {amount} registrado para el servicio {serviceRecordId}",
+    "payment_delete": "Pago {id} eliminado",
+    "quoteRequest_update": "Estado de la solicitud de presupuesto cambiado a {status, select, pending {pendiente} approved {aprobada} rejected {rechazada} completed {completada} other {{status}}}",
+    "quote_convert": "Presupuesto {ref} convertido en servicio {serviceRecordId}",
+    "quote_create": "Presupuesto {ref} creado",
+    "quote_delete": "Presupuesto {ref} eliminado",
+    "quote_status": "Estado del presupuesto cambiado a {status, select, draft {borrador} sent {enviado} accepted {aceptado} rejected {rechazado} expired {caducado} converted {convertido} other {{status}}}",
+    "quote_update": "Presupuesto {ref} actualizado",
+    "recurringInvoice_create": "Factura recurrente «{title}» creada",
+    "recurringInvoice_delete": "Factura recurrente {id} eliminada",
+    "recurringInvoice_update": "Factura recurrente «{title}» actualizada",
+    "reminder_create": "Recordatorio «{title}» creado",
+    "reminder_delete": "Recordatorio {id} eliminado",
+    "role_create": "Rol «{name}» creado",
+    "role_delete": "Rol «{name}» eliminado",
+    "role_update": "Rol «{name}» actualizado",
+    "scheduled_message_cancel": "Mensaje programado cancelado",
+    "scheduled_message_create": "{channel, select, email {Correo} sms {SMS} telegram {Mensaje de Telegram} in_app {Aviso en la app} other {{channel}}} programado para {sendAt}",
+    "scheduled_message_delete": "Mensaje programado eliminado",
+    "scheduled_message_send_now": "{channel, select, email {Correo} sms {SMS} telegram {Mensaje de Telegram} in_app {Aviso en la app} other {{channel}}} programado enviado antes de tiempo",
+    "scheduled_message_update": "{channel, select, email {Correo} sms {SMS} telegram {Mensaje de Telegram} in_app {Aviso en la app} other {{channel}}} programado actualizado",
+    "service_create": "Servicio {ref} creado",
+    "service_createCounterSale": "Venta en mostrador {ref} creada",
+    "service_createDraft": "Borrador de servicio {ref} creado",
+    "service_createFromInspection": "Orden de trabajo {ref} creada a partir de una inspección",
+    "service_delete": "Servicio {ref} eliminado",
+    "service_status": "Estado del servicio cambiado a {status, select, pending {pendiente} scheduled {programado} in_progress {en curso} waiting_parts {esperando piezas} ready {listo} completed {completado} cancelled {cancelado} other {{status}}}",
+    "service_update": "Servicio {ref} actualizado",
+    "settings_applyTaxRate": "{rate}% de impuesto aplicado a {records, plural, one {# servicio} other {# servicios}} y {quotes, plural, one {# presupuesto} other {# presupuestos}}",
+    "settings_convertToExclusive": "{records, plural, one {# servicio convertido} other {# servicios convertidos}} y {quotes, plural, one {# presupuesto convertido} other {# presupuestos convertidos}} a precios sin impuestos",
+    "settings_convertToInclusive": "{records, plural, one {# servicio convertido} other {# servicios convertidos}} y {quotes, plural, one {# presupuesto convertido} other {# presupuestos convertidos}} a precios con impuestos",
+    "settings_deleteContent": "Todos los datos eliminados: {items}",
+    "settings_updateInvoiceLayout": "Diseño de la factura actualizado",
+    "settings_updateQuoteLayout": "Diseño del presupuesto actualizado",
+    "sms_send": "SMS enviado a {name} ({phone})",
+    "statusReport_create": "Informe de estado creado para el servicio {serviceRecordId}",
+    "statusReport_delete": "Informe de estado eliminado",
+    "statusReport_send": "Informe de estado enviado por {channels}",
+    "subscription_cancel": "Suscripción cancelada, efectiva al final del periodo",
+    "subscription_resume": "Suscripción reanudada",
+    "team_cancelInvitation": "Invitación para {email} cancelada",
+    "team_removeMember": "Miembro del equipo {id} eliminado",
+    "team_sendInvitation": "{email} invitado como {role, select, owner {propietario} admin {administrador} member {miembro} other {{role}}}",
+    "team_updateRole": "Rol de un miembro cambiado a {role, select, owner {propietario} admin {administrador} member {miembro} other {{role}}}",
+    "technician_create": "Técnico «{name}» creado",
+    "technician_delete": "Técnico {id} eliminado",
+    "technician_update": "Técnico {id} actualizado",
+    "telegram_send": "Mensaje de Telegram enviado a {name}",
+    "tire_location_bulk_create": "{count, plural, one {# ubicación de neumáticos creada} other {# ubicaciones de neumáticos creadas}}",
+    "tire_location_create": "Ubicación de neumáticos {code} creada",
+    "tire_location_delete": "Ubicación de neumáticos {code} eliminada",
+    "tire_location_update": "Ubicación de neumáticos {code} actualizada",
+    "tire_measurement_create": "{count, plural, one {# medición registrada} other {# mediciones registradas}} en el juego de neumáticos {ref}",
+    "tire_set_add_to_work_order": "Juego de neumáticos {ref} añadido a la orden de trabajo {jobRef}",
+    "tire_set_attach": "{count, plural, one {# archivo añadido} other {# archivos añadidos}} al juego de neumáticos {ref}",
+    "tire_set_check_in": "Juego de neumáticos {ref} guardado en {code}",
+    "tire_set_check_out": "Juego de neumáticos {ref} entregado",
+    "tire_set_create_quote": "Presupuesto {ref} creado a partir de un juego de neumáticos",
+    "tire_set_create_work_order": "Orden de trabajo {ref} creada a partir de un juego de neumáticos",
+    "tire_set_delete": "Juego de neumáticos {ref} eliminado",
+    "tire_set_detach": "{fileName} quitado de un juego de neumáticos",
+    "tire_set_dispose": "Juego de neumáticos {ref} dado de baja",
+    "tire_set_message_customer": "Cliente avisado sobre el juego de neumáticos {ref} por {channel, select, email {correo} sms {SMS} telegram {Telegram} in_app {aviso en la app} other {{channel}}}",
+    "tire_set_relocate": "Juego de neumáticos {ref} movido de {from} a {to}",
+    "tire_set_relocateUnassigned": "Juego de neumáticos {ref} colocado en {to}",
+    "tire_set_return": "Juego de neumáticos {ref} guardado de nuevo, en {code}",
+    "tire_set_unlink_work_order": "Juego de neumáticos {ref} desvinculado de la orden de trabajo {jobRef}",
+    "tire_set_update": "Juego de neumáticos {ref} actualizado",
+    "tire_treatment_complete_all": "{count, plural, one {# tarea de preparación completada} other {# tareas de preparación completadas}} en el juego de neumáticos {ref}",
+    "tire_treatment_mark": "{type, select, wash_tires {Lavado de neumáticos} wash_rims {Lavado de llantas} balance {Equilibrado} tpms_service {Servicio TPMS} new_valves {Válvulas nuevas} repair {Reparación} other {{type}}} marcado como {status, select, done {hecho} pending {pendiente} skipped {omitido} other {{status}}} en el juego de neumáticos {ref}",
+    "tire_treatment_update": "Trabajo de preparación del juego de neumáticos {ref} actualizado",
+    "tire_warehouse_archive": "Almacén de neumáticos {name} archivado",
+    "tire_warehouse_create": "Almacén de neumáticos {name} creado",
+    "tire_warehouse_delete": "Almacén de neumáticos {name} eliminado",
+    "tire_warehouse_update": "Almacén de neumáticos {name} actualizado",
+    "vehicle_archive": "Vehículo {id} archivado",
+    "vehicle_create": "Vehículo {year} {make} {model} creado",
+    "vehicle_delete": "Vehículo {name} eliminado",
+    "vehicle_unarchive": "Vehículo {id} recuperado del archivo",
+    "vehicle_update": "Vehículo {name} actualizado, cambios: {fields}",
+    "vehicle_updateNoChanges": "Vehículo {name} actualizado, sin cambios",
+    "webhook_create": "Webhook {name} creado"
   }
   }
 }
 }

+ 119 - 0
messages/fr/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Jeu de pneus mis au rebut",
     "tire_set_dispose": "Jeu de pneus mis au rebut",
     "tire_set_attach": "Fichiers ajoutés à un jeu de pneus",
     "tire_set_attach": "Fichiers ajoutés à un jeu de pneus",
     "tire_set_detach": "Fichier retiré d’un jeu de pneus"
     "tire_set_detach": "Fichier retiré d’un jeu de pneus"
+  },
+  "summary": {
+    "customField_create": "Champ personnalisé « {name} » créé",
+    "customField_delete": "Champ personnalisé « {name} » supprimé",
+    "customField_update": "Champ personnalisé « {name} » mis à jour",
+    "customer_backfillNumbers": "Numéros de client attribués à {count, plural, one {# client} other {# clients}}",
+    "customer_create": "Client {name} créé",
+    "customer_delete": "Client {id} supprimé",
+    "customer_update": "Client {id} mis à jour",
+    "email_sendInspection": "Contrôle envoyé par e-mail à {recipient}",
+    "email_sendInvoice": "Facture envoyée par e-mail à {recipient}",
+    "email_sendQuote": "Devis envoyé par e-mail à {recipient}",
+    "finding_create": "Constat « {description} » créé sur le véhicule {vehicleId}",
+    "finding_delete": "Constat {id} supprimé",
+    "finding_resolve": "Constat « {description} » résolu",
+    "finding_update": "Constat « {description} » mis à jour",
+    "inspectionTemplate_create": "Modèle de contrôle « {name} » créé",
+    "inspectionTemplate_createFromPreset": "Modèle de contrôle « {name} » créé à partir d'un préréglage",
+    "inspectionTemplate_delete": "Modèle de contrôle « {name} » supprimé",
+    "inspectionTemplate_duplicate": "Modèle de contrôle dupliqué vers « {name} »",
+    "inspectionTemplate_import": "{count, plural, one {# modèle de contrôle importé} other {# modèles de contrôle importés}} depuis un paquet",
+    "inspectionTemplate_update": "Modèle de contrôle « {name} » mis à jour",
+    "inspection_complete": "Contrôle {id} terminé",
+    "inspection_create": "Contrôle {id} créé",
+    "inspection_delete": "Contrôle {id} supprimé",
+    "inspection_reopen": "Contrôle {id} rouvert",
+    "inventory_bulkDelete": "{count, plural, one {# pièce en stock supprimée} other {# pièces en stock supprimées}} en lot",
+    "inventory_create": "Pièce en stock « {name} » créée",
+    "inventory_delete": "Pièce en stock {id} supprimée",
+    "inventory_update": "Pièce en stock {id} mise à jour",
+    "labor_preset_create": "Préréglage de main-d'œuvre « {name} » créé",
+    "labor_preset_delete": "Préréglage de main-d'œuvre {id} supprimé",
+    "labor_preset_update": "Préréglage de main-d'œuvre {id} mis à jour",
+    "note_create": "Note créée sur le véhicule {vehicleId}",
+    "note_delete": "Note {id} supprimée",
+    "organization_create": "Organisation « {name} » créée",
+    "organization_sampleDataRemove": "Données d'exemple de la mise en route supprimées",
+    "payment_create": "Paiement {amount} enregistré pour l'intervention {serviceRecordId}",
+    "payment_delete": "Paiement {id} supprimé",
+    "quoteRequest_update": "Statut de la demande de devis passé à {status, select, pending {en attente} approved {approuvée} rejected {refusée} completed {terminée} other {{status}}}",
+    "quote_convert": "Devis {ref} converti en intervention {serviceRecordId}",
+    "quote_create": "Devis {ref} créé",
+    "quote_delete": "Devis {ref} supprimé",
+    "quote_status": "Statut du devis passé à {status, select, draft {brouillon} sent {envoyé} accepted {accepté} rejected {refusé} expired {expiré} converted {converti} other {{status}}}",
+    "quote_update": "Devis {ref} mis à jour",
+    "recurringInvoice_create": "Facture récurrente « {title} » créée",
+    "recurringInvoice_delete": "Facture récurrente {id} supprimée",
+    "recurringInvoice_update": "Facture récurrente « {title} » mise à jour",
+    "reminder_create": "Rappel « {title} » créé",
+    "reminder_delete": "Rappel {id} supprimé",
+    "role_create": "Rôle « {name} » créé",
+    "role_delete": "Rôle « {name} » supprimé",
+    "role_update": "Rôle « {name} » mis à jour",
+    "scheduled_message_cancel": "Message programmé annulé",
+    "scheduled_message_create": "{channel, select, email {E-mail} sms {SMS} telegram {Message Telegram} in_app {Note dans l'application} other {{channel}}} programmé pour {sendAt}",
+    "scheduled_message_delete": "Message programmé supprimé",
+    "scheduled_message_send_now": "{channel, select, email {E-mail} sms {SMS} telegram {Message Telegram} in_app {Note dans l'application} other {{channel}}} programmé envoyé en avance",
+    "scheduled_message_update": "{channel, select, email {E-mail} sms {SMS} telegram {Message Telegram} in_app {Note dans l'application} other {{channel}}} programmé mis à jour",
+    "service_create": "Intervention {ref} créée",
+    "service_createCounterSale": "Vente au comptoir {ref} créée",
+    "service_createDraft": "Brouillon d'intervention {ref} créé",
+    "service_createFromInspection": "Ordre de travail {ref} créé à partir d'un contrôle",
+    "service_delete": "Intervention {ref} supprimée",
+    "service_status": "Statut de l'intervention passé à {status, select, pending {en attente} scheduled {planifiée} in_progress {en cours} waiting_parts {en attente de pièces} ready {prête} completed {terminée} cancelled {annulée} other {{status}}}",
+    "service_update": "Intervention {ref} mise à jour",
+    "settings_applyTaxRate": "TVA de {rate} % appliquée à {records, plural, one {# intervention} other {# interventions}} et {quotes, plural, one {# devis} other {# devis}}",
+    "settings_convertToExclusive": "{records, plural, one {# intervention convertie} other {# interventions converties}} et {quotes, plural, one {# devis converti} other {# devis convertis}} en prix hors taxes",
+    "settings_convertToInclusive": "{records, plural, one {# intervention convertie} other {# interventions converties}} et {quotes, plural, one {# devis converti} other {# devis convertis}} en prix toutes taxes comprises",
+    "settings_deleteContent": "Toutes les données supprimées : {items}",
+    "settings_updateInvoiceLayout": "Mise en page de la facture mise à jour",
+    "settings_updateQuoteLayout": "Mise en page du devis mise à jour",
+    "sms_send": "SMS envoyé à {name} ({phone})",
+    "statusReport_create": "Rapport d'état créé pour l'intervention {serviceRecordId}",
+    "statusReport_delete": "Rapport d'état supprimé",
+    "statusReport_send": "Rapport d'état envoyé via {channels}",
+    "subscription_cancel": "Abonnement résilié à la fin de la période",
+    "subscription_resume": "Abonnement repris",
+    "team_cancelInvitation": "Invitation de {email} annulée",
+    "team_removeMember": "Membre de l'équipe {id} retiré",
+    "team_sendInvitation": "{email} invité en tant que {role, select, owner {propriétaire} admin {administrateur} member {membre} other {{role}}}",
+    "team_updateRole": "Rôle d'un membre changé en {role, select, owner {propriétaire} admin {administrateur} member {membre} other {{role}}}",
+    "technician_create": "Technicien « {name} » créé",
+    "technician_delete": "Technicien {id} supprimé",
+    "technician_update": "Technicien {id} mis à jour",
+    "telegram_send": "Message Telegram envoyé à {name}",
+    "tire_location_bulk_create": "{count, plural, one {# emplacement de stockage pneus créé} other {# emplacements de stockage pneus créés}}",
+    "tire_location_create": "Emplacement de stockage pneus {code} créé",
+    "tire_location_delete": "Emplacement de stockage pneus {code} supprimé",
+    "tire_location_update": "Emplacement de stockage pneus {code} mis à jour",
+    "tire_measurement_create": "{count, plural, one {# relevé enregistré} other {# relevés enregistrés}} sur le jeu de pneus {ref}",
+    "tire_set_add_to_work_order": "Jeu de pneus {ref} ajouté à l'ordre de travail {jobRef}",
+    "tire_set_attach": "{count, plural, one {# fichier ajouté} other {# fichiers ajoutés}} au jeu de pneus {ref}",
+    "tire_set_check_in": "Jeu de pneus {ref} rentré en {code}",
+    "tire_set_check_out": "Jeu de pneus {ref} restitué",
+    "tire_set_create_quote": "Devis {ref} créé à partir d'un jeu de pneus",
+    "tire_set_create_work_order": "Ordre de travail {ref} créé à partir d'un jeu de pneus",
+    "tire_set_delete": "Jeu de pneus {ref} supprimé",
+    "tire_set_detach": "{fileName} retiré d'un jeu de pneus",
+    "tire_set_dispose": "Jeu de pneus {ref} mis au rebut",
+    "tire_set_message_customer": "Client informé au sujet du jeu de pneus {ref} par {channel, select, email {e-mail} sms {SMS} telegram {Telegram} in_app {note dans l'application} other {{channel}}}",
+    "tire_set_relocate": "Jeu de pneus {ref} déplacé de {from} vers {to}",
+    "tire_set_relocateUnassigned": "Jeu de pneus {ref} placé en {to}",
+    "tire_set_return": "Jeu de pneus {ref} de nouveau stocké, en {code}",
+    "tire_set_unlink_work_order": "Jeu de pneus {ref} dissocié de l'ordre de travail {jobRef}",
+    "tire_set_update": "Jeu de pneus {ref} mis à jour",
+    "tire_treatment_complete_all": "{count, plural, one {# tâche de préparation terminée} other {# tâches de préparation terminées}} sur le jeu de pneus {ref}",
+    "tire_treatment_mark": "{type, select, wash_tires {Lavage des pneus} wash_rims {Lavage des jantes} balance {Équilibrage} tpms_service {Service TPMS} new_valves {Valves neuves} repair {Réparation} other {{type}}} marqué comme {status, select, done {fait} pending {à faire} skipped {ignoré} other {{status}}} sur le jeu de pneus {ref}",
+    "tire_treatment_update": "Préparation du jeu de pneus {ref} mise à jour",
+    "tire_warehouse_archive": "Dépôt de pneus {name} archivé",
+    "tire_warehouse_create": "Dépôt de pneus {name} créé",
+    "tire_warehouse_delete": "Dépôt de pneus {name} supprimé",
+    "tire_warehouse_update": "Dépôt de pneus {name} mis à jour",
+    "vehicle_archive": "Véhicule {id} archivé",
+    "vehicle_create": "Véhicule {year} {make} {model} créé",
+    "vehicle_delete": "Véhicule {name} supprimé",
+    "vehicle_unarchive": "Véhicule {id} sorti des archives",
+    "vehicle_update": "Véhicule {name} mis à jour, modifié : {fields}",
+    "vehicle_updateNoChanges": "Véhicule {name} mis à jour, aucun changement",
+    "webhook_create": "Webhook {name} créé"
   }
   }
 }
 }

+ 119 - 0
messages/it/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Treno di pneumatici dismesso",
     "tire_set_dispose": "Treno di pneumatici dismesso",
     "tire_set_attach": "File aggiunti a un treno di pneumatici",
     "tire_set_attach": "File aggiunti a un treno di pneumatici",
     "tire_set_detach": "File rimosso da un treno di pneumatici"
     "tire_set_detach": "File rimosso da un treno di pneumatici"
+  },
+  "summary": {
+    "customField_create": "Campo personalizzato «{name}» creato",
+    "customField_delete": "Campo personalizzato «{name}» eliminato",
+    "customField_update": "Campo personalizzato «{name}» aggiornato",
+    "customer_backfillNumbers": "Numeri cliente assegnati a {count, plural, one {# cliente} other {# clienti}}",
+    "customer_create": "Cliente {name} creato",
+    "customer_delete": "Cliente {id} eliminato",
+    "customer_update": "Cliente {id} aggiornato",
+    "email_sendInspection": "Controllo inviato per e-mail a {recipient}",
+    "email_sendInvoice": "Fattura inviata per e-mail a {recipient}",
+    "email_sendQuote": "Preventivo inviato per e-mail a {recipient}",
+    "finding_create": "Rilievo «{description}» creato sul veicolo {vehicleId}",
+    "finding_delete": "Rilievo {id} eliminato",
+    "finding_resolve": "Rilievo «{description}» risolto",
+    "finding_update": "Rilievo «{description}» aggiornato",
+    "inspectionTemplate_create": "Modello di controllo «{name}» creato",
+    "inspectionTemplate_createFromPreset": "Modello di controllo «{name}» creato da una preimpostazione",
+    "inspectionTemplate_delete": "Modello di controllo «{name}» eliminato",
+    "inspectionTemplate_duplicate": "Modello di controllo duplicato in «{name}»",
+    "inspectionTemplate_import": "{count, plural, one {# modello di controllo importato} other {# modelli di controllo importati}} da un pacchetto",
+    "inspectionTemplate_update": "Modello di controllo «{name}» aggiornato",
+    "inspection_complete": "Controllo {id} completato",
+    "inspection_create": "Controllo {id} creato",
+    "inspection_delete": "Controllo {id} eliminato",
+    "inspection_reopen": "Controllo {id} riaperto",
+    "inventory_bulkDelete": "{count, plural, one {# ricambio a magazzino eliminato} other {# ricambi a magazzino eliminati}} in blocco",
+    "inventory_create": "Ricambio a magazzino «{name}» creato",
+    "inventory_delete": "Ricambio a magazzino {id} eliminato",
+    "inventory_update": "Ricambio a magazzino {id} aggiornato",
+    "labor_preset_create": "Preimpostazione di manodopera «{name}» creata",
+    "labor_preset_delete": "Preimpostazione di manodopera {id} eliminata",
+    "labor_preset_update": "Preimpostazione di manodopera {id} aggiornata",
+    "note_create": "Nota creata sul veicolo {vehicleId}",
+    "note_delete": "Nota {id} eliminata",
+    "organization_create": "Organizzazione «{name}» creata",
+    "organization_sampleDataRemove": "Dati di esempio dell'avvio rimossi",
+    "payment_create": "Pagamento {amount} registrato per l'intervento {serviceRecordId}",
+    "payment_delete": "Pagamento {id} eliminato",
+    "quoteRequest_update": "Stato della richiesta di preventivo cambiato in {status, select, pending {in attesa} approved {approvata} rejected {rifiutata} completed {completata} other {{status}}}",
+    "quote_convert": "Preventivo {ref} convertito nell'intervento {serviceRecordId}",
+    "quote_create": "Preventivo {ref} creato",
+    "quote_delete": "Preventivo {ref} eliminato",
+    "quote_status": "Stato del preventivo cambiato in {status, select, draft {bozza} sent {inviato} accepted {accettato} rejected {rifiutato} expired {scaduto} converted {convertito} other {{status}}}",
+    "quote_update": "Preventivo {ref} aggiornato",
+    "recurringInvoice_create": "Fattura ricorrente «{title}» creata",
+    "recurringInvoice_delete": "Fattura ricorrente {id} eliminata",
+    "recurringInvoice_update": "Fattura ricorrente «{title}» aggiornata",
+    "reminder_create": "Promemoria «{title}» creato",
+    "reminder_delete": "Promemoria {id} eliminato",
+    "role_create": "Ruolo «{name}» creato",
+    "role_delete": "Ruolo «{name}» eliminato",
+    "role_update": "Ruolo «{name}» aggiornato",
+    "scheduled_message_cancel": "Messaggio programmato annullato",
+    "scheduled_message_create": "{channel, select, email {E-mail} sms {SMS} telegram {Messaggio Telegram} in_app {Avviso nell'app} other {{channel}}} programmato per {sendAt}",
+    "scheduled_message_delete": "Messaggio programmato eliminato",
+    "scheduled_message_send_now": "{channel, select, email {E-mail} sms {SMS} telegram {Messaggio Telegram} in_app {Avviso nell'app} other {{channel}}} programmato inviato in anticipo",
+    "scheduled_message_update": "{channel, select, email {E-mail} sms {SMS} telegram {Messaggio Telegram} in_app {Avviso nell'app} other {{channel}}} programmato aggiornato",
+    "service_create": "Intervento {ref} creato",
+    "service_createCounterSale": "Vendita al banco {ref} creata",
+    "service_createDraft": "Bozza di intervento {ref} creata",
+    "service_createFromInspection": "Ordine di lavoro {ref} creato da un controllo",
+    "service_delete": "Intervento {ref} eliminato",
+    "service_status": "Stato dell'intervento cambiato in {status, select, pending {in attesa} scheduled {programmato} in_progress {in lavorazione} waiting_parts {in attesa di ricambi} ready {pronto} completed {completato} cancelled {annullato} other {{status}}}",
+    "service_update": "Intervento {ref} aggiornato",
+    "settings_applyTaxRate": "IVA al {rate}% applicata a {records, plural, one {# intervento} other {# interventi}} e {quotes, plural, one {# preventivo} other {# preventivi}}",
+    "settings_convertToExclusive": "{records, plural, one {# intervento convertito} other {# interventi convertiti}} e {quotes, plural, one {# preventivo convertito} other {# preventivi convertiti}} a prezzi IVA esclusa",
+    "settings_convertToInclusive": "{records, plural, one {# intervento convertito} other {# interventi convertiti}} e {quotes, plural, one {# preventivo convertito} other {# preventivi convertiti}} a prezzi IVA inclusa",
+    "settings_deleteContent": "Tutti i dati eliminati: {items}",
+    "settings_updateInvoiceLayout": "Layout della fattura aggiornato",
+    "settings_updateQuoteLayout": "Layout del preventivo aggiornato",
+    "sms_send": "SMS inviato a {name} ({phone})",
+    "statusReport_create": "Rapporto di stato creato per l'intervento {serviceRecordId}",
+    "statusReport_delete": "Rapporto di stato eliminato",
+    "statusReport_send": "Rapporto di stato inviato via {channels}",
+    "subscription_cancel": "Abbonamento disdetto con effetto a fine periodo",
+    "subscription_resume": "Abbonamento ripreso",
+    "team_cancelInvitation": "Invito per {email} annullato",
+    "team_removeMember": "Membro del team {id} rimosso",
+    "team_sendInvitation": "{email} invitato come {role, select, owner {titolare} admin {amministratore} member {membro} other {{role}}}",
+    "team_updateRole": "Ruolo di un membro cambiato in {role, select, owner {titolare} admin {amministratore} member {membro} other {{role}}}",
+    "technician_create": "Tecnico «{name}» creato",
+    "technician_delete": "Tecnico {id} eliminato",
+    "technician_update": "Tecnico {id} aggiornato",
+    "telegram_send": "Messaggio Telegram inviato a {name}",
+    "tire_location_bulk_create": "{count, plural, one {# posto pneumatici creato} other {# posti pneumatici creati}}",
+    "tire_location_create": "Posto pneumatici {code} creato",
+    "tire_location_delete": "Posto pneumatici {code} eliminato",
+    "tire_location_update": "Posto pneumatici {code} aggiornato",
+    "tire_measurement_create": "{count, plural, one {# misurazione registrata} other {# misurazioni registrate}} sul treno di pneumatici {ref}",
+    "tire_set_add_to_work_order": "Treno di pneumatici {ref} aggiunto all'ordine di lavoro {jobRef}",
+    "tire_set_attach": "{count, plural, one {# file aggiunto} other {# file aggiunti}} al treno di pneumatici {ref}",
+    "tire_set_check_in": "Treno di pneumatici {ref} depositato in {code}",
+    "tire_set_check_out": "Treno di pneumatici {ref} riconsegnato",
+    "tire_set_create_quote": "Preventivo {ref} creato da un treno di pneumatici",
+    "tire_set_create_work_order": "Ordine di lavoro {ref} creato da un treno di pneumatici",
+    "tire_set_delete": "Treno di pneumatici {ref} eliminato",
+    "tire_set_detach": "{fileName} rimosso da un treno di pneumatici",
+    "tire_set_dispose": "Treno di pneumatici {ref} rottamato",
+    "tire_set_message_customer": "Cliente avvisato del treno di pneumatici {ref} via {channel, select, email {e-mail} sms {SMS} telegram {Telegram} in_app {avviso nell'app} other {{channel}}}",
+    "tire_set_relocate": "Treno di pneumatici {ref} spostato da {from} a {to}",
+    "tire_set_relocateUnassigned": "Treno di pneumatici {ref} sistemato in {to}",
+    "tire_set_return": "Treno di pneumatici {ref} depositato di nuovo, in {code}",
+    "tire_set_unlink_work_order": "Treno di pneumatici {ref} scollegato dall'ordine di lavoro {jobRef}",
+    "tire_set_update": "Treno di pneumatici {ref} aggiornato",
+    "tire_treatment_complete_all": "{count, plural, one {# lavoro di preparazione completato} other {# lavori di preparazione completati}} sul treno di pneumatici {ref}",
+    "tire_treatment_mark": "{type, select, wash_tires {Lavaggio pneumatici} wash_rims {Lavaggio cerchi} balance {Equilibratura} tpms_service {Servizio TPMS} new_valves {Valvole nuove} repair {Riparazione} other {{type}}} segnato come {status, select, done {fatto} pending {da fare} skipped {saltato} other {{status}}} sul treno di pneumatici {ref}",
+    "tire_treatment_update": "Preparazione del treno di pneumatici {ref} aggiornata",
+    "tire_warehouse_archive": "Deposito pneumatici {name} archiviato",
+    "tire_warehouse_create": "Deposito pneumatici {name} creato",
+    "tire_warehouse_delete": "Deposito pneumatici {name} eliminato",
+    "tire_warehouse_update": "Deposito pneumatici {name} aggiornato",
+    "vehicle_archive": "Veicolo {id} archiviato",
+    "vehicle_create": "Veicolo {year} {make} {model} creato",
+    "vehicle_delete": "Veicolo {name} eliminato",
+    "vehicle_unarchive": "Veicolo {id} tolto dall'archivio",
+    "vehicle_update": "Veicolo {name} aggiornato, modifiche: {fields}",
+    "vehicle_updateNoChanges": "Veicolo {name} aggiornato, nessuna modifica",
+    "webhook_create": "Webhook {name} creato"
   }
   }
 }
 }

+ 119 - 0
messages/lt/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Padangų komplektas nurašytas",
     "tire_set_dispose": "Padangų komplektas nurašytas",
     "tire_set_attach": "Prie padangų komplekto pridėta failų",
     "tire_set_attach": "Prie padangų komplekto pridėta failų",
     "tire_set_detach": "Failas pašalintas iš padangų komplekto"
     "tire_set_detach": "Failas pašalintas iš padangų komplekto"
+  },
+  "summary": {
+    "customField_create": "Sukurtas pasirinktinis laukas „{name}“",
+    "customField_delete": "Ištrintas pasirinktinis laukas „{name}“",
+    "customField_update": "Atnaujintas pasirinktinis laukas „{name}“",
+    "customer_backfillNumbers": "Kliento numeriai priskirti {count, plural, one {# klientui} few {# klientams} many {# kliento} other {# klientų}}",
+    "customer_create": "Sukurtas klientas {name}",
+    "customer_delete": "Ištrintas klientas {id}",
+    "customer_update": "Atnaujintas klientas {id}",
+    "email_sendInspection": "Patikra išsiųsta el. paštu adresu {recipient}",
+    "email_sendInvoice": "Sąskaita išsiųsta el. paštu adresu {recipient}",
+    "email_sendQuote": "Pasiūlymas išsiųstas el. paštu adresu {recipient}",
+    "finding_create": "Sukurtas radinys „{description}“ transporto priemonei {vehicleId}",
+    "finding_delete": "Ištrintas radinys {id}",
+    "finding_resolve": "Išspręstas radinys „{description}“",
+    "finding_update": "Atnaujintas radinys „{description}“",
+    "inspectionTemplate_create": "Sukurtas patikros šablonas „{name}“",
+    "inspectionTemplate_createFromPreset": "Patikros šablonas „{name}“ sukurtas iš paruošto rinkinio",
+    "inspectionTemplate_delete": "Ištrintas patikros šablonas „{name}“",
+    "inspectionTemplate_duplicate": "Patikros šablonas nukopijuotas į „{name}“",
+    "inspectionTemplate_import": "Iš paketo importuota {count, plural, one {# patikros šablonas} few {# patikros šablonai} many {# patikros šablono} other {# patikros šablonų}}",
+    "inspectionTemplate_update": "Atnaujintas patikros šablonas „{name}“",
+    "inspection_complete": "Užbaigta patikra {id}",
+    "inspection_create": "Sukurta patikra {id}",
+    "inspection_delete": "Ištrinta patikra {id}",
+    "inspection_reopen": "Patikra {id} atidaryta iš naujo",
+    "inventory_bulkDelete": "Masiškai ištrinta {count, plural, one {# sandėlio dalis} few {# sandėlio dalys} many {# sandėlio dalies} other {# sandėlio dalių}}",
+    "inventory_create": "Sukurta sandėlio dalis „{name}“",
+    "inventory_delete": "Ištrinta sandėlio dalis {id}",
+    "inventory_update": "Atnaujinta sandėlio dalis {id}",
+    "labor_preset_create": "Sukurtas darbų šablonas „{name}“",
+    "labor_preset_delete": "Ištrintas darbų šablonas {id}",
+    "labor_preset_update": "Atnaujintas darbų šablonas {id}",
+    "note_create": "Sukurtas užrašas transporto priemonei {vehicleId}",
+    "note_delete": "Ištrintas užrašas {id}",
+    "organization_create": "Sukurta organizacija „{name}“",
+    "organization_sampleDataRemove": "Pašalinti pradinės sąrankos pavyzdiniai duomenys",
+    "payment_create": "Užregistruotas mokėjimas {amount} už darbą {serviceRecordId}",
+    "payment_delete": "Ištrintas mokėjimas {id}",
+    "quoteRequest_update": "Pasiūlymo užklausos būsena pakeista į „{status, select, pending {laukiama} approved {patvirtinta} rejected {atmesta} completed {užbaigta} other {{status}}}“",
+    "quote_convert": "Pasiūlymas {ref} paverstas darbu {serviceRecordId}",
+    "quote_create": "Sukurtas pasiūlymas {ref}",
+    "quote_delete": "Ištrintas pasiūlymas {ref}",
+    "quote_status": "Pasiūlymo būsena pakeista į „{status, select, draft {juodraštis} sent {išsiųstas} accepted {priimtas} rejected {atmestas} expired {nebegalioja} converted {paverstas} other {{status}}}“",
+    "quote_update": "Atnaujintas pasiūlymas {ref}",
+    "recurringInvoice_create": "Sukurta pasikartojanti sąskaita „{title}“",
+    "recurringInvoice_delete": "Ištrinta pasikartojanti sąskaita {id}",
+    "recurringInvoice_update": "Atnaujinta pasikartojanti sąskaita „{title}“",
+    "reminder_create": "Sukurtas priminimas „{title}“",
+    "reminder_delete": "Ištrintas priminimas {id}",
+    "role_create": "Sukurta rolė „{name}“",
+    "role_delete": "Ištrinta rolė „{name}“",
+    "role_update": "Atnaujinta rolė „{name}“",
+    "scheduled_message_cancel": "Suplanuota žinutė atšaukta",
+    "scheduled_message_create": "Suplanuota {sendAt}: {channel, select, email {el. laiškas} sms {SMS} telegram {Telegram žinutė} in_app {pranešimas programoje} other {{channel}}}",
+    "scheduled_message_delete": "Suplanuota žinutė ištrinta",
+    "scheduled_message_send_now": "Suplanuotas {channel, select, email {el. laiškas} sms {SMS} telegram {Telegram žinutė} in_app {pranešimas programoje} other {{channel}}} išsiųstas anksčiau laiko",
+    "scheduled_message_update": "Atnaujintas suplanuotas {channel, select, email {el. laiškas} sms {SMS} telegram {Telegram žinutė} in_app {pranešimas programoje} other {{channel}}}",
+    "service_create": "Sukurtas darbas {ref}",
+    "service_createCounterSale": "Sukurtas pardavimas prie prekystalio {ref}",
+    "service_createDraft": "Sukurtas darbo juodraštis {ref}",
+    "service_createFromInspection": "Iš patikros sukurtas darbo užsakymas {ref}",
+    "service_delete": "Ištrintas darbas {ref}",
+    "service_status": "Darbo būsena pakeista į „{status, select, pending {laukiama} scheduled {suplanuota} in_progress {vykdoma} waiting_parts {laukiama dalių} ready {paruošta} completed {užbaigta} cancelled {atšaukta} other {{status}}}“",
+    "service_update": "Atnaujintas darbas {ref}",
+    "settings_applyTaxRate": "{rate}% mokestis pritaikytas {records, plural, one {# darbui} few {# darbams} many {# darbo} other {# darbų}} ir {quotes, plural, one {# pasiūlymui} few {# pasiūlymams} many {# pasiūlymo} other {# pasiūlymų}}",
+    "settings_convertToExclusive": "{records, plural, one {# darbas} few {# darbai} many {# darbo} other {# darbų}} ir {quotes, plural, one {# pasiūlymas} few {# pasiūlymai} many {# pasiūlymo} other {# pasiūlymų}} perskaičiuoti į kainas be mokesčių",
+    "settings_convertToInclusive": "{records, plural, one {# darbas} few {# darbai} many {# darbo} other {# darbų}} ir {quotes, plural, one {# pasiūlymas} few {# pasiūlymai} many {# pasiūlymo} other {# pasiūlymų}} perskaičiuoti į kainas su mokesčiais",
+    "settings_deleteContent": "Ištrinti visi duomenys: {items}",
+    "settings_updateInvoiceLayout": "Atnaujintas sąskaitos išdėstymas",
+    "settings_updateQuoteLayout": "Atnaujintas pasiūlymo išdėstymas",
+    "sms_send": "SMS išsiųsta {name} ({phone})",
+    "statusReport_create": "Sukurta būsenos ataskaita darbui {serviceRecordId}",
+    "statusReport_delete": "Ištrinta būsenos ataskaita",
+    "statusReport_send": "Būsenos ataskaita išsiųsta per {channels}",
+    "subscription_cancel": "Prenumerata atšaukta, galios iki laikotarpio pabaigos",
+    "subscription_resume": "Prenumerata atnaujinta",
+    "team_cancelInvitation": "Atšauktas kvietimas {email}",
+    "team_removeMember": "Pašalintas komandos narys {id}",
+    "team_sendInvitation": "{email} pakviestas kaip {role, select, owner {savininkas} admin {administratorius} member {narys} other {{role}}}",
+    "team_updateRole": "Nario rolė pakeista į „{role, select, owner {savininkas} admin {administratorius} member {narys} other {{role}}}“",
+    "technician_create": "Sukurtas meistras „{name}“",
+    "technician_delete": "Ištrintas meistras {id}",
+    "technician_update": "Atnaujintas meistras {id}",
+    "telegram_send": "Telegram žinutė išsiųsta {name}",
+    "tire_location_bulk_create": "Sukurta {count, plural, one {# padangų saugojimo vieta} few {# padangų saugojimo vietos} many {# padangų saugojimo vietos} other {# padangų saugojimo vietų}}",
+    "tire_location_create": "Sukurta padangų saugojimo vieta {code}",
+    "tire_location_delete": "Ištrinta padangų saugojimo vieta {code}",
+    "tire_location_update": "Atnaujinta padangų saugojimo vieta {code}",
+    "tire_measurement_create": "Padangų komplektui {ref} užrašyta {count, plural, one {# matavimas} few {# matavimai} many {# matavimo} other {# matavimų}}",
+    "tire_set_add_to_work_order": "Padangų komplektas {ref} pridėtas prie darbo užsakymo {jobRef}",
+    "tire_set_attach": "Prie padangų komplekto {ref} pridėta {count, plural, one {# failas} few {# failai} many {# failo} other {# failų}}",
+    "tire_set_check_in": "Padangų komplektas {ref} priimtas į {code}",
+    "tire_set_check_out": "Padangų komplektas {ref} atiduotas",
+    "tire_set_create_quote": "Iš padangų komplekto sukurtas pasiūlymas {ref}",
+    "tire_set_create_work_order": "Iš padangų komplekto sukurtas darbo užsakymas {ref}",
+    "tire_set_delete": "Ištrintas padangų komplektas {ref}",
+    "tire_set_detach": "{fileName} pašalintas iš padangų komplekto",
+    "tire_set_dispose": "Padangų komplektas {ref} nurašytas",
+    "tire_set_message_customer": "Klientui pranešta apie padangų komplektą {ref} per {channel, select, email {el. paštą} sms {SMS} telegram {Telegram} in_app {pranešimą programoje} other {{channel}}}",
+    "tire_set_relocate": "Padangų komplektas {ref} perkeltas iš {from} į {to}",
+    "tire_set_relocateUnassigned": "Padangų komplektas {ref} padėtas į {to}",
+    "tire_set_return": "Padangų komplektas {ref} vėl padėtas saugoti, į {code}",
+    "tire_set_unlink_work_order": "Padangų komplektas {ref} atsietas nuo darbo užsakymo {jobRef}",
+    "tire_set_update": "Atnaujintas padangų komplektas {ref}",
+    "tire_treatment_complete_all": "Padangų komplektui {ref} užbaigta {count, plural, one {# paruošimo darbas} few {# paruošimo darbai} many {# paruošimo darbo} other {# paruošimo darbų}}",
+    "tire_treatment_mark": "Padangų komplekto {ref} darbas {type, select, wash_tires {padangų plovimas} wash_rims {ratlankių plovimas} balance {balansavimas} tpms_service {TPMS aptarnavimas} new_valves {nauji ventiliai} repair {remontas} other {{type}}} pažymėtas kaip „{status, select, done {atlikta} pending {dar reikia} skipped {praleista} other {{status}}}“",
+    "tire_treatment_update": "Atnaujinti padangų komplekto {ref} paruošimo darbai",
+    "tire_warehouse_archive": "Padangų sandėlis {name} perkeltas į archyvą",
+    "tire_warehouse_create": "Sukurtas padangų sandėlis {name}",
+    "tire_warehouse_delete": "Ištrintas padangų sandėlis {name}",
+    "tire_warehouse_update": "Atnaujintas padangų sandėlis {name}",
+    "vehicle_archive": "Transporto priemonė {id} perkelta į archyvą",
+    "vehicle_create": "Sukurta transporto priemonė {year} {make} {model}",
+    "vehicle_delete": "Ištrinta transporto priemonė {name}",
+    "vehicle_unarchive": "Transporto priemonė {id} grąžinta iš archyvo",
+    "vehicle_update": "Atnaujinta transporto priemonė {name}, pakeista: {fields}",
+    "vehicle_updateNoChanges": "Atnaujinta transporto priemonė {name}, niekas nepasikeitė",
+    "webhook_create": "Sukurtas webhook {name}"
   }
   }
 }
 }

+ 119 - 0
messages/nb/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Kasserte et dekksett",
     "tire_set_dispose": "Kasserte et dekksett",
     "tire_set_attach": "La til filer på et dekksett",
     "tire_set_attach": "La til filer på et dekksett",
     "tire_set_detach": "Fjernet en fil fra et dekksett"
     "tire_set_detach": "Fjernet en fil fra et dekksett"
+  },
+  "summary": {
+    "customField_create": "Opprettet egendefinert felt «{name}»",
+    "customField_delete": "Slettet egendefinert felt «{name}»",
+    "customField_update": "Oppdaterte egendefinert felt «{name}»",
+    "customer_backfillNumbers": "Tildelte kundenummer til {count, plural, one {# kunde} other {# kunder}}",
+    "customer_create": "Opprettet kunde {name}",
+    "customer_delete": "Slettet kunde {id}",
+    "customer_update": "Oppdaterte kunde {id}",
+    "email_sendInspection": "Sendte kontroll på e-post til {recipient}",
+    "email_sendInvoice": "Sendte faktura på e-post til {recipient}",
+    "email_sendQuote": "Sendte tilbud på e-post til {recipient}",
+    "finding_create": "Opprettet funn «{description}» på kjøretøy {vehicleId}",
+    "finding_delete": "Slettet funn {id}",
+    "finding_resolve": "Lukket funn «{description}»",
+    "finding_update": "Oppdaterte funn «{description}»",
+    "inspectionTemplate_create": "Opprettet kontrollmal «{name}»",
+    "inspectionTemplate_createFromPreset": "Opprettet kontrollmal «{name}» fra en ferdigmal",
+    "inspectionTemplate_delete": "Slettet kontrollmal «{name}»",
+    "inspectionTemplate_duplicate": "Kopierte kontrollmal til «{name}»",
+    "inspectionTemplate_import": "Importerte {count, plural, one {# kontrollmal} other {# kontrollmaler}} fra en pakke",
+    "inspectionTemplate_update": "Oppdaterte kontrollmal «{name}»",
+    "inspection_complete": "Fullførte kontroll {id}",
+    "inspection_create": "Opprettet kontroll {id}",
+    "inspection_delete": "Slettet kontroll {id}",
+    "inspection_reopen": "Gjenåpnet kontroll {id}",
+    "inventory_bulkDelete": "Masseslettet {count, plural, one {# lagerdel} other {# lagerdeler}}",
+    "inventory_create": "Opprettet lagerdel «{name}»",
+    "inventory_delete": "Slettet lagerdel {id}",
+    "inventory_update": "Oppdaterte lagerdel {id}",
+    "labor_preset_create": "Opprettet arbeidsmal «{name}»",
+    "labor_preset_delete": "Slettet arbeidsmal {id}",
+    "labor_preset_update": "Oppdaterte arbeidsmal {id}",
+    "note_create": "Opprettet notat på kjøretøy {vehicleId}",
+    "note_delete": "Slettet notat {id}",
+    "organization_create": "Opprettet organisasjon «{name}»",
+    "organization_sampleDataRemove": "Fjernet eksempeldata fra oppstarten",
+    "payment_create": "Registrerte betaling {amount} for jobb {serviceRecordId}",
+    "payment_delete": "Slettet betaling {id}",
+    "quoteRequest_update": "Endret status på tilbudsforespørsel til {status, select, pending {venter} approved {godkjent} rejected {avslått} completed {fullført} other {{status}}}",
+    "quote_convert": "Gjorde tilbud {ref} om til jobb {serviceRecordId}",
+    "quote_create": "Opprettet tilbud {ref}",
+    "quote_delete": "Slettet tilbud {ref}",
+    "quote_status": "Endret tilbudsstatus til {status, select, draft {utkast} sent {sendt} accepted {godtatt} rejected {avslått} expired {utløpt} converted {konvertert} other {{status}}}",
+    "quote_update": "Oppdaterte tilbud {ref}",
+    "recurringInvoice_create": "Opprettet gjentakende faktura «{title}»",
+    "recurringInvoice_delete": "Slettet gjentakende faktura {id}",
+    "recurringInvoice_update": "Oppdaterte gjentakende faktura «{title}»",
+    "reminder_create": "Opprettet påminnelse «{title}»",
+    "reminder_delete": "Slettet påminnelse {id}",
+    "role_create": "Opprettet rollen «{name}»",
+    "role_delete": "Slettet rollen «{name}»",
+    "role_update": "Oppdaterte rollen «{name}»",
+    "scheduled_message_cancel": "Avbrøt en planlagt melding",
+    "scheduled_message_create": "Planla {channel, select, email {en e-post} sms {en SMS} telegram {en Telegram-melding} in_app {et varsel i appen} other {{channel}}} til {sendAt}",
+    "scheduled_message_delete": "Slettet en planlagt melding",
+    "scheduled_message_send_now": "Sendte {channel, select, email {en planlagt e-post} sms {en planlagt SMS} telegram {en planlagt Telegram-melding} in_app {et planlagt varsel i appen} other {{channel}}} før tiden",
+    "scheduled_message_update": "Oppdaterte {channel, select, email {en planlagt e-post} sms {en planlagt SMS} telegram {en planlagt Telegram-melding} in_app {et planlagt varsel i appen} other {{channel}}}",
+    "service_create": "Opprettet jobb {ref}",
+    "service_createCounterSale": "Opprettet disksalg {ref}",
+    "service_createDraft": "Opprettet utkast til jobb {ref}",
+    "service_createFromInspection": "Opprettet arbeidsordre {ref} fra en kontroll",
+    "service_delete": "Slettet jobb {ref}",
+    "service_status": "Endret jobbstatus til {status, select, pending {venter} scheduled {planlagt} in_progress {under arbeid} waiting_parts {venter på deler} ready {klar} completed {fullført} cancelled {avbrutt} other {{status}}}",
+    "service_update": "Oppdaterte jobb {ref}",
+    "settings_applyTaxRate": "La {rate} % mva på {records, plural, one {# jobb} other {# jobber}} og {quotes, plural, one {# tilbud} other {# tilbud}}",
+    "settings_convertToExclusive": "Gjorde om {records, plural, one {# jobb} other {# jobber}} og {quotes, plural, one {# tilbud} other {# tilbud}} til priser uten mva",
+    "settings_convertToInclusive": "Gjorde om {records, plural, one {# jobb} other {# jobber}} og {quotes, plural, one {# tilbud} other {# tilbud}} til priser med mva",
+    "settings_deleteContent": "Slettet alle data: {items}",
+    "settings_updateInvoiceLayout": "Oppdaterte fakturaoppsettet",
+    "settings_updateQuoteLayout": "Oppdaterte tilbudsoppsettet",
+    "sms_send": "Sendte SMS til {name} ({phone})",
+    "statusReport_create": "Opprettet statusrapport for jobb {serviceRecordId}",
+    "statusReport_delete": "Slettet en statusrapport",
+    "statusReport_send": "Sendte statusrapport via {channels}",
+    "subscription_cancel": "Sa opp abonnementet, med virkning ved periodeslutt",
+    "subscription_resume": "Gjenopptok abonnementet",
+    "team_cancelInvitation": "Trakk tilbake invitasjonen til {email}",
+    "team_removeMember": "Fjernet teammedlem {id}",
+    "team_sendInvitation": "Inviterte {email} som {role, select, owner {eier} admin {administrator} member {medlem} other {{role}}}",
+    "team_updateRole": "Endret rollen til et medlem til {role, select, owner {eier} admin {administrator} member {medlem} other {{role}}}",
+    "technician_create": "Opprettet mekaniker «{name}»",
+    "technician_delete": "Slettet mekaniker {id}",
+    "technician_update": "Oppdaterte mekaniker {id}",
+    "telegram_send": "Sendte Telegram-melding til {name}",
+    "tire_location_bulk_create": "Opprettet {count, plural, one {# dekkplass} other {# dekkplasser}}",
+    "tire_location_create": "Opprettet dekkplass {code}",
+    "tire_location_delete": "Slettet dekkplass {code}",
+    "tire_location_update": "Oppdaterte dekkplass {code}",
+    "tire_measurement_create": "Registrerte {count, plural, one {# måling} other {# målinger}} på dekksett {ref}",
+    "tire_set_add_to_work_order": "La dekksett {ref} til arbeidsordre {jobRef}",
+    "tire_set_attach": "La {count, plural, one {# fil} other {# filer}} til dekksett {ref}",
+    "tire_set_check_in": "Tok inn dekksett {ref} på {code}",
+    "tire_set_check_out": "Leverte ut dekksett {ref}",
+    "tire_set_create_quote": "Opprettet tilbud {ref} fra et dekksett",
+    "tire_set_create_work_order": "Opprettet arbeidsordre {ref} fra et dekksett",
+    "tire_set_delete": "Slettet dekksett {ref}",
+    "tire_set_detach": "Fjernet {fileName} fra et dekksett",
+    "tire_set_dispose": "Kasserte dekksett {ref}",
+    "tire_set_message_customer": "Sendte melding til kunden om dekksett {ref} på {channel, select, email {e-post} sms {SMS} telegram {Telegram} in_app {varsel i appen} other {{channel}}}",
+    "tire_set_relocate": "Flyttet dekksett {ref} fra {from} til {to}",
+    "tire_set_relocateUnassigned": "Flyttet dekksett {ref} til {to}",
+    "tire_set_return": "Lagret dekksett {ref} igjen, på {code}",
+    "tire_set_unlink_work_order": "Koblet dekksett {ref} fra arbeidsordre {jobRef}",
+    "tire_set_update": "Oppdaterte dekksett {ref}",
+    "tire_treatment_complete_all": "Fullførte {count, plural, one {# klargjøringsjobb} other {# klargjøringsjobber}} på dekksett {ref}",
+    "tire_treatment_mark": "Merket {type, select, wash_tires {dekkvask} wash_rims {felgvask} balance {balansering} tpms_service {TPMS-service} new_valves {nye ventiler} repair {reparasjon} other {{type}}} som {status, select, done {utført} pending {gjenstår} skipped {hoppet over} other {{status}}} på dekksett {ref}",
+    "tire_treatment_update": "Oppdaterte klargjøringen på dekksett {ref}",
+    "tire_warehouse_archive": "Arkiverte dekklager {name}",
+    "tire_warehouse_create": "Opprettet dekklager {name}",
+    "tire_warehouse_delete": "Slettet dekklager {name}",
+    "tire_warehouse_update": "Oppdaterte dekklager {name}",
+    "vehicle_archive": "Arkiverte kjøretøy {id}",
+    "vehicle_create": "Opprettet kjøretøy {year} {make} {model}",
+    "vehicle_delete": "Slettet kjøretøy {name}",
+    "vehicle_unarchive": "Hentet kjøretøy {id} ut av arkivet",
+    "vehicle_update": "Oppdaterte kjøretøy {name}, endret: {fields}",
+    "vehicle_updateNoChanges": "Oppdaterte kjøretøy {name}, ingenting endret",
+    "webhook_create": "Opprettet webhook {name}"
   }
   }
 }
 }

+ 119 - 0
messages/nl/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Bandenset afgeschreven",
     "tire_set_dispose": "Bandenset afgeschreven",
     "tire_set_attach": "Bestanden aan een bandenset toegevoegd",
     "tire_set_attach": "Bestanden aan een bandenset toegevoegd",
     "tire_set_detach": "Bestand van een bandenset verwijderd"
     "tire_set_detach": "Bestand van een bandenset verwijderd"
+  },
+  "summary": {
+    "customField_create": "Aangepast veld \"{name}\" aangemaakt",
+    "customField_delete": "Aangepast veld \"{name}\" verwijderd",
+    "customField_update": "Aangepast veld \"{name}\" bijgewerkt",
+    "customer_backfillNumbers": "Klantnummers toegekend aan {count, plural, one {# klant} other {# klanten}}",
+    "customer_create": "Klant {name} aangemaakt",
+    "customer_delete": "Klant {id} verwijderd",
+    "customer_update": "Klant {id} bijgewerkt",
+    "email_sendInspection": "Keuring per e-mail verstuurd naar {recipient}",
+    "email_sendInvoice": "Factuur per e-mail verstuurd naar {recipient}",
+    "email_sendQuote": "Offerte per e-mail verstuurd naar {recipient}",
+    "finding_create": "Bevinding \"{description}\" aangemaakt op voertuig {vehicleId}",
+    "finding_delete": "Bevinding {id} verwijderd",
+    "finding_resolve": "Bevinding \"{description}\" afgehandeld",
+    "finding_update": "Bevinding \"{description}\" bijgewerkt",
+    "inspectionTemplate_create": "Keuringssjabloon \"{name}\" aangemaakt",
+    "inspectionTemplate_createFromPreset": "Keuringssjabloon \"{name}\" aangemaakt vanuit een voorinstelling",
+    "inspectionTemplate_delete": "Keuringssjabloon \"{name}\" verwijderd",
+    "inspectionTemplate_duplicate": "Keuringssjabloon gekopieerd naar \"{name}\"",
+    "inspectionTemplate_import": "{count, plural, one {# keuringssjabloon} other {# keuringssjablonen}} geïmporteerd uit een pakket",
+    "inspectionTemplate_update": "Keuringssjabloon \"{name}\" bijgewerkt",
+    "inspection_complete": "Keuring {id} afgerond",
+    "inspection_create": "Keuring {id} aangemaakt",
+    "inspection_delete": "Keuring {id} verwijderd",
+    "inspection_reopen": "Keuring {id} heropend",
+    "inventory_bulkDelete": "{count, plural, one {# voorraadonderdeel} other {# voorraadonderdelen}} in bulk verwijderd",
+    "inventory_create": "Voorraadonderdeel \"{name}\" aangemaakt",
+    "inventory_delete": "Voorraadonderdeel {id} verwijderd",
+    "inventory_update": "Voorraadonderdeel {id} bijgewerkt",
+    "labor_preset_create": "Werksjabloon \"{name}\" aangemaakt",
+    "labor_preset_delete": "Werksjabloon {id} verwijderd",
+    "labor_preset_update": "Werksjabloon {id} bijgewerkt",
+    "note_create": "Notitie aangemaakt op voertuig {vehicleId}",
+    "note_delete": "Notitie {id} verwijderd",
+    "organization_create": "Organisatie \"{name}\" aangemaakt",
+    "organization_sampleDataRemove": "Voorbeeldgegevens uit de installatie verwijderd",
+    "payment_create": "Betaling {amount} geregistreerd voor opdracht {serviceRecordId}",
+    "payment_delete": "Betaling {id} verwijderd",
+    "quoteRequest_update": "Status van offerteaanvraag gewijzigd naar {status, select, pending {in behandeling} approved {goedgekeurd} rejected {afgewezen} completed {afgerond} other {{status}}}",
+    "quote_convert": "Offerte {ref} omgezet naar opdracht {serviceRecordId}",
+    "quote_create": "Offerte {ref} aangemaakt",
+    "quote_delete": "Offerte {ref} verwijderd",
+    "quote_status": "Offertestatus gewijzigd naar {status, select, draft {concept} sent {verzonden} accepted {geaccepteerd} rejected {afgewezen} expired {verlopen} converted {omgezet} other {{status}}}",
+    "quote_update": "Offerte {ref} bijgewerkt",
+    "recurringInvoice_create": "Terugkerende factuur \"{title}\" aangemaakt",
+    "recurringInvoice_delete": "Terugkerende factuur {id} verwijderd",
+    "recurringInvoice_update": "Terugkerende factuur \"{title}\" bijgewerkt",
+    "reminder_create": "Herinnering \"{title}\" aangemaakt",
+    "reminder_delete": "Herinnering {id} verwijderd",
+    "role_create": "Rol \"{name}\" aangemaakt",
+    "role_delete": "Rol \"{name}\" verwijderd",
+    "role_update": "Rol \"{name}\" bijgewerkt",
+    "scheduled_message_cancel": "Ingepland bericht geannuleerd",
+    "scheduled_message_create": "{channel, select, email {E-mail} sms {Sms} telegram {Telegram-bericht} in_app {Melding in de app} other {{channel}}} ingepland voor {sendAt}",
+    "scheduled_message_delete": "Ingepland bericht verwijderd",
+    "scheduled_message_send_now": "Ingeplande {channel, select, email {e-mail} sms {sms} telegram {Telegram-bericht} in_app {melding in de app} other {{channel}}} eerder verstuurd",
+    "scheduled_message_update": "Ingeplande {channel, select, email {e-mail} sms {sms} telegram {Telegram-bericht} in_app {melding in de app} other {{channel}}} bijgewerkt",
+    "service_create": "Opdracht {ref} aangemaakt",
+    "service_createCounterSale": "Baliekoop {ref} aangemaakt",
+    "service_createDraft": "Conceptopdracht {ref} aangemaakt",
+    "service_createFromInspection": "Werkorder {ref} aangemaakt vanuit een keuring",
+    "service_delete": "Opdracht {ref} verwijderd",
+    "service_status": "Opdrachtstatus gewijzigd naar {status, select, pending {open} scheduled {ingepland} in_progress {in behandeling} waiting_parts {wacht op onderdelen} ready {klaar} completed {afgerond} cancelled {geannuleerd} other {{status}}}",
+    "service_update": "Opdracht {ref} bijgewerkt",
+    "settings_applyTaxRate": "{rate}% btw toegepast op {records, plural, one {# opdracht} other {# opdrachten}} en {quotes, plural, one {# offerte} other {# offertes}}",
+    "settings_convertToExclusive": "{records, plural, one {# opdracht} other {# opdrachten}} en {quotes, plural, one {# offerte} other {# offertes}} omgezet naar prijzen exclusief btw",
+    "settings_convertToInclusive": "{records, plural, one {# opdracht} other {# opdrachten}} en {quotes, plural, one {# offerte} other {# offertes}} omgezet naar prijzen inclusief btw",
+    "settings_deleteContent": "Alle gegevens verwijderd: {items}",
+    "settings_updateInvoiceLayout": "Factuurindeling bijgewerkt",
+    "settings_updateQuoteLayout": "Offerte-indeling bijgewerkt",
+    "sms_send": "Sms verstuurd naar {name} ({phone})",
+    "statusReport_create": "Statusrapport aangemaakt voor opdracht {serviceRecordId}",
+    "statusReport_delete": "Statusrapport verwijderd",
+    "statusReport_send": "Statusrapport verstuurd via {channels}",
+    "subscription_cancel": "Abonnement opgezegd per einde van de periode",
+    "subscription_resume": "Abonnement hervat",
+    "team_cancelInvitation": "Uitnodiging voor {email} ingetrokken",
+    "team_removeMember": "Teamlid {id} verwijderd",
+    "team_sendInvitation": "{email} uitgenodigd als {role, select, owner {eigenaar} admin {beheerder} member {lid} other {{role}}}",
+    "team_updateRole": "Rol van een lid gewijzigd naar {role, select, owner {eigenaar} admin {beheerder} member {lid} other {{role}}}",
+    "technician_create": "Monteur \"{name}\" aangemaakt",
+    "technician_delete": "Monteur {id} verwijderd",
+    "technician_update": "Monteur {id} bijgewerkt",
+    "telegram_send": "Telegram-bericht verstuurd naar {name}",
+    "tire_location_bulk_create": "{count, plural, one {# bandenplek} other {# bandenplekken}} aangemaakt",
+    "tire_location_create": "Bandenplek {code} aangemaakt",
+    "tire_location_delete": "Bandenplek {code} verwijderd",
+    "tire_location_update": "Bandenplek {code} bijgewerkt",
+    "tire_measurement_create": "{count, plural, one {# meting} other {# metingen}} vastgelegd op bandenset {ref}",
+    "tire_set_add_to_work_order": "Bandenset {ref} toegevoegd aan werkorder {jobRef}",
+    "tire_set_attach": "{count, plural, one {# bestand} other {# bestanden}} toegevoegd aan bandenset {ref}",
+    "tire_set_check_in": "Bandenset {ref} ingenomen op {code}",
+    "tire_set_check_out": "Bandenset {ref} meegegeven",
+    "tire_set_create_quote": "Offerte {ref} aangemaakt vanuit een bandenset",
+    "tire_set_create_work_order": "Werkorder {ref} aangemaakt vanuit een bandenset",
+    "tire_set_delete": "Bandenset {ref} verwijderd",
+    "tire_set_detach": "{fileName} verwijderd van een bandenset",
+    "tire_set_dispose": "Bandenset {ref} afgeschreven",
+    "tire_set_message_customer": "Klant bericht over bandenset {ref} via {channel, select, email {e-mail} sms {sms} telegram {Telegram} in_app {melding in de app} other {{channel}}}",
+    "tire_set_relocate": "Bandenset {ref} verplaatst van {from} naar {to}",
+    "tire_set_relocateUnassigned": "Bandenset {ref} op {to} gezet",
+    "tire_set_return": "Bandenset {ref} opnieuw opgeslagen, op {code}",
+    "tire_set_unlink_work_order": "Bandenset {ref} losgekoppeld van werkorder {jobRef}",
+    "tire_set_update": "Bandenset {ref} bijgewerkt",
+    "tire_treatment_complete_all": "{count, plural, one {# voorbereidende klus} other {# voorbereidende klussen}} afgerond op bandenset {ref}",
+    "tire_treatment_mark": "{type, select, wash_tires {Banden wassen} wash_rims {Velgen wassen} balance {Balanceren} tpms_service {TPMS-service} new_valves {Nieuwe ventielen} repair {Reparatie} other {{type}}} op bandenset {ref} gemarkeerd als {status, select, done {klaar} pending {nog te doen} skipped {overgeslagen} other {{status}}}",
+    "tire_treatment_update": "Voorbereidend werk op bandenset {ref} bijgewerkt",
+    "tire_warehouse_archive": "Bandenopslag {name} gearchiveerd",
+    "tire_warehouse_create": "Bandenopslag {name} aangemaakt",
+    "tire_warehouse_delete": "Bandenopslag {name} verwijderd",
+    "tire_warehouse_update": "Bandenopslag {name} bijgewerkt",
+    "vehicle_archive": "Voertuig {id} gearchiveerd",
+    "vehicle_create": "Voertuig {year} {make} {model} aangemaakt",
+    "vehicle_delete": "Voertuig {name} verwijderd",
+    "vehicle_unarchive": "Voertuig {id} uit het archief gehaald",
+    "vehicle_update": "Voertuig {name} bijgewerkt, gewijzigd: {fields}",
+    "vehicle_updateNoChanges": "Voertuig {name} bijgewerkt, niets gewijzigd",
+    "webhook_create": "Webhook {name} aangemaakt"
   }
   }
 }
 }

+ 119 - 0
messages/pl/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Spisano komplet opon",
     "tire_set_dispose": "Spisano komplet opon",
     "tire_set_attach": "Dodano pliki do kompletu opon",
     "tire_set_attach": "Dodano pliki do kompletu opon",
     "tire_set_detach": "Usunięto plik z kompletu opon"
     "tire_set_detach": "Usunięto plik z kompletu opon"
+  },
+  "summary": {
+    "customField_create": "Utworzono pole niestandardowe „{name}”",
+    "customField_delete": "Usunięto pole niestandardowe „{name}”",
+    "customField_update": "Zaktualizowano pole niestandardowe „{name}”",
+    "customer_backfillNumbers": "Nadano numery klienta {count, plural, one {# klientowi} few {# klientom} many {# klientom} other {# klientom}}",
+    "customer_create": "Utworzono klienta {name}",
+    "customer_delete": "Usunięto klienta {id}",
+    "customer_update": "Zaktualizowano klienta {id}",
+    "email_sendInspection": "Wysłano przegląd e-mailem do {recipient}",
+    "email_sendInvoice": "Wysłano fakturę e-mailem do {recipient}",
+    "email_sendQuote": "Wysłano wycenę e-mailem do {recipient}",
+    "finding_create": "Utworzono usterkę „{description}” w pojeździe {vehicleId}",
+    "finding_delete": "Usunięto usterkę {id}",
+    "finding_resolve": "Rozwiązano usterkę „{description}”",
+    "finding_update": "Zaktualizowano usterkę „{description}”",
+    "inspectionTemplate_create": "Utworzono szablon przeglądu „{name}”",
+    "inspectionTemplate_createFromPreset": "Utworzono szablon przeglądu „{name}” z gotowego zestawu",
+    "inspectionTemplate_delete": "Usunięto szablon przeglądu „{name}”",
+    "inspectionTemplate_duplicate": "Zduplikowano szablon przeglądu jako „{name}”",
+    "inspectionTemplate_import": "Zaimportowano z pakietu {count, plural, one {# szablon przeglądu} few {# szablony przeglądu} many {# szablonów przeglądu} other {# szablonu przeglądu}}",
+    "inspectionTemplate_update": "Zaktualizowano szablon przeglądu „{name}”",
+    "inspection_complete": "Zakończono przegląd {id}",
+    "inspection_create": "Utworzono przegląd {id}",
+    "inspection_delete": "Usunięto przegląd {id}",
+    "inspection_reopen": "Ponownie otwarto przegląd {id}",
+    "inventory_bulkDelete": "Usunięto zbiorczo {count, plural, one {# część magazynową} few {# części magazynowe} many {# części magazynowych} other {# części magazynowej}}",
+    "inventory_create": "Utworzono część magazynową „{name}”",
+    "inventory_delete": "Usunięto część magazynową {id}",
+    "inventory_update": "Zaktualizowano część magazynową {id}",
+    "labor_preset_create": "Utworzono szablon robocizny „{name}”",
+    "labor_preset_delete": "Usunięto szablon robocizny {id}",
+    "labor_preset_update": "Zaktualizowano szablon robocizny {id}",
+    "note_create": "Utworzono notatkę przy pojeździe {vehicleId}",
+    "note_delete": "Usunięto notatkę {id}",
+    "organization_create": "Utworzono organizację „{name}”",
+    "organization_sampleDataRemove": "Usunięto dane przykładowe z konfiguracji startowej",
+    "payment_create": "Zarejestrowano płatność {amount} do zlecenia {serviceRecordId}",
+    "payment_delete": "Usunięto płatność {id}",
+    "quoteRequest_update": "Zmieniono status zapytania o wycenę na {status, select, pending {oczekujące} approved {zatwierdzone} rejected {odrzucone} completed {zakończone} other {{status}}}",
+    "quote_convert": "Przekształcono wycenę {ref} w zlecenie {serviceRecordId}",
+    "quote_create": "Utworzono wycenę {ref}",
+    "quote_delete": "Usunięto wycenę {ref}",
+    "quote_status": "Zmieniono status wyceny na {status, select, draft {szkic} sent {wysłana} accepted {zaakceptowana} rejected {odrzucona} expired {wygasła} converted {przekształcona} other {{status}}}",
+    "quote_update": "Zaktualizowano wycenę {ref}",
+    "recurringInvoice_create": "Utworzono fakturę cykliczną „{title}”",
+    "recurringInvoice_delete": "Usunięto fakturę cykliczną {id}",
+    "recurringInvoice_update": "Zaktualizowano fakturę cykliczną „{title}”",
+    "reminder_create": "Utworzono przypomnienie „{title}”",
+    "reminder_delete": "Usunięto przypomnienie {id}",
+    "role_create": "Utworzono rolę „{name}”",
+    "role_delete": "Usunięto rolę „{name}”",
+    "role_update": "Zaktualizowano rolę „{name}”",
+    "scheduled_message_cancel": "Anulowano zaplanowaną wiadomość",
+    "scheduled_message_create": "Zaplanowano {channel, select, email {e-mail} sms {SMS} telegram {wiadomość Telegram} in_app {powiadomienie w aplikacji} other {{channel}}} na {sendAt}",
+    "scheduled_message_delete": "Usunięto zaplanowaną wiadomość",
+    "scheduled_message_send_now": "Wysłano przed czasem zaplanowany {channel, select, email {e-mail} sms {SMS} telegram {wiadomość Telegram} in_app {powiadomienie w aplikacji} other {{channel}}}",
+    "scheduled_message_update": "Zaktualizowano zaplanowany {channel, select, email {e-mail} sms {SMS} telegram {wiadomość Telegram} in_app {powiadomienie w aplikacji} other {{channel}}}",
+    "service_create": "Utworzono zlecenie {ref}",
+    "service_createCounterSale": "Utworzono sprzedaż z lady {ref}",
+    "service_createDraft": "Utworzono szkic zlecenia {ref}",
+    "service_createFromInspection": "Utworzono zlecenie warsztatowe {ref} z przeglądu",
+    "service_delete": "Usunięto zlecenie {ref}",
+    "service_status": "Zmieniono status zlecenia na {status, select, pending {oczekujące} scheduled {zaplanowane} in_progress {w realizacji} waiting_parts {czeka na części} ready {gotowe} completed {zakończone} cancelled {anulowane} other {{status}}}",
+    "service_update": "Zaktualizowano zlecenie {ref}",
+    "settings_applyTaxRate": "Zastosowano {rate}% podatku do {records, plural, one {# zlecenia} few {# zleceń} many {# zleceń} other {# zlecenia}} i {quotes, plural, one {# wyceny} few {# wycen} many {# wycen} other {# wyceny}}",
+    "settings_convertToExclusive": "Przeliczono {records, plural, one {# zlecenie} few {# zlecenia} many {# zleceń} other {# zlecenia}} i {quotes, plural, one {# wycenę} few {# wyceny} many {# wycen} other {# wyceny}} na ceny netto",
+    "settings_convertToInclusive": "Przeliczono {records, plural, one {# zlecenie} few {# zlecenia} many {# zleceń} other {# zlecenia}} i {quotes, plural, one {# wycenę} few {# wyceny} many {# wycen} other {# wyceny}} na ceny brutto",
+    "settings_deleteContent": "Usunięto wszystkie dane: {items}",
+    "settings_updateInvoiceLayout": "Zaktualizowano układ faktury",
+    "settings_updateQuoteLayout": "Zaktualizowano układ wyceny",
+    "sms_send": "Wysłano SMS do {name} ({phone})",
+    "statusReport_create": "Utworzono raport statusu dla zlecenia {serviceRecordId}",
+    "statusReport_delete": "Usunięto raport statusu",
+    "statusReport_send": "Wysłano raport statusu przez {channels}",
+    "subscription_cancel": "Anulowano abonament ze skutkiem na koniec okresu",
+    "subscription_resume": "Wznowiono abonament",
+    "team_cancelInvitation": "Anulowano zaproszenie dla {email}",
+    "team_removeMember": "Usunięto członka zespołu {id}",
+    "team_sendInvitation": "Zaproszono {email} jako {role, select, owner {właściciela} admin {administratora} member {członka} other {{role}}}",
+    "team_updateRole": "Zmieniono rolę członka na {role, select, owner {właściciel} admin {administrator} member {członek} other {{role}}}",
+    "technician_create": "Utworzono mechanika „{name}”",
+    "technician_delete": "Usunięto mechanika {id}",
+    "technician_update": "Zaktualizowano mechanika {id}",
+    "telegram_send": "Wysłano wiadomość Telegram do {name}",
+    "tire_location_bulk_create": "Utworzono {count, plural, one {# miejsce na opony} few {# miejsca na opony} many {# miejsc na opony} other {# miejsca na opony}}",
+    "tire_location_create": "Utworzono miejsce na opony {code}",
+    "tire_location_delete": "Usunięto miejsce na opony {code}",
+    "tire_location_update": "Zaktualizowano miejsce na opony {code}",
+    "tire_measurement_create": "Zapisano {count, plural, one {# pomiar} few {# pomiary} many {# pomiarów} other {# pomiaru}} na zestawie opon {ref}",
+    "tire_set_add_to_work_order": "Dodano zestaw opon {ref} do zlecenia {jobRef}",
+    "tire_set_attach": "Dodano {count, plural, one {# plik} few {# pliki} many {# plików} other {# pliku}} do zestawu opon {ref}",
+    "tire_set_check_in": "Przyjęto zestaw opon {ref} na {code}",
+    "tire_set_check_out": "Wydano zestaw opon {ref}",
+    "tire_set_create_quote": "Utworzono wycenę {ref} z zestawu opon",
+    "tire_set_create_work_order": "Utworzono zlecenie warsztatowe {ref} z zestawu opon",
+    "tire_set_delete": "Usunięto zestaw opon {ref}",
+    "tire_set_detach": "Usunięto {fileName} z zestawu opon",
+    "tire_set_dispose": "Zezłomowano zestaw opon {ref}",
+    "tire_set_message_customer": "Powiadomiono klienta o zestawie opon {ref} przez {channel, select, email {e-mail} sms {SMS} telegram {Telegram} in_app {powiadomienie w aplikacji} other {{channel}}}",
+    "tire_set_relocate": "Przeniesiono zestaw opon {ref} z {from} na {to}",
+    "tire_set_relocateUnassigned": "Ustawiono zestaw opon {ref} na {to}",
+    "tire_set_return": "Ponownie przyjęto zestaw opon {ref}, na {code}",
+    "tire_set_unlink_work_order": "Odłączono zestaw opon {ref} od zlecenia {jobRef}",
+    "tire_set_update": "Zaktualizowano zestaw opon {ref}",
+    "tire_treatment_complete_all": "Ukończono {count, plural, one {# pracę przygotowawczą} few {# prace przygotowawcze} many {# prac przygotowawczych} other {# pracy przygotowawczej}} przy zestawie opon {ref}",
+    "tire_treatment_mark": "Oznaczono {type, select, wash_tires {mycie opon} wash_rims {mycie felg} balance {wyważanie} tpms_service {serwis TPMS} new_valves {nowe zawory} repair {naprawę} other {{type}}} jako {status, select, done {wykonane} pending {do zrobienia} skipped {pominięte} other {{status}}} przy zestawie opon {ref}",
+    "tire_treatment_update": "Zaktualizowano prace przygotowawcze przy zestawie opon {ref}",
+    "tire_warehouse_archive": "Zarchiwizowano magazyn opon {name}",
+    "tire_warehouse_create": "Utworzono magazyn opon {name}",
+    "tire_warehouse_delete": "Usunięto magazyn opon {name}",
+    "tire_warehouse_update": "Zaktualizowano magazyn opon {name}",
+    "vehicle_archive": "Zarchiwizowano pojazd {id}",
+    "vehicle_create": "Utworzono pojazd {year} {make} {model}",
+    "vehicle_delete": "Usunięto pojazd {name}",
+    "vehicle_unarchive": "Przywrócono pojazd {id} z archiwum",
+    "vehicle_update": "Zaktualizowano pojazd {name}, zmieniono: {fields}",
+    "vehicle_updateNoChanges": "Zaktualizowano pojazd {name}, nic nie zmieniono",
+    "webhook_create": "Utworzono webhook {name}"
   }
   }
 }
 }

+ 119 - 0
messages/pt-BR/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Jogo de pneus baixado",
     "tire_set_dispose": "Jogo de pneus baixado",
     "tire_set_attach": "Arquivos adicionados a um jogo de pneus",
     "tire_set_attach": "Arquivos adicionados a um jogo de pneus",
     "tire_set_detach": "Arquivo removido de um jogo de pneus"
     "tire_set_detach": "Arquivo removido de um jogo de pneus"
+  },
+  "summary": {
+    "customField_create": "Campo personalizado \"{name}\" criado",
+    "customField_delete": "Campo personalizado \"{name}\" excluído",
+    "customField_update": "Campo personalizado \"{name}\" atualizado",
+    "customer_backfillNumbers": "Números de cliente atribuídos a {count, plural, one {# cliente} other {# clientes}}",
+    "customer_create": "Cliente {name} criado",
+    "customer_delete": "Cliente {id} excluído",
+    "customer_update": "Cliente {id} atualizado",
+    "email_sendInspection": "Inspeção enviada por e-mail para {recipient}",
+    "email_sendInvoice": "Fatura enviada por e-mail para {recipient}",
+    "email_sendQuote": "Orçamento enviado por e-mail para {recipient}",
+    "finding_create": "Constatação \"{description}\" criada no veículo {vehicleId}",
+    "finding_delete": "Constatação {id} excluída",
+    "finding_resolve": "Constatação \"{description}\" resolvida",
+    "finding_update": "Constatação \"{description}\" atualizada",
+    "inspectionTemplate_create": "Modelo de inspeção \"{name}\" criado",
+    "inspectionTemplate_createFromPreset": "Modelo de inspeção \"{name}\" criado a partir de uma predefinição",
+    "inspectionTemplate_delete": "Modelo de inspeção \"{name}\" excluído",
+    "inspectionTemplate_duplicate": "Modelo de inspeção duplicado como \"{name}\"",
+    "inspectionTemplate_import": "{count, plural, one {# modelo de inspeção importado} other {# modelos de inspeção importados}} de um pacote",
+    "inspectionTemplate_update": "Modelo de inspeção \"{name}\" atualizado",
+    "inspection_complete": "Inspeção {id} concluída",
+    "inspection_create": "Inspeção {id} criada",
+    "inspection_delete": "Inspeção {id} excluída",
+    "inspection_reopen": "Inspeção {id} reaberta",
+    "inventory_bulkDelete": "{count, plural, one {# peça de estoque excluída} other {# peças de estoque excluídas}} em massa",
+    "inventory_create": "Peça de estoque \"{name}\" criada",
+    "inventory_delete": "Peça de estoque {id} excluída",
+    "inventory_update": "Peça de estoque {id} atualizada",
+    "labor_preset_create": "Predefinição de mão de obra \"{name}\" criada",
+    "labor_preset_delete": "Predefinição de mão de obra {id} excluída",
+    "labor_preset_update": "Predefinição de mão de obra {id} atualizada",
+    "note_create": "Nota criada no veículo {vehicleId}",
+    "note_delete": "Nota {id} excluída",
+    "organization_create": "Organização \"{name}\" criada",
+    "organization_sampleDataRemove": "Dados de exemplo da configuração inicial removidos",
+    "payment_create": "Pagamento {amount} registrado para o serviço {serviceRecordId}",
+    "payment_delete": "Pagamento {id} excluído",
+    "quoteRequest_update": "Status da solicitação de orçamento alterado para {status, select, pending {pendente} approved {aprovada} rejected {recusada} completed {concluída} other {{status}}}",
+    "quote_convert": "Orçamento {ref} convertido no serviço {serviceRecordId}",
+    "quote_create": "Orçamento {ref} criado",
+    "quote_delete": "Orçamento {ref} excluído",
+    "quote_status": "Status do orçamento alterado para {status, select, draft {rascunho} sent {enviado} accepted {aceito} rejected {recusado} expired {vencido} converted {convertido} other {{status}}}",
+    "quote_update": "Orçamento {ref} atualizado",
+    "recurringInvoice_create": "Fatura recorrente \"{title}\" criada",
+    "recurringInvoice_delete": "Fatura recorrente {id} excluída",
+    "recurringInvoice_update": "Fatura recorrente \"{title}\" atualizada",
+    "reminder_create": "Lembrete \"{title}\" criado",
+    "reminder_delete": "Lembrete {id} excluído",
+    "role_create": "Função \"{name}\" criada",
+    "role_delete": "Função \"{name}\" excluída",
+    "role_update": "Função \"{name}\" atualizada",
+    "scheduled_message_cancel": "Mensagem agendada cancelada",
+    "scheduled_message_create": "{channel, select, email {E-mail} sms {SMS} telegram {Mensagem do Telegram} in_app {Aviso no aplicativo} other {{channel}}} agendado para {sendAt}",
+    "scheduled_message_delete": "Mensagem agendada excluída",
+    "scheduled_message_send_now": "{channel, select, email {E-mail} sms {SMS} telegram {Mensagem do Telegram} in_app {Aviso no aplicativo} other {{channel}}} agendado enviado antes da hora",
+    "scheduled_message_update": "{channel, select, email {E-mail} sms {SMS} telegram {Mensagem do Telegram} in_app {Aviso no aplicativo} other {{channel}}} agendado atualizado",
+    "service_create": "Serviço {ref} criado",
+    "service_createCounterSale": "Venda no balcão {ref} criada",
+    "service_createDraft": "Rascunho de serviço {ref} criado",
+    "service_createFromInspection": "Ordem de serviço {ref} criada a partir de uma inspeção",
+    "service_delete": "Serviço {ref} excluído",
+    "service_status": "Status do serviço alterado para {status, select, pending {pendente} scheduled {agendado} in_progress {em andamento} waiting_parts {aguardando peças} ready {pronto} completed {concluído} cancelled {cancelado} other {{status}}}",
+    "service_update": "Serviço {ref} atualizado",
+    "settings_applyTaxRate": "Imposto de {rate}% aplicado a {records, plural, one {# serviço} other {# serviços}} e {quotes, plural, one {# orçamento} other {# orçamentos}}",
+    "settings_convertToExclusive": "{records, plural, one {# serviço convertido} other {# serviços convertidos}} e {quotes, plural, one {# orçamento convertido} other {# orçamentos convertidos}} para preços sem imposto",
+    "settings_convertToInclusive": "{records, plural, one {# serviço convertido} other {# serviços convertidos}} e {quotes, plural, one {# orçamento convertido} other {# orçamentos convertidos}} para preços com imposto",
+    "settings_deleteContent": "Todos os dados excluídos: {items}",
+    "settings_updateInvoiceLayout": "Layout da fatura atualizado",
+    "settings_updateQuoteLayout": "Layout do orçamento atualizado",
+    "sms_send": "SMS enviado para {name} ({phone})",
+    "statusReport_create": "Relatório de status criado para o serviço {serviceRecordId}",
+    "statusReport_delete": "Relatório de status excluído",
+    "statusReport_send": "Relatório de status enviado via {channels}",
+    "subscription_cancel": "Assinatura cancelada, com efeito no fim do período",
+    "subscription_resume": "Assinatura retomada",
+    "team_cancelInvitation": "Convite para {email} cancelado",
+    "team_removeMember": "Membro da equipe {id} removido",
+    "team_sendInvitation": "{email} convidado como {role, select, owner {proprietário} admin {administrador} member {membro} other {{role}}}",
+    "team_updateRole": "Função de um membro alterada para {role, select, owner {proprietário} admin {administrador} member {membro} other {{role}}}",
+    "technician_create": "Técnico \"{name}\" criado",
+    "technician_delete": "Técnico {id} excluído",
+    "technician_update": "Técnico {id} atualizado",
+    "telegram_send": "Mensagem do Telegram enviada para {name}",
+    "tire_location_bulk_create": "{count, plural, one {# local de pneus criado} other {# locais de pneus criados}}",
+    "tire_location_create": "Local de pneus {code} criado",
+    "tire_location_delete": "Local de pneus {code} excluído",
+    "tire_location_update": "Local de pneus {code} atualizado",
+    "tire_measurement_create": "{count, plural, one {# medição registrada} other {# medições registradas}} no jogo de pneus {ref}",
+    "tire_set_add_to_work_order": "Jogo de pneus {ref} adicionado à ordem de serviço {jobRef}",
+    "tire_set_attach": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} ao jogo de pneus {ref}",
+    "tire_set_check_in": "Jogo de pneus {ref} guardado em {code}",
+    "tire_set_check_out": "Jogo de pneus {ref} entregue",
+    "tire_set_create_quote": "Orçamento {ref} criado a partir de um jogo de pneus",
+    "tire_set_create_work_order": "Ordem de serviço {ref} criada a partir de um jogo de pneus",
+    "tire_set_delete": "Jogo de pneus {ref} excluído",
+    "tire_set_detach": "{fileName} removido de um jogo de pneus",
+    "tire_set_dispose": "Jogo de pneus {ref} descartado",
+    "tire_set_message_customer": "Cliente avisado sobre o jogo de pneus {ref} por {channel, select, email {e-mail} sms {SMS} telegram {Telegram} in_app {aviso no aplicativo} other {{channel}}}",
+    "tire_set_relocate": "Jogo de pneus {ref} movido de {from} para {to}",
+    "tire_set_relocateUnassigned": "Jogo de pneus {ref} colocado em {to}",
+    "tire_set_return": "Jogo de pneus {ref} guardado novamente, em {code}",
+    "tire_set_unlink_work_order": "Jogo de pneus {ref} desvinculado da ordem de serviço {jobRef}",
+    "tire_set_update": "Jogo de pneus {ref} atualizado",
+    "tire_treatment_complete_all": "{count, plural, one {# tarefa de preparação concluída} other {# tarefas de preparação concluídas}} no jogo de pneus {ref}",
+    "tire_treatment_mark": "{type, select, wash_tires {Lavagem dos pneus} wash_rims {Lavagem das rodas} balance {Balanceamento} tpms_service {Serviço TPMS} new_valves {Válvulas novas} repair {Reparo} other {{type}}} marcado como {status, select, done {feito} pending {a fazer} skipped {ignorado} other {{status}}} no jogo de pneus {ref}",
+    "tire_treatment_update": "Preparação do jogo de pneus {ref} atualizada",
+    "tire_warehouse_archive": "Depósito de pneus {name} arquivado",
+    "tire_warehouse_create": "Depósito de pneus {name} criado",
+    "tire_warehouse_delete": "Depósito de pneus {name} excluído",
+    "tire_warehouse_update": "Depósito de pneus {name} atualizado",
+    "vehicle_archive": "Veículo {id} arquivado",
+    "vehicle_create": "Veículo {year} {make} {model} criado",
+    "vehicle_delete": "Veículo {name} excluído",
+    "vehicle_unarchive": "Veículo {id} retirado do arquivo",
+    "vehicle_update": "Veículo {name} atualizado, alterado: {fields}",
+    "vehicle_updateNoChanges": "Veículo {name} atualizado, nada alterado",
+    "webhook_create": "Webhook {name} criado"
   }
   }
 }
 }

+ 119 - 0
messages/ru/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Комплект шин списан",
     "tire_set_dispose": "Комплект шин списан",
     "tire_set_attach": "Файлы добавлены к комплекту шин",
     "tire_set_attach": "Файлы добавлены к комплекту шин",
     "tire_set_detach": "Файл удалён из комплекта шин"
     "tire_set_detach": "Файл удалён из комплекта шин"
+  },
+  "summary": {
+    "customField_create": "Создано настраиваемое поле «{name}»",
+    "customField_delete": "Удалено настраиваемое поле «{name}»",
+    "customField_update": "Обновлено настраиваемое поле «{name}»",
+    "customer_backfillNumbers": "Номера клиентов присвоены {count, plural, one {# клиенту} few {# клиентам} many {# клиентам} other {# клиентам}}",
+    "customer_create": "Создан клиент {name}",
+    "customer_delete": "Удалён клиент {id}",
+    "customer_update": "Обновлён клиент {id}",
+    "email_sendInspection": "Отчёт о проверке отправлен по почте на {recipient}",
+    "email_sendInvoice": "Счёт отправлен по почте на {recipient}",
+    "email_sendQuote": "Предложение отправлено по почте на {recipient}",
+    "finding_create": "Создана неисправность «{description}» на автомобиле {vehicleId}",
+    "finding_delete": "Удалена неисправность {id}",
+    "finding_resolve": "Устранена неисправность «{description}»",
+    "finding_update": "Обновлена неисправность «{description}»",
+    "inspectionTemplate_create": "Создан шаблон проверки «{name}»",
+    "inspectionTemplate_createFromPreset": "Создан шаблон проверки «{name}» из готового набора",
+    "inspectionTemplate_delete": "Удалён шаблон проверки «{name}»",
+    "inspectionTemplate_duplicate": "Шаблон проверки скопирован как «{name}»",
+    "inspectionTemplate_import": "Импортировано из пакета {count, plural, one {# шаблон проверки} few {# шаблона проверки} many {# шаблонов проверки} other {# шаблона проверки}}",
+    "inspectionTemplate_update": "Обновлён шаблон проверки «{name}»",
+    "inspection_complete": "Завершена проверка {id}",
+    "inspection_create": "Создана проверка {id}",
+    "inspection_delete": "Удалена проверка {id}",
+    "inspection_reopen": "Проверка {id} открыта заново",
+    "inventory_bulkDelete": "Массово удалено {count, plural, one {# деталь} few {# детали} many {# деталей} other {# детали}} со склада",
+    "inventory_create": "Создана деталь на складе «{name}»",
+    "inventory_delete": "Удалена деталь на складе {id}",
+    "inventory_update": "Обновлена деталь на складе {id}",
+    "labor_preset_create": "Создан шаблон работ «{name}»",
+    "labor_preset_delete": "Удалён шаблон работ {id}",
+    "labor_preset_update": "Обновлён шаблон работ {id}",
+    "note_create": "Создана заметка к автомобилю {vehicleId}",
+    "note_delete": "Удалена заметка {id}",
+    "organization_create": "Создана организация «{name}»",
+    "organization_sampleDataRemove": "Удалены demo-данные первичной настройки",
+    "payment_create": "Записан платёж {amount} по заказу {serviceRecordId}",
+    "payment_delete": "Удалён платёж {id}",
+    "quoteRequest_update": "Статус запроса на предложение изменён на «{status, select, pending {в ожидании} approved {одобрен} rejected {отклонён} completed {завершён} other {{status}}}»",
+    "quote_convert": "Предложение {ref} преобразовано в заказ {serviceRecordId}",
+    "quote_create": "Создано предложение {ref}",
+    "quote_delete": "Удалено предложение {ref}",
+    "quote_status": "Статус предложения изменён на «{status, select, draft {черновик} sent {отправлено} accepted {принято} rejected {отклонено} expired {просрочено} converted {преобразовано} other {{status}}}»",
+    "quote_update": "Обновлено предложение {ref}",
+    "recurringInvoice_create": "Создан регулярный счёт «{title}»",
+    "recurringInvoice_delete": "Удалён регулярный счёт {id}",
+    "recurringInvoice_update": "Обновлён регулярный счёт «{title}»",
+    "reminder_create": "Создано напоминание «{title}»",
+    "reminder_delete": "Удалено напоминание {id}",
+    "role_create": "Создана роль «{name}»",
+    "role_delete": "Удалена роль «{name}»",
+    "role_update": "Обновлена роль «{name}»",
+    "scheduled_message_cancel": "Запланированное сообщение отменено",
+    "scheduled_message_create": "Запланировано на {sendAt}: {channel, select, email {письмо} sms {SMS} telegram {сообщение в Telegram} in_app {уведомление в приложении} other {{channel}}}",
+    "scheduled_message_delete": "Запланированное сообщение удалено",
+    "scheduled_message_send_now": "Запланированное {channel, select, email {письмо} sms {SMS} telegram {сообщение в Telegram} in_app {уведомление в приложении} other {{channel}}} отправлено досрочно",
+    "scheduled_message_update": "Обновлено запланированное {channel, select, email {письмо} sms {SMS} telegram {сообщение в Telegram} in_app {уведомление в приложении} other {{channel}}}",
+    "service_create": "Создан заказ {ref}",
+    "service_createCounterSale": "Создана продажа со стойки {ref}",
+    "service_createDraft": "Создан черновик заказа {ref}",
+    "service_createFromInspection": "Создан наряд-заказ {ref} по итогам проверки",
+    "service_delete": "Удалён заказ {ref}",
+    "service_status": "Статус заказа изменён на «{status, select, pending {в ожидании} scheduled {запланирован} in_progress {в работе} waiting_parts {ждёт запчасти} ready {готов} completed {завершён} cancelled {отменён} other {{status}}}»",
+    "service_update": "Обновлён заказ {ref}",
+    "settings_applyTaxRate": "Налог {rate}% применён к {records, plural, one {# заказу} few {# заказам} many {# заказам} other {# заказам}} и {quotes, plural, one {# предложению} few {# предложениям} many {# предложениям} other {# предложениям}}",
+    "settings_convertToExclusive": "{records, plural, one {# заказ} few {# заказа} many {# заказов} other {# заказа}} и {quotes, plural, one {# предложение} few {# предложения} many {# предложений} other {# предложения}} переведены на цены без налога",
+    "settings_convertToInclusive": "{records, plural, one {# заказ} few {# заказа} many {# заказов} other {# заказа}} и {quotes, plural, one {# предложение} few {# предложения} many {# предложений} other {# предложения}} переведены на цены с налогом",
+    "settings_deleteContent": "Удалены все данные: {items}",
+    "settings_updateInvoiceLayout": "Обновлён макет счёта",
+    "settings_updateQuoteLayout": "Обновлён макет предложения",
+    "sms_send": "SMS отправлено {name} ({phone})",
+    "statusReport_create": "Создан отчёт о статусе по заказу {serviceRecordId}",
+    "statusReport_delete": "Удалён отчёт о статусе",
+    "statusReport_send": "Отчёт о статусе отправлен через {channels}",
+    "subscription_cancel": "Подписка отменена, с окончанием текущего периода",
+    "subscription_resume": "Подписка возобновлена",
+    "team_cancelInvitation": "Приглашение для {email} отозвано",
+    "team_removeMember": "Участник команды {id} удалён",
+    "team_sendInvitation": "{email} приглашён как {role, select, owner {владелец} admin {администратор} member {участник} other {{role}}}",
+    "team_updateRole": "Роль участника изменена на «{role, select, owner {владелец} admin {администратор} member {участник} other {{role}}}»",
+    "technician_create": "Создан механик «{name}»",
+    "technician_delete": "Удалён механик {id}",
+    "technician_update": "Обновлён механик {id}",
+    "telegram_send": "Сообщение в Telegram отправлено {name}",
+    "tire_location_bulk_create": "Создано {count, plural, one {# место хранения шин} few {# места хранения шин} many {# мест хранения шин} other {# места хранения шин}}",
+    "tire_location_create": "Создано место хранения шин {code}",
+    "tire_location_delete": "Удалено место хранения шин {code}",
+    "tire_location_update": "Обновлено место хранения шин {code}",
+    "tire_measurement_create": "Записано {count, plural, one {# измерение} few {# измерения} many {# измерений} other {# измерения}} по комплекту шин {ref}",
+    "tire_set_add_to_work_order": "Комплект шин {ref} добавлен в наряд-заказ {jobRef}",
+    "tire_set_attach": "К комплекту шин {ref} добавлено {count, plural, one {# файл} few {# файла} many {# файлов} other {# файла}}",
+    "tire_set_check_in": "Комплект шин {ref} принят на {code}",
+    "tire_set_check_out": "Комплект шин {ref} выдан",
+    "tire_set_create_quote": "Создано предложение {ref} по комплекту шин",
+    "tire_set_create_work_order": "Создан наряд-заказ {ref} по комплекту шин",
+    "tire_set_delete": "Удалён комплект шин {ref}",
+    "tire_set_detach": "{fileName} удалён из комплекта шин",
+    "tire_set_dispose": "Комплект шин {ref} списан",
+    "tire_set_message_customer": "Клиент извещён о комплекте шин {ref} через {channel, select, email {почту} sms {SMS} telegram {Telegram} in_app {уведомление в приложении} other {{channel}}}",
+    "tire_set_relocate": "Комплект шин {ref} перемещён с {from} на {to}",
+    "tire_set_relocateUnassigned": "Комплект шин {ref} поставлен на {to}",
+    "tire_set_return": "Комплект шин {ref} снова принят на хранение, на {code}",
+    "tire_set_unlink_work_order": "Комплект шин {ref} отвязан от наряд-заказа {jobRef}",
+    "tire_set_update": "Обновлён комплект шин {ref}",
+    "tire_treatment_complete_all": "Завершено {count, plural, one {# подготовительная работа} few {# подготовительные работы} many {# подготовительных работ} other {# подготовительной работы}} по комплекту шин {ref}",
+    "tire_treatment_mark": "{type, select, wash_tires {Мойка шин} wash_rims {Мойка дисков} balance {Балансировка} tpms_service {Обслуживание TPMS} new_valves {Новые вентили} repair {Ремонт} other {{type}}} по комплекту шин {ref} отмечено как «{status, select, done {выполнено} pending {в работе} skipped {пропущено} other {{status}}}»",
+    "tire_treatment_update": "Обновлена подготовка комплекта шин {ref}",
+    "tire_warehouse_archive": "Склад шин {name} отправлен в архив",
+    "tire_warehouse_create": "Создан склад шин {name}",
+    "tire_warehouse_delete": "Удалён склад шин {name}",
+    "tire_warehouse_update": "Обновлён склад шин {name}",
+    "vehicle_archive": "Автомобиль {id} отправлен в архив",
+    "vehicle_create": "Создан автомобиль {year} {make} {model}",
+    "vehicle_delete": "Удалён автомобиль {name}",
+    "vehicle_unarchive": "Автомобиль {id} возвращён из архива",
+    "vehicle_update": "Обновлён автомобиль {name}, изменено: {fields}",
+    "vehicle_updateNoChanges": "Обновлён автомобиль {name}, изменений нет",
+    "webhook_create": "Создан вебхук {name}"
   }
   }
 }
 }

+ 119 - 0
messages/tr/audit.json

@@ -136,5 +136,124 @@
     "tire_set_dispose": "Lastik takımı hurdaya ayrıldı",
     "tire_set_dispose": "Lastik takımı hurdaya ayrıldı",
     "tire_set_attach": "Lastik takımına dosya eklendi",
     "tire_set_attach": "Lastik takımına dosya eklendi",
     "tire_set_detach": "Lastik takımından dosya kaldırıldı"
     "tire_set_detach": "Lastik takımından dosya kaldırıldı"
+  },
+  "summary": {
+    "customField_create": "\"{name}\" özel alanı oluşturuldu",
+    "customField_delete": "\"{name}\" özel alanı silindi",
+    "customField_update": "\"{name}\" özel alanı güncellendi",
+    "customer_backfillNumbers": "{count, plural, one {# müşteriye} other {# müşteriye}} müşteri numarası atandı",
+    "customer_create": "{name} müşterisi oluşturuldu",
+    "customer_delete": "{id} müşterisi silindi",
+    "customer_update": "{id} müşterisi güncellendi",
+    "email_sendInspection": "Muayene {recipient} adresine e-postayla gönderildi",
+    "email_sendInvoice": "Fatura {recipient} adresine e-postayla gönderildi",
+    "email_sendQuote": "Teklif {recipient} adresine e-postayla gönderildi",
+    "finding_create": "{vehicleId} aracında \"{description}\" bulgusu oluşturuldu",
+    "finding_delete": "{id} bulgusu silindi",
+    "finding_resolve": "\"{description}\" bulgusu giderildi",
+    "finding_update": "\"{description}\" bulgusu güncellendi",
+    "inspectionTemplate_create": "\"{name}\" muayene şablonu oluşturuldu",
+    "inspectionTemplate_createFromPreset": "\"{name}\" muayene şablonu bir hazır ayardan oluşturuldu",
+    "inspectionTemplate_delete": "\"{name}\" muayene şablonu silindi",
+    "inspectionTemplate_duplicate": "Muayene şablonu \"{name}\" olarak çoğaltıldı",
+    "inspectionTemplate_import": "Bir paketten {count, plural, one {# muayene şablonu} other {# muayene şablonu}} içe aktarıldı",
+    "inspectionTemplate_update": "\"{name}\" muayene şablonu güncellendi",
+    "inspection_complete": "{id} muayenesi tamamlandı",
+    "inspection_create": "{id} muayenesi oluşturuldu",
+    "inspection_delete": "{id} muayenesi silindi",
+    "inspection_reopen": "{id} muayenesi yeniden açıldı",
+    "inventory_bulkDelete": "{count, plural, one {# stok parçası} other {# stok parçası}} toplu olarak silindi",
+    "inventory_create": "\"{name}\" stok parçası oluşturuldu",
+    "inventory_delete": "{id} stok parçası silindi",
+    "inventory_update": "{id} stok parçası güncellendi",
+    "labor_preset_create": "\"{name}\" işçilik ön ayarı oluşturuldu",
+    "labor_preset_delete": "{id} işçilik ön ayarı silindi",
+    "labor_preset_update": "{id} işçilik ön ayarı güncellendi",
+    "note_create": "{vehicleId} aracına not eklendi",
+    "note_delete": "{id} notu silindi",
+    "organization_create": "\"{name}\" organizasyonu oluşturuldu",
+    "organization_sampleDataRemove": "Kuruluma ait örnek veriler kaldırıldı",
+    "payment_create": "{serviceRecordId} işi için {amount} tutarında ödeme kaydedildi",
+    "payment_delete": "{id} ödemesi silindi",
+    "quoteRequest_update": "Teklif talebinin durumu {status, select, pending {beklemede} approved {onaylandı} rejected {reddedildi} completed {tamamlandı} other {{status}}} olarak değiştirildi",
+    "quote_convert": "{ref} teklifi {serviceRecordId} iş kaydına dönüştürüldü",
+    "quote_create": "{ref} teklifi oluşturuldu",
+    "quote_delete": "{ref} teklifi silindi",
+    "quote_status": "Teklif durumu {status, select, draft {taslak} sent {gönderildi} accepted {kabul edildi} rejected {reddedildi} expired {süresi doldu} converted {dönüştürüldü} other {{status}}} olarak değiştirildi",
+    "quote_update": "{ref} teklifi güncellendi",
+    "recurringInvoice_create": "\"{title}\" yinelenen faturası oluşturuldu",
+    "recurringInvoice_delete": "{id} yinelenen faturası silindi",
+    "recurringInvoice_update": "\"{title}\" yinelenen faturası güncellendi",
+    "reminder_create": "\"{title}\" hatırlatıcısı oluşturuldu",
+    "reminder_delete": "{id} hatırlatıcısı silindi",
+    "role_create": "\"{name}\" rolü oluşturuldu",
+    "role_delete": "\"{name}\" rolü silindi",
+    "role_update": "\"{name}\" rolü güncellendi",
+    "scheduled_message_cancel": "Planlanmış mesaj iptal edildi",
+    "scheduled_message_create": "{sendAt} için {channel, select, email {e-posta} sms {SMS} telegram {Telegram mesajı} in_app {uygulama içi bildirim} other {{channel}}} planlandı",
+    "scheduled_message_delete": "Planlanmış mesaj silindi",
+    "scheduled_message_send_now": "Planlanmış {channel, select, email {e-posta} sms {SMS} telegram {Telegram mesajı} in_app {uygulama içi bildirim} other {{channel}}} erkenden gönderildi",
+    "scheduled_message_update": "Planlanmış {channel, select, email {e-posta} sms {SMS} telegram {Telegram mesajı} in_app {uygulama içi bildirim} other {{channel}}} güncellendi",
+    "service_create": "{ref} iş kaydı oluşturuldu",
+    "service_createCounterSale": "{ref} tezgah satışı oluşturuldu",
+    "service_createDraft": "{ref} taslak iş kaydı oluşturuldu",
+    "service_createFromInspection": "Bir muayeneden {ref} iş emri oluşturuldu",
+    "service_delete": "{ref} iş kaydı silindi",
+    "service_status": "İş kaydının durumu {status, select, pending {beklemede} scheduled {planlandı} in_progress {devam ediyor} waiting_parts {parça bekliyor} ready {hazır} completed {tamamlandı} cancelled {iptal edildi} other {{status}}} olarak değiştirildi",
+    "service_update": "{ref} iş kaydı güncellendi",
+    "settings_applyTaxRate": "{records, plural, one {# iş kaydına} other {# iş kaydına}} ve {quotes, plural, one {# teklife} other {# teklife}} %{rate} vergi uygulandı",
+    "settings_convertToExclusive": "{records, plural, one {# iş kaydı} other {# iş kaydı}} ve {quotes, plural, one {# teklif} other {# teklif}} vergi hariç fiyatlara dönüştürüldü",
+    "settings_convertToInclusive": "{records, plural, one {# iş kaydı} other {# iş kaydı}} ve {quotes, plural, one {# teklif} other {# teklif}} vergi dahil fiyatlara dönüştürüldü",
+    "settings_deleteContent": "Tüm veriler silindi: {items}",
+    "settings_updateInvoiceLayout": "Fatura düzeni güncellendi",
+    "settings_updateQuoteLayout": "Teklif düzeni güncellendi",
+    "sms_send": "{name} ({phone}) kişisine SMS gönderildi",
+    "statusReport_create": "{serviceRecordId} işi için durum raporu oluşturuldu",
+    "statusReport_delete": "Durum raporu silindi",
+    "statusReport_send": "Durum raporu {channels} ile gönderildi",
+    "subscription_cancel": "Abonelik dönem sonunda geçerli olmak üzere iptal edildi",
+    "subscription_resume": "Abonelik yeniden başlatıldı",
+    "team_cancelInvitation": "{email} için gönderilen davet iptal edildi",
+    "team_removeMember": "{id} ekip üyesi çıkarıldı",
+    "team_sendInvitation": "{email} {role, select, owner {sahip} admin {yönetici} member {üye} other {{role}}} olarak davet edildi",
+    "team_updateRole": "Bir üyenin rolü {role, select, owner {sahip} admin {yönetici} member {üye} other {{role}}} olarak değiştirildi",
+    "technician_create": "\"{name}\" teknisyeni oluşturuldu",
+    "technician_delete": "{id} teknisyeni silindi",
+    "technician_update": "{id} teknisyeni güncellendi",
+    "telegram_send": "{name} kişisine Telegram mesajı gönderildi",
+    "tire_location_bulk_create": "{count, plural, one {# lastik rafı} other {# lastik rafı}} oluşturuldu",
+    "tire_location_create": "{code} lastik rafı oluşturuldu",
+    "tire_location_delete": "{code} lastik rafı silindi",
+    "tire_location_update": "{code} lastik rafı güncellendi",
+    "tire_measurement_create": "{ref} lastik setinde {count, plural, one {# ölçüm} other {# ölçüm}} kaydedildi",
+    "tire_set_add_to_work_order": "{ref} lastik seti {jobRef} iş emrine eklendi",
+    "tire_set_attach": "{ref} lastik setine {count, plural, one {# dosya} other {# dosya}} eklendi",
+    "tire_set_check_in": "{ref} lastik seti {code} rafına alındı",
+    "tire_set_check_out": "{ref} lastik seti teslim edildi",
+    "tire_set_create_quote": "Bir lastik setinden {ref} teklifi oluşturuldu",
+    "tire_set_create_work_order": "Bir lastik setinden {ref} iş emri oluşturuldu",
+    "tire_set_delete": "{ref} lastik seti silindi",
+    "tire_set_detach": "{fileName} bir lastik setinden kaldırıldı",
+    "tire_set_dispose": "{ref} lastik seti hurdaya ayrıldı",
+    "tire_set_message_customer": "{ref} lastik seti hakkında müşteriye {channel, select, email {e-posta} sms {SMS} telegram {Telegram} in_app {uygulama içi bildirim} other {{channel}}} ile mesaj gönderildi",
+    "tire_set_relocate": "{ref} lastik seti {from} rafından {to} rafına taşındı",
+    "tire_set_relocateUnassigned": "{ref} lastik seti {to} rafına yerleştirildi",
+    "tire_set_return": "{ref} lastik seti yeniden depolandı, {code} rafında",
+    "tire_set_unlink_work_order": "{ref} lastik seti {jobRef} iş emrinden ayrıldı",
+    "tire_set_update": "{ref} lastik seti güncellendi",
+    "tire_treatment_complete_all": "{ref} lastik setinde {count, plural, one {# hazırlık işi} other {# hazırlık işi}} tamamlandı",
+    "tire_treatment_mark": "{ref} lastik setinde {type, select, wash_tires {lastik yıkama} wash_rims {jant yıkama} balance {balans} tpms_service {TPMS servisi} new_valves {yeni sibop} repair {tamir} other {{type}}} işi {status, select, done {tamamlandı} pending {bekliyor} skipped {atlandı} other {{status}}} olarak işaretlendi",
+    "tire_treatment_update": "{ref} lastik setindeki hazırlık işi güncellendi",
+    "tire_warehouse_archive": "{name} lastik deposu arşivlendi",
+    "tire_warehouse_create": "{name} lastik deposu oluşturuldu",
+    "tire_warehouse_delete": "{name} lastik deposu silindi",
+    "tire_warehouse_update": "{name} lastik deposu güncellendi",
+    "vehicle_archive": "{id} aracı arşivlendi",
+    "vehicle_create": "{year} {make} {model} aracı oluşturuldu",
+    "vehicle_delete": "{name} aracı silindi",
+    "vehicle_unarchive": "{id} aracı arşivden çıkarıldı",
+    "vehicle_update": "{name} aracı güncellendi, değişenler: {fields}",
+    "vehicle_updateNoChanges": "{name} aracı güncellendi, hiçbir şey değişmedi",
+    "webhook_create": "{name} webhook'u oluşturuldu"
   }
   }
 }
 }

+ 6 - 28
src/__tests__/features/tire-hotel/audit-entries.test.ts

@@ -8,16 +8,14 @@
  * unfindable by the one filter meant to find it. Every other feature sets it,
  * unfindable by the one filter meant to find it. Every other feature sets it,
  * so the omission looks deliberate rather than forgotten.
  * so the omission looks deliberate rather than forgotten.
  *
  *
- * And the message is composed once and stored, so it can never be corrected
- * at read time the way a translated string can. A database enum or a `(s)`
- * that reaches it is there for good.
+ * And a `(s)` in a message means someone has gone back to composing English
+ * by hand instead of naming a key. The sentences live in the message
+ * catalogue now, one per language, checked by audit-summary-keys.test.ts.
  */
  */
 
 
 import { describe, it, expect } from 'vitest'
 import { describe, it, expect } from 'vitest'
 import { readFileSync, readdirSync } from 'node:fs'
 import { readFileSync, readdirSync } from 'node:fs'
 import path from 'node:path'
 import path from 'node:path'
-import { plural, humanise } from '@/features/tire-hotel/Lib/auditText'
-import { TREATMENT_TYPES } from '@/features/tire-hotel/Lib/treatments'
 
 
 const ACTIONS_DIR = 'src/features/tire-hotel/Actions'
 const ACTIONS_DIR = 'src/features/tire-hotel/Actions'
 
 
@@ -29,7 +27,9 @@ function auditBlocks(): { file: string; action: string; body: string }[] {
     const file = path.join(ACTIONS_DIR, name)
     const file = path.join(ACTIONS_DIR, name)
     const source = readFileSync(file, 'utf-8')
     const source = readFileSync(file, 'utf-8')
     for (const match of source.matchAll(
     for (const match of source.matchAll(
-      /audit:\s*(?:\([^)]*\)\s*=>\s*)?\(?\{((?:[^{}]|\{[^{}]*\})*)\}/g
+      // Three levels deep: the block, its details object, and that
+      // object's params.
+      /audit:\s*(?:\([^)]*\)\s*=>\s*)?\(?\{((?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*)\}/g
     )) {
     )) {
       const body = match[1]
       const body = match[1]
       const action = body.match(/action:\s*(.+)/)?.[1]?.trim()
       const action = body.match(/action:\s*(.+)/)?.[1]?.trim()
@@ -62,25 +62,3 @@ describe('every tire hotel audit row', () => {
     expect(sloppy, `these store "(s)":\n${sloppy.join('\n')}`).toEqual([])
     expect(sloppy, `these store "(s)":\n${sloppy.join('\n')}`).toEqual([])
   })
   })
 })
 })
-
-describe('the wording helpers', () => {
-  it('counts one thing and several things differently', () => {
-    expect(plural(1, 'file')).toBe('1 file')
-    expect(plural(3, 'file')).toBe('3 files')
-    expect(plural(0, 'file')).toBe('0 files')
-  })
-
-  it('takes an irregular plural when the noun needs one', () => {
-    expect(plural(2, 'entry', 'entries')).toBe('2 entries')
-  })
-
-  it('turns every treatment type into words', () => {
-    // The reported symptom: "Marked wash_tires as done", a column value
-    // sitting in the middle of an English sentence.
-    for (const type of TREATMENT_TYPES) {
-      expect(humanise(type)).not.toContain('_')
-    }
-    expect(humanise('wash_tires')).toBe('wash tires')
-    expect(humanise('tpms_service')).toBe('tpms service')
-  })
-})

+ 137 - 0
src/__tests__/i18n/audit-summaries.test.ts

@@ -0,0 +1,137 @@
+/**
+ * Every audit sentence must have a key, and every key must be reachable.
+ *
+ * An audit row is written once and read years later, so a wrong key is not
+ * something the next deploy can fix: the row keeps whatever it was given. The
+ * fallback keeps such a row readable in English, which also means a missing
+ * key is invisible to anyone testing in English. Only a sweep finds it.
+ */
+
+import { describe, it, expect } from 'vitest'
+import { IntlMessageFormat } from 'intl-messageformat'
+import { readFileSync, readdirSync } from 'node:fs'
+import path from 'node:path'
+
+const LOCALES = ['en', 'nb', 'de', 'es', 'fr', 'it', 'lt', 'nl', 'pl', 'pt-BR', 'ru', 'tr']
+
+function summary(locale: string): Record<string, string> {
+  return JSON.parse(readFileSync(`messages/${locale}/audit.json`, 'utf-8')).summary ?? {}
+}
+
+/** Every `key:` inside a `details:` block across the app. */
+function keysInUse(): { file: string; key: string }[] {
+  const found: { file: string; key: string }[] = []
+  const walk = (dir: string) => {
+    for (const entry of readdirSync(dir, { withFileTypes: true })) {
+      const full = path.join(dir, entry.name)
+      if (entry.isDirectory()) {
+        if (entry.name === '__tests__') continue
+        walk(full)
+        continue
+      }
+      if (!entry.name.endsWith('.ts') && !entry.name.endsWith('.tsx')) continue
+      const source = readFileSync(full, 'utf-8')
+      // details: { key: 'x' ... }  and  auditDetails('x', ...)
+      for (const m of source.matchAll(/details:\s*\{\s*key:\s*['"]([a-zA-Z_]+)['"]/g)) {
+        found.push({ file: full, key: m[1] })
+      }
+      for (const m of source.matchAll(/auditDetails\(\s*\n?\s*['"]([a-zA-Z_]+)['"]/g)) {
+        found.push({ file: full, key: m[1] })
+      }
+      // The conditional form: auditDetails(cond ? 'a' : 'b', ...)
+      for (const m of source.matchAll(
+        /auditDetails\([^)]*?\?\s*['"]([a-zA-Z_]+)['"]\s*:\s*['"]([a-zA-Z_]+)['"]/g
+      )) {
+        found.push({ file: full, key: m[1] }, { file: full, key: m[2] })
+      }
+    }
+  }
+  walk('src')
+  return found
+}
+
+describe('audit summaries', () => {
+  it('finds the call sites at all', () => {
+    // Without this the sweeps below pass by inspecting nothing.
+    expect(keysInUse().length).toBeGreaterThan(100)
+  })
+
+  it('has English wording for every key the app uses', () => {
+    const en = summary('en')
+    const orphans = [...new Set(keysInUse().filter((k) => !en[k.key]).map((k) => k.key))]
+    expect(orphans, `no English for:\n${orphans.join('\n')}`).toEqual([])
+  })
+
+  it('uses every key it defines', () => {
+    // A key nothing writes is either a typo at the call site or wording left
+    // behind by a rename. Both are worth knowing about.
+    const used = new Set(keysInUse().map((k) => k.key))
+    const unused = Object.keys(summary('en')).filter((k) => !used.has(k))
+    expect(unused, `defined but never written:\n${unused.join('\n')}`).toEqual([])
+  })
+
+  it('says the same things in every language', () => {
+    const en = Object.keys(summary('en')).sort()
+    for (const locale of LOCALES.slice(1)) {
+      const theirs = summary(locale)
+      const missing = en.filter((k) => !theirs[k])
+      expect(missing, `${locale} is missing:\n${missing.join('\n')}`).toEqual([])
+      const extra = Object.keys(theirs).filter((k) => !en.includes(k))
+      expect(extra, `${locale} has keys English does not:\n${extra.join('\n')}`).toEqual([])
+    }
+  })
+
+  it('renders in every language without an ICU error', () => {
+    // 117 sentences times 12 languages, several with nested plural and select
+    // blocks. One unbalanced brace throws at render time, on the audit page,
+    // in that language only, which is exactly the sort of thing that reaches
+    // production because nobody tests in Lithuanian.
+    const sample: Record<string, string | number> = {
+      name: 'X', id: 'X', ref: 'X', code: 'X', from: 'A', to: 'B', jobRef: 'J',
+      title: 'T', description: 'D', fileName: 'f.jpg', recipient: 'a@b.c',
+      email: 'a@b.c', phone: '+1', amount: '10', rate: 8, items: 'a, b',
+      channels: 'email', vehicleId: 'V', serviceRecordId: 'S', sendAt: 'now',
+      fields: 'colour', year: 2024, make: 'M', model: 'M',
+      status: 'draft', role: 'admin', channel: 'email', type: 'wash_tires',
+      count: 2, records: 2, quotes: 2,
+    }
+    for (const locale of LOCALES) {
+      const messages = summary(locale)
+      for (const [key, text] of Object.entries(messages)) {
+        let out = ''
+        expect(() => {
+          out = new IntlMessageFormat(text, locale).format(sample) as string
+        }, `${locale} ${key}: ${text}`).not.toThrow()
+        expect(out, `${locale} ${key} rendered empty`).toBeTruthy()
+        expect(out, `${locale} ${key} left a brace behind`).not.toMatch(/[{}]/)
+      }
+    }
+  })
+
+  it('keeps the same placeholders in every language', () => {
+    // A dropped {ref} loses the one detail that identifies the record, and
+    // next-intl throws on a placeholder with no value rather than skipping it.
+    const names = (t: string) => {
+      const out: string[] = []
+      let depth = 0
+      for (let i = 0; i < t.length; i += 1) {
+        if (t[i] === '{') {
+          if (depth === 0) {
+            const m = /^\{(\w+)/.exec(t.slice(i))
+            if (m) out.push(m[1])
+          }
+          depth += 1
+        } else if (t[i] === '}') depth -= 1
+      }
+      return [...new Set(out)].sort()
+    }
+    const en = summary('en')
+    for (const locale of LOCALES.slice(1)) {
+      const theirs = summary(locale)
+      for (const [key, text] of Object.entries(en)) {
+        if (!theirs[key]) continue
+        expect(names(theirs[key]), `${locale} ${key} placeholders`).toEqual(names(text))
+      }
+    }
+  })
+})

+ 24 - 3
src/app/(authenticated)/audit-log/audit-log-client.tsx

@@ -103,6 +103,25 @@ export function AuditLogClient({
     return t.has(key) ? t(key) : action;
     return t.has(key) ? t(key) : action;
   };
   };
 
 
+  /**
+   * The sentence, in the reader's language where we can manage it.
+   *
+   * Rows written before this existed, and any written by a build that does not
+   * know the key, still have the English the event was composed with. That is
+   * the fallback rather than a blank cell: a log entry nobody can read is
+   * worse than one in the wrong language.
+   */
+  const getSummary = (log: AuditLogData["logs"][number]) => {
+    const details = log.metadata?.details as
+      | { key?: string; params?: Record<string, string | number> }
+      | undefined;
+    if (details?.key) {
+      const key = `summary.${details.key}`;
+      if (t.has(key)) return t(key, details.params);
+    }
+    return log.message;
+  };
+
   const navigate = useCallback(
   const navigate = useCallback(
     (params: Record<string, string | number | undefined>) => {
     (params: Record<string, string | number | undefined>) => {
       const newParams = new URLSearchParams(searchParams.toString());
       const newParams = new URLSearchParams(searchParams.toString());
@@ -210,7 +229,7 @@ export function AuditLogClient({
                   {formatDateTime(log.timestamp)}
                   {formatDateTime(log.timestamp)}
                 </span>
                 </span>
               </div>
               </div>
-              {log.message && <p className="mt-1.5 text-sm">{log.message}</p>}
+              {getSummary(log) && <p className="mt-1.5 text-sm">{getSummary(log)}</p>}
               <p className="mt-1 truncate text-xs text-muted-foreground">
               <p className="mt-1 truncate text-xs text-muted-foreground">
                 {log.user?.name || log.user?.email || t("unknownUser")}
                 {log.user?.name || log.user?.email || t("unknownUser")}
                 {log.entityId && (
                 {log.entityId && (
@@ -259,7 +278,7 @@ export function AuditLogClient({
                     </Badge>
                     </Badge>
                   </TableCell>
                   </TableCell>
                   <TableCell className="hidden md:table-cell text-sm text-muted-foreground truncate max-w-[300px]">
                   <TableCell className="hidden md:table-cell text-sm text-muted-foreground truncate max-w-[300px]">
-                    {log.message}
+                    {getSummary(log)}
                   </TableCell>
                   </TableCell>
                   <TableCell className="hidden lg:table-cell font-mono text-xs text-muted-foreground">
                   <TableCell className="hidden lg:table-cell font-mono text-xs text-muted-foreground">
                     {log.entityId?.substring(0, 8)}
                     {log.entityId?.substring(0, 8)}
@@ -302,7 +321,9 @@ export function AuditLogClient({
               <DetailRow label={t("timestamp")} value={formatDateTime(selectedLog.timestamp)} suppressHydrationWarning />
               <DetailRow label={t("timestamp")} value={formatDateTime(selectedLog.timestamp)} suppressHydrationWarning />
               <DetailRow label={t("user")} value={selectedLog.user?.name || selectedLog.user?.email || t("unknownUser")} />
               <DetailRow label={t("user")} value={selectedLog.user?.name || selectedLog.user?.email || t("unknownUser")} />
               <DetailRow label={t("action")} value={getActionLabel(selectedLog.action)} />
               <DetailRow label={t("action")} value={getActionLabel(selectedLog.action)} />
-              {selectedLog.message && <DetailRow label={t("details")} value={selectedLog.message} />}
+              {getSummary(selectedLog) && (
+                <DetailRow label={t("details")} value={getSummary(selectedLog) as string} />
+              )}
               {selectedLog.entity && <DetailRow label={t("entity")} value={selectedLog.entity} />}
               {selectedLog.entity && <DetailRow label={t("entity")} value={selectedLog.entity} />}
               {selectedLog.entityId && <DetailRow label={t("entityId")} value={selectedLog.entityId} mono />}
               {selectedLog.entityId && <DetailRow label={t("entityId")} value={selectedLog.entityId} mono />}
               {selectedLog.ip && <DetailRow label={t("ipAddress")} value={selectedLog.ip} mono />}
               {selectedLog.ip && <DetailRow label={t("ipAddress")} value={selectedLog.ip} mono />}

+ 3 - 3
src/features/billing/Actions/recurringInvoiceActions.ts

@@ -98,7 +98,7 @@ export async function createRecurringInvoice(input: CreateRecurringInvoiceInput)
       action: "recurringInvoice.create",
       action: "recurringInvoice.create",
       entity: "RecurringInvoice",
       entity: "RecurringInvoice",
       entityId: result.id,
       entityId: result.id,
-      message: `Created recurring invoice "${result.title}"`,
+      details: { key: "recurringInvoice_create", params: { title: result.title } },
       metadata: { recurringInvoiceId: result.id },
       metadata: { recurringInvoiceId: result.id },
     }),
     }),
   });
   });
@@ -175,7 +175,7 @@ export async function updateRecurringInvoice(input: UpdateRecurringInvoiceInput)
       action: "recurringInvoice.update",
       action: "recurringInvoice.update",
       entity: "RecurringInvoice",
       entity: "RecurringInvoice",
       entityId: result.id,
       entityId: result.id,
-      message: `Updated recurring invoice "${result.title}"`,
+      details: { key: "recurringInvoice_update", params: { title: result.title } },
       metadata: { recurringInvoiceId: result.id },
       metadata: { recurringInvoiceId: result.id },
     }),
     }),
   });
   });
@@ -198,7 +198,7 @@ export async function deleteRecurringInvoice(id: string) {
       action: "recurringInvoice.delete",
       action: "recurringInvoice.delete",
       entity: "RecurringInvoice",
       entity: "RecurringInvoice",
       entityId: result.recurringInvoiceId,
       entityId: result.recurringInvoiceId,
-      message: `Deleted recurring invoice ${result.recurringInvoiceId}`,
+      details: { key: "recurringInvoice_delete", params: { id: result.recurringInvoiceId } },
       metadata: { recurringInvoiceId: result.recurringInvoiceId },
       metadata: { recurringInvoiceId: result.recurringInvoiceId },
     }),
     }),
   });
   });

+ 3 - 3
src/features/custom-fields/Actions/customFieldActions.ts

@@ -43,7 +43,7 @@ export async function createFieldDefinition(input: unknown) {
       action: "customField.create",
       action: "customField.create",
       entity: "CustomFieldDefinition",
       entity: "CustomFieldDefinition",
       entityId: result.id,
       entityId: result.id,
-      message: `Created custom field "${result.name}"`,
+      details: { key: "customField_create", params: { name: result.name } },
       metadata: { fieldId: result.id, fieldName: result.name, entityType: result.entityType },
       metadata: { fieldId: result.id, fieldName: result.name, entityType: result.entityType },
     }),
     }),
   });
   });
@@ -72,7 +72,7 @@ export async function updateFieldDefinition(input: unknown) {
       action: "customField.update",
       action: "customField.update",
       entity: "CustomFieldDefinition",
       entity: "CustomFieldDefinition",
       entityId: result.id,
       entityId: result.id,
-      message: `Updated custom field "${result.name}"`,
+      details: { key: "customField_update", params: { name: result.name } },
       metadata: { fieldId: result.id, fieldName: result.name },
       metadata: { fieldId: result.id, fieldName: result.name },
     }),
     }),
   });
   });
@@ -98,7 +98,7 @@ export async function deleteFieldDefinition(fieldId: string) {
       action: "customField.delete",
       action: "customField.delete",
       entity: "CustomFieldDefinition",
       entity: "CustomFieldDefinition",
       entityId: result.fieldId,
       entityId: result.fieldId,
-      message: `Deleted custom field "${result.fieldName}"`,
+      details: { key: "customField_delete", params: { name: result.fieldName } },
       metadata: { fieldId: result.fieldId, fieldName: result.fieldName },
       metadata: { fieldId: result.fieldId, fieldName: result.fieldName },
     }),
     }),
   });
   });

+ 4 - 4
src/features/customers/Actions/customerActions.ts

@@ -98,7 +98,7 @@ export async function createCustomer(input: unknown) {
       action: "customer.create",
       action: "customer.create",
       entity: "Customer",
       entity: "Customer",
       entityId: result.id,
       entityId: result.id,
-      message: `Created customer ${result.name}`,
+      details: { key: "customer_create", params: { name: result.name } },
       metadata: { customerId: result.id },
       metadata: { customerId: result.id },
     }),
     }),
   });
   });
@@ -139,7 +139,7 @@ export async function updateCustomer(input: unknown) {
       action: "customer.update",
       action: "customer.update",
       entity: "Customer",
       entity: "Customer",
       entityId: result.id,
       entityId: result.id,
-      message: `Updated customer ${result.id}`,
+      details: { key: "customer_update", params: { id: result.id } },
       metadata: { customerId: result.id },
       metadata: { customerId: result.id },
     }),
     }),
   });
   });
@@ -157,7 +157,7 @@ export async function deleteCustomer(customerId: string) {
       action: "customer.delete",
       action: "customer.delete",
       entity: "Customer",
       entity: "Customer",
       entityId: result.customerId,
       entityId: result.customerId,
-      message: `Deleted customer ${result.customerId}`,
+      details: { key: "customer_delete", params: { id: result.customerId } },
       metadata: { customerId: result.customerId },
       metadata: { customerId: result.customerId },
     }),
     }),
   });
   });
@@ -215,7 +215,7 @@ export async function backfillCustomerNumbers() {
     audit: ({ result }) => ({
     audit: ({ result }) => ({
       action: "customer.backfillNumbers",
       action: "customer.backfillNumbers",
       entity: "Customer",
       entity: "Customer",
-      message: `Assigned customer numbers to ${result.assigned} customers`,
+      details: { key: "customer_backfillNumbers", params: { count: result.assigned } },
       metadata: { assigned: result.assigned },
       metadata: { assigned: result.assigned },
     }),
     }),
   });
   });

+ 3 - 3
src/features/email/Actions/emailActions.ts

@@ -169,7 +169,7 @@ export async function sendQuoteEmail(input: {
       action: "email.sendQuote",
       action: "email.sendQuote",
       entity: "Quote",
       entity: "Quote",
       entityId: result.quoteId,
       entityId: result.quoteId,
-      message: `Sent quote email to ${result.recipientEmail}`,
+      details: { key: "email_sendQuote", params: { recipient: result.recipientEmail } },
       metadata: { quoteId: result.quoteId, recipientEmail: result.recipientEmail },
       metadata: { quoteId: result.quoteId, recipientEmail: result.recipientEmail },
     }),
     }),
   });
   });
@@ -309,7 +309,7 @@ export async function sendInvoiceEmail(input: {
       action: "email.sendInvoice",
       action: "email.sendInvoice",
       entity: "ServiceRecord",
       entity: "ServiceRecord",
       entityId: result.serviceRecordId,
       entityId: result.serviceRecordId,
-      message: `Sent invoice email to ${result.recipientEmail}`,
+      details: { key: "email_sendInvoice", params: { recipient: result.recipientEmail } },
       metadata: { serviceRecordId: result.serviceRecordId, recipientEmail: result.recipientEmail },
       metadata: { serviceRecordId: result.serviceRecordId, recipientEmail: result.recipientEmail },
     }),
     }),
   });
   });
@@ -427,7 +427,7 @@ export async function sendInspectionEmail(input: {
       action: "email.sendInspection",
       action: "email.sendInspection",
       entity: "Inspection",
       entity: "Inspection",
       entityId: result.inspectionId,
       entityId: result.inspectionId,
-      message: `Sent inspection email to ${result.recipientEmail}`,
+      details: { key: "email_sendInspection", params: { recipient: result.recipientEmail } },
       metadata: { inspectionId: result.inspectionId, recipientEmail: result.recipientEmail },
       metadata: { inspectionId: result.inspectionId, recipientEmail: result.recipientEmail },
     }),
     }),
   });
   });

+ 5 - 5
src/features/inspections/Actions/inspectionActions.ts

@@ -233,7 +233,7 @@ export async function createInspection(input: unknown) {
       action: "inspection.create",
       action: "inspection.create",
       entity: "Inspection",
       entity: "Inspection",
       entityId: result.id,
       entityId: result.id,
-      message: `Created inspection ${result.id}`,
+      details: { key: "inspection_create", params: { id: result.id } },
       metadata: { inspectionId: result.id, vehicleId: result.vehicleId },
       metadata: { inspectionId: result.id, vehicleId: result.vehicleId },
     }),
     }),
   });
   });
@@ -388,7 +388,7 @@ export async function completeInspection(id: string) {
       action: "inspection.complete",
       action: "inspection.complete",
       entity: "Inspection",
       entity: "Inspection",
       entityId: result.inspectionId,
       entityId: result.inspectionId,
-      message: `Completed inspection ${result.inspectionId}`,
+      details: { key: "inspection_complete", params: { id: result.inspectionId } },
       metadata: { inspectionId: result.inspectionId },
       metadata: { inspectionId: result.inspectionId },
     }),
     }),
   });
   });
@@ -435,7 +435,7 @@ export async function reopenInspection(id: string) {
       action: "inspection.reopen",
       action: "inspection.reopen",
       entity: "Inspection",
       entity: "Inspection",
       entityId: result.inspectionId,
       entityId: result.inspectionId,
-      message: `Reopened inspection ${result.inspectionId}`,
+      details: { key: "inspection_reopen", params: { id: result.inspectionId } },
       metadata: { inspectionId: result.inspectionId },
       metadata: { inspectionId: result.inspectionId },
     }),
     }),
   });
   });
@@ -458,7 +458,7 @@ export async function deleteInspection(id: string) {
       action: "inspection.delete",
       action: "inspection.delete",
       entity: "Inspection",
       entity: "Inspection",
       entityId: result.inspectionId,
       entityId: result.inspectionId,
-      message: `Deleted inspection ${result.inspectionId}`,
+      details: { key: "inspection_delete", params: { id: result.inspectionId } },
       metadata: { inspectionId: result.inspectionId },
       metadata: { inspectionId: result.inspectionId },
     }),
     }),
   });
   });
@@ -677,7 +677,7 @@ export async function createWorkOrderFromInspection(id: string) {
       action: "service.create",
       action: "service.create",
       entity: "ServiceRecord",
       entity: "ServiceRecord",
       entityId: result.id,
       entityId: result.id,
-      message: `Created work order ${result.id} from an inspection`,
+      details: { key: "service_createFromInspection", params: { ref: result.id } },
       metadata: { serviceRecordId: result.id, vehicleId: result.vehicleId },
       metadata: { serviceRecordId: result.id, vehicleId: result.vehicleId },
     }),
     }),
   });
   });

+ 1 - 1
src/features/inspections/Actions/packageActions.ts

@@ -190,7 +190,7 @@ export async function importTemplatePackage(raw: unknown) {
       action: "inspectionTemplate.import",
       action: "inspectionTemplate.import",
       entity: "InspectionTemplate",
       entity: "InspectionTemplate",
       entityId: result.templates[0]?.id,
       entityId: result.templates[0]?.id,
-      message: `Imported ${result.templates.length} inspection template(s) from a package`,
+      details: { key: "inspectionTemplate_import", params: { count: result.templates.length } },
       metadata: { count: result.templates.length },
       metadata: { count: result.templates.length },
     }),
     }),
   });
   });

+ 1 - 1
src/features/inspections/Actions/quoteRequestActions.ts

@@ -141,7 +141,7 @@ export async function updateQuoteRequestStatus(id: string, status: "quoted" | "d
       action: "quoteRequest.update",
       action: "quoteRequest.update",
       entity: "InspectionQuoteRequest",
       entity: "InspectionQuoteRequest",
       entityId: result.requestId,
       entityId: result.requestId,
-      message: `Updated quote request status to ${result.status}`,
+      details: { key: "quoteRequest_update", params: { status: result.status } },
       metadata: { requestId: result.requestId, status: result.status },
       metadata: { requestId: result.requestId, status: result.status },
     }),
     }),
   });
   });

+ 5 - 5
src/features/inspections/Actions/templateActions.ts

@@ -119,7 +119,7 @@ export async function createTemplate(input: unknown) {
       action: "inspectionTemplate.create",
       action: "inspectionTemplate.create",
       entity: "InspectionTemplate",
       entity: "InspectionTemplate",
       entityId: result.id,
       entityId: result.id,
-      message: `Created inspection template "${result.name}"`,
+      details: { key: "inspectionTemplate_create", params: { name: result.name } },
       metadata: { templateId: result.id, templateName: result.name },
       metadata: { templateId: result.id, templateName: result.name },
     }),
     }),
   });
   });
@@ -175,7 +175,7 @@ export async function updateTemplate(input: unknown) {
       action: "inspectionTemplate.update",
       action: "inspectionTemplate.update",
       entity: "InspectionTemplate",
       entity: "InspectionTemplate",
       entityId: result.id,
       entityId: result.id,
-      message: `Updated inspection template "${result.name}"`,
+      details: { key: "inspectionTemplate_update", params: { name: result.name } },
       metadata: { templateId: result.id, templateName: result.name },
       metadata: { templateId: result.id, templateName: result.name },
     }),
     }),
   });
   });
@@ -205,7 +205,7 @@ export async function deleteTemplate(id: string) {
       action: "inspectionTemplate.delete",
       action: "inspectionTemplate.delete",
       entity: "InspectionTemplate",
       entity: "InspectionTemplate",
       entityId: result.templateId,
       entityId: result.templateId,
-      message: `Deleted inspection template "${result.templateName}"`,
+      details: { key: "inspectionTemplate_delete", params: { name: result.templateName } },
       metadata: { templateId: result.templateId, templateName: result.templateName },
       metadata: { templateId: result.templateId, templateName: result.templateName },
     }),
     }),
   });
   });
@@ -238,7 +238,7 @@ export async function createTemplateFromPreset(presetId: string) {
       action: "inspectionTemplate.create",
       action: "inspectionTemplate.create",
       entity: "InspectionTemplate",
       entity: "InspectionTemplate",
       entityId: result.id,
       entityId: result.id,
-      message: `Created inspection template "${result.name}" from a preset`,
+      details: { key: "inspectionTemplate_createFromPreset", params: { name: result.name } },
       metadata: { templateId: result.id, templateName: result.name },
       metadata: { templateId: result.id, templateName: result.name },
     }),
     }),
   });
   });
@@ -307,7 +307,7 @@ export async function duplicateTemplate(id: string) {
       action: "inspectionTemplate.create",
       action: "inspectionTemplate.create",
       entity: "InspectionTemplate",
       entity: "InspectionTemplate",
       entityId: result.id,
       entityId: result.id,
-      message: `Duplicated inspection template into "${result.name}"`,
+      details: { key: "inspectionTemplate_duplicate", params: { name: result.name } },
       metadata: { templateId: result.id, templateName: result.name },
       metadata: { templateId: result.id, templateName: result.name },
     }),
     }),
   });
   });

+ 4 - 4
src/features/inventory/Actions/inventoryActions.ts

@@ -154,7 +154,7 @@ export async function createInventoryPart(input: unknown) {
       action: "inventory.create",
       action: "inventory.create",
       entity: "InventoryPart",
       entity: "InventoryPart",
       entityId: result.id,
       entityId: result.id,
-      message: `Created inventory part "${result.name}"`,
+      details: { key: "inventory_create", params: { name: result.name } },
       metadata: { partId: result.id },
       metadata: { partId: result.id },
     }),
     }),
   });
   });
@@ -223,7 +223,7 @@ export async function updateInventoryPart(input: unknown) {
       action: "inventory.update",
       action: "inventory.update",
       entity: "InventoryPart",
       entity: "InventoryPart",
       entityId: result.partId,
       entityId: result.partId,
-      message: `Updated inventory part ${result.partId}`,
+      details: { key: "inventory_update", params: { id: result.partId } },
       metadata: { partId: result.partId },
       metadata: { partId: result.partId },
     }),
     }),
   });
   });
@@ -261,7 +261,7 @@ export async function deleteInventoryPart(partId: string) {
       action: "inventory.delete",
       action: "inventory.delete",
       entity: "InventoryPart",
       entity: "InventoryPart",
       entityId: result.partId,
       entityId: result.partId,
-      message: `Deleted inventory part ${result.partId}`,
+      details: { key: "inventory_delete", params: { id: result.partId } },
       metadata: { partId: result.partId },
       metadata: { partId: result.partId },
     }),
     }),
   });
   });
@@ -299,7 +299,7 @@ export async function deleteInventoryParts(partIds: string[]) {
       action: "inventory.bulkDelete",
       action: "inventory.bulkDelete",
       entity: "InventoryPart",
       entity: "InventoryPart",
       entityId: partIds.join(","),
       entityId: partIds.join(","),
-      message: `Bulk deleted ${result.deleted} inventory parts`,
+      details: { key: "inventory_bulkDelete", params: { count: result.deleted } },
       metadata: { partIds, deleted: result.deleted },
       metadata: { partIds, deleted: result.deleted },
     }),
     }),
   });
   });

+ 3 - 3
src/features/labor-presets/Actions/laborPresetActions.ts

@@ -120,7 +120,7 @@ export async function createLaborPreset(input: unknown) {
       action: "labor_preset.create",
       action: "labor_preset.create",
       entity: "LaborPreset",
       entity: "LaborPreset",
       entityId: result.id,
       entityId: result.id,
-      message: `Created labor preset "${result.name}"`,
+      details: { key: "labor_preset_create", params: { name: result.name } },
       metadata: { presetId: result.id },
       metadata: { presetId: result.id },
     }),
     }),
   });
   });
@@ -183,7 +183,7 @@ export async function updateLaborPreset(input: unknown) {
       action: "labor_preset.update",
       action: "labor_preset.update",
       entity: "LaborPreset",
       entity: "LaborPreset",
       entityId: result.presetId,
       entityId: result.presetId,
-      message: `Updated labor preset ${result.presetId}`,
+      details: { key: "labor_preset_update", params: { id: result.presetId } },
       metadata: { presetId: result.presetId },
       metadata: { presetId: result.presetId },
     }),
     }),
   });
   });
@@ -203,7 +203,7 @@ export async function deleteLaborPreset(presetId: string) {
       action: "labor_preset.delete",
       action: "labor_preset.delete",
       entity: "LaborPreset",
       entity: "LaborPreset",
       entityId: result.presetId,
       entityId: result.presetId,
-      message: `Deleted labor preset ${result.presetId}`,
+      details: { key: "labor_preset_delete", params: { id: result.presetId } },
       metadata: { presetId: result.presetId },
       metadata: { presetId: result.presetId },
     }),
     }),
   });
   });

+ 1 - 1
src/features/onboarding/Actions/checklistActions.ts

@@ -154,7 +154,7 @@ export async function removeSampleData() {
     audit: () => ({
     audit: () => ({
       action: "organization.sampleDataRemove",
       action: "organization.sampleDataRemove",
       entity: "Organization",
       entity: "Organization",
-      message: "Removed onboarding sample data",
+      details: { key: "organization_sampleDataRemove" },
     }),
     }),
   });
   });
 }
 }

+ 2 - 2
src/features/payments/Actions/paymentActions.ts

@@ -40,7 +40,7 @@ export async function createPayment(input: unknown) {
       action: "payment.create",
       action: "payment.create",
       entity: "Payment",
       entity: "Payment",
       entityId: result.id,
       entityId: result.id,
-      message: `Recorded payment ${result.amount} for service ${result.serviceRecordId}`,
+      details: { key: "payment_create", params: { amount: result.amount, serviceRecordId: result.serviceRecordId } },
       metadata: { paymentId: result.id, serviceRecordId: result.serviceRecordId, amount: result.amount, method: result.method },
       metadata: { paymentId: result.id, serviceRecordId: result.serviceRecordId, amount: result.amount, method: result.method },
     }),
     }),
   });
   });
@@ -67,7 +67,7 @@ export async function deletePayment(paymentId: string) {
       action: "payment.delete",
       action: "payment.delete",
       entity: "Payment",
       entity: "Payment",
       entityId: result.paymentId,
       entityId: result.paymentId,
-      message: `Deleted payment ${result.paymentId}`,
+      details: { key: "payment_delete", params: { id: result.paymentId } },
       metadata: { paymentId: result.paymentId },
       metadata: { paymentId: result.paymentId },
     }),
     }),
   });
   });

+ 5 - 5
src/features/quotes/Actions/quoteActions.ts

@@ -280,7 +280,7 @@ export async function createQuote(input: unknown) {
         action: 'quote.create',
         action: 'quote.create',
         entity: 'Quote',
         entity: 'Quote',
         entityId: result.id,
         entityId: result.id,
-        message: `Created quote ${result.quoteNumber || result.id}`,
+        details: { key: 'quote_create', params: { ref: result.quoteNumber || result.id } },
         metadata: { quoteId: result.id },
         metadata: { quoteId: result.id },
       }),
       }),
     }
     }
@@ -339,7 +339,7 @@ export async function updateQuote(input: unknown) {
         action: 'quote.update',
         action: 'quote.update',
         entity: 'Quote',
         entity: 'Quote',
         entityId: result.id,
         entityId: result.id,
-        message: `Updated quote ${result.id}`,
+        details: { key: 'quote_update', params: { ref: result.id } },
         metadata: { quoteId: result.id },
         metadata: { quoteId: result.id },
       }),
       }),
     }
     }
@@ -369,7 +369,7 @@ export async function updateQuoteStatus(quoteId: string, status: string) {
         action: 'quote.status',
         action: 'quote.status',
         entity: 'Quote',
         entity: 'Quote',
         entityId: result.quoteId,
         entityId: result.quoteId,
-        message: `Changed quote status to ${result.status}`,
+        details: { key: 'quote_status', params: { status: result.status } },
         metadata: { quoteId: result.quoteId, status: result.status },
         metadata: { quoteId: result.quoteId, status: result.status },
       }),
       }),
     }
     }
@@ -394,7 +394,7 @@ export async function deleteQuote(quoteId: string) {
         action: 'quote.delete',
         action: 'quote.delete',
         entity: 'Quote',
         entity: 'Quote',
         entityId: result.quoteId,
         entityId: result.quoteId,
-        message: `Deleted quote ${result.quoteId}`,
+        details: { key: 'quote_delete', params: { ref: result.quoteId } },
         metadata: { quoteId: result.quoteId },
         metadata: { quoteId: result.quoteId },
       }),
       }),
     }
     }
@@ -577,7 +577,7 @@ export async function convertQuoteToServiceRecord(quoteId: string, vehicleId: st
         action: 'quote.convert',
         action: 'quote.convert',
         entity: 'Quote',
         entity: 'Quote',
         entityId: result.convertedFromQuoteId,
         entityId: result.convertedFromQuoteId,
-        message: `Converted quote ${result.convertedFromQuoteId} to service record ${result.id}`,
+        details: { key: 'quote_convert', params: { ref: result.convertedFromQuoteId, serviceRecordId: result.id } },
         metadata: { quoteId: result.convertedFromQuoteId, serviceRecordId: result.id },
         metadata: { quoteId: result.convertedFromQuoteId, serviceRecordId: result.id },
       }),
       }),
     }
     }

+ 5 - 5
src/features/scheduled-messages/Actions/scheduledMessageActions.ts

@@ -147,7 +147,7 @@ export async function createScheduledMessage(input: unknown) {
         action: "scheduled_message.create",
         action: "scheduled_message.create",
         entity: "ScheduledMessage",
         entity: "ScheduledMessage",
         entityId: result.id,
         entityId: result.id,
-        message: `Scheduled a ${result.channel} message for ${result.sendAt.toISOString()}`,
+        details: { key: "scheduled_message_create", params: { channel: result.channel, sendAt: result.sendAt.toISOString() } },
         metadata: { channel: result.channel, frequency: result.frequency },
         metadata: { channel: result.channel, frequency: result.frequency },
       }),
       }),
     },
     },
@@ -197,7 +197,7 @@ export async function updateScheduledMessage(input: unknown) {
         action: "scheduled_message.update",
         action: "scheduled_message.update",
         entity: "ScheduledMessage",
         entity: "ScheduledMessage",
         entityId: result.id,
         entityId: result.id,
-        message: `Updated a scheduled ${result.channel} message`,
+        details: { key: "scheduled_message_update", params: { channel: result.channel } },
       }),
       }),
     },
     },
   );
   );
@@ -229,7 +229,7 @@ export async function cancelScheduledMessage(id: string) {
         action: "scheduled_message.cancel",
         action: "scheduled_message.cancel",
         entity: "ScheduledMessage",
         entity: "ScheduledMessage",
         entityId: result.id,
         entityId: result.id,
-        message: "Cancelled a scheduled message",
+        details: { key: "scheduled_message_cancel" },
       }),
       }),
     },
     },
   );
   );
@@ -257,7 +257,7 @@ export async function deleteScheduledMessage(id: string) {
         action: "scheduled_message.delete",
         action: "scheduled_message.delete",
         entity: "ScheduledMessage",
         entity: "ScheduledMessage",
         entityId: result.id,
         entityId: result.id,
-        message: "Deleted a scheduled message",
+        details: { key: "scheduled_message_delete" },
       }),
       }),
     },
     },
   );
   );
@@ -323,7 +323,7 @@ export async function sendScheduledMessageNow(id: string) {
         action: "scheduled_message.send_now",
         action: "scheduled_message.send_now",
         entity: "ScheduledMessage",
         entity: "ScheduledMessage",
         entityId: result.id,
         entityId: result.id,
-        message: `Sent a scheduled ${result.channel} message ahead of time`,
+        details: { key: "scheduled_message_send_now", params: { channel: result.channel } },
       }),
       }),
     },
     },
   );
   );

+ 3 - 3
src/features/settings/Actions/applyTaxRateToExisting.ts

@@ -124,7 +124,7 @@ export async function applyTaxRateToExisting() {
       audit: ({ result }) => ({
       audit: ({ result }) => ({
         action: "settings.applyTaxRate",
         action: "settings.applyTaxRate",
         entity: "Organization",
         entity: "Organization",
-        message: `Applied ${result.taxRate}% tax to ${result.serviceRecordsUpdated} service records and ${result.quotesUpdated} quotes`,
+        details: { key: "settings_applyTaxRate", params: { rate: result.taxRate, records: result.serviceRecordsUpdated, quotes: result.quotesUpdated } },
         metadata: {
         metadata: {
           taxRate: result.taxRate,
           taxRate: result.taxRate,
           serviceRecordsUpdated: result.serviceRecordsUpdated,
           serviceRecordsUpdated: result.serviceRecordsUpdated,
@@ -371,7 +371,7 @@ export async function convertRecordsToInclusive() {
       audit: ({ result }) => ({
       audit: ({ result }) => ({
         action: "settings.convertToInclusive",
         action: "settings.convertToInclusive",
         entity: "Organization",
         entity: "Organization",
-        message: `Converted ${result.serviceRecordsUpdated} service records and ${result.quotesUpdated} quotes to inclusive tax mode`,
+        details: { key: "settings_convertToInclusive", params: { records: result.serviceRecordsUpdated, quotes: result.quotesUpdated } },
         metadata: result,
         metadata: result,
       }),
       }),
     },
     },
@@ -538,7 +538,7 @@ export async function convertRecordsToExclusive() {
       audit: ({ result }) => ({
       audit: ({ result }) => ({
         action: "settings.convertToExclusive",
         action: "settings.convertToExclusive",
         entity: "Organization",
         entity: "Organization",
-        message: `Converted ${result.serviceRecordsUpdated} service records and ${result.quotesUpdated} quotes to exclusive tax mode`,
+        details: { key: "settings_convertToExclusive", params: { records: result.serviceRecordsUpdated, quotes: result.quotesUpdated } },
         metadata: result,
         metadata: result,
       }),
       }),
     },
     },

+ 1 - 1
src/features/settings/Actions/deleteContent.ts

@@ -173,7 +173,7 @@ export async function deleteContent(input: unknown) {
     audit: ({ result }) => ({
     audit: ({ result }) => ({
       action: "settings.deleteContent",
       action: "settings.deleteContent",
       entity: "Organization",
       entity: "Organization",
-      message: `Deleted all data: ${result.deleted.join(", ")}`,
+      details: { key: "settings_deleteContent", params: { items: result.deleted.join(", ") } },
       metadata: { deleted: result.deleted },
       metadata: { deleted: result.deleted },
     }),
     }),
   });
   });

+ 2 - 2
src/features/settings/Actions/invoiceLayoutActions.ts

@@ -61,7 +61,7 @@ export async function saveInvoiceLayoutConfig(config: InvoiceLayoutConfig) {
     audit: () => ({
     audit: () => ({
       action: "settings.updateInvoiceLayout",
       action: "settings.updateInvoiceLayout",
       entity: "AppSetting",
       entity: "AppSetting",
-      message: "Updated invoice layout configuration",
+      details: { key: "settings_updateInvoiceLayout" },
     }),
     }),
   });
   });
 }
 }
@@ -114,7 +114,7 @@ export async function saveQuoteLayoutConfig(config: InvoiceLayoutConfig) {
     audit: () => ({
     audit: () => ({
       action: "settings.updateQuoteLayout",
       action: "settings.updateQuoteLayout",
       entity: "AppSetting",
       entity: "AppSetting",
-      message: "Updated quote layout configuration",
+      details: { key: "settings_updateQuoteLayout" },
     }),
     }),
   });
   });
 }
 }

+ 1 - 1
src/features/sms/Actions/smsActions.ts

@@ -99,7 +99,7 @@ export async function sendSmsToCustomer(input: {
         action: 'sms.send',
         action: 'sms.send',
         entity: 'SmsMessage',
         entity: 'SmsMessage',
         entityId: result.id,
         entityId: result.id,
-        message: `Sent SMS to ${result.customerName} (${result.customerPhone})`,
+        details: { key: 'sms_send', params: { name: result.customerName, phone: result.customerPhone } },
         metadata: { messageId: result.id },
         metadata: { messageId: result.id },
       }),
       }),
     }
     }

+ 1 - 1
src/features/status-reports/Actions/createStatusReport.ts

@@ -50,7 +50,7 @@ export async function createStatusReport(input: unknown) {
         action: "statusReport.create",
         action: "statusReport.create",
         entity: "StatusReport",
         entity: "StatusReport",
         entityId: result.id,
         entityId: result.id,
-        message: `Created status report for service ${result.serviceRecordId}`,
+        details: { key: "statusReport_create", params: { serviceRecordId: result.serviceRecordId } },
       }),
       }),
     }
     }
   );
   );

+ 1 - 1
src/features/status-reports/Actions/deleteStatusReport.ts

@@ -49,7 +49,7 @@ export async function deleteStatusReport(statusReportId: string) {
         action: "statusReport.delete",
         action: "statusReport.delete",
         entity: "StatusReport",
         entity: "StatusReport",
         entityId: result.statusReportId,
         entityId: result.statusReportId,
-        message: "Deleted status report",
+        details: { key: "statusReport_delete" },
       }),
       }),
     }
     }
   );
   );

+ 1 - 1
src/features/status-reports/Actions/sendStatusReport.ts

@@ -115,7 +115,7 @@ export async function sendStatusReport(input: unknown) {
         action: "statusReport.send",
         action: "statusReport.send",
         entity: "StatusReport",
         entity: "StatusReport",
         entityId: result.statusReportId,
         entityId: result.statusReportId,
-        message: `Sent status report via ${result.channels.join(", ")}`,
+        details: { key: "statusReport_send", params: { channels: result.channels.join(", ") } },
       }),
       }),
     }
     }
   );
   );

+ 2 - 2
src/features/subscription/Actions/subscriptionActions.ts

@@ -32,7 +32,7 @@ export async function cancelSubscription() {
     audit: () => ({
     audit: () => ({
       action: "subscription.cancel",
       action: "subscription.cancel",
       entity: "Subscription",
       entity: "Subscription",
-      message: "Cancelled subscription (end of period)",
+      details: { key: "subscription_cancel" },
     }),
     }),
   });
   });
 }
 }
@@ -64,7 +64,7 @@ export async function resumeSubscription() {
     audit: () => ({
     audit: () => ({
       action: "subscription.resume",
       action: "subscription.resume",
       entity: "Subscription",
       entity: "Subscription",
-      message: "Resumed subscription",
+      details: { key: "subscription_resume" },
     }),
     }),
   });
   });
 }
 }

+ 1 - 1
src/features/team/Actions/cancelInvitation.ts

@@ -35,7 +35,7 @@ export async function cancelInvitation(input: unknown) {
       action: "team.cancelInvitation",
       action: "team.cancelInvitation",
       entity: "TeamInvitation",
       entity: "TeamInvitation",
       entityId: result.invitationId,
       entityId: result.invitationId,
-      message: `Cancelled invitation for ${result.email}`,
+      details: { key: "team_cancelInvitation", params: { email: result.email } },
       metadata: { invitationId: result.invitationId, email: result.email },
       metadata: { invitationId: result.invitationId, email: result.email },
     }),
     }),
   });
   });

+ 1 - 1
src/features/team/Actions/createNewOrganization.ts

@@ -60,7 +60,7 @@ export async function createNewOrganization(input: unknown) {
       action: "organization.create",
       action: "organization.create",
       entity: "Organization",
       entity: "Organization",
       entityId: result.id,
       entityId: result.id,
-      message: `Created organization "${result.name}"`,
+      details: { key: "organization_create", params: { name: result.name } },
       metadata: { organizationId: result.id },
       metadata: { organizationId: result.id },
     }),
     }),
   });
   });

+ 1 - 1
src/features/team/Actions/createRole.ts

@@ -37,7 +37,7 @@ export async function createRole(input: unknown) {
       action: "role.create",
       action: "role.create",
       entity: "Role",
       entity: "Role",
       entityId: result.id,
       entityId: result.id,
-      message: `Created role "${result.name}"`,
+      details: { key: "role_create", params: { name: result.name } },
       metadata: { roleId: result.id, roleName: result.name },
       metadata: { roleId: result.id, roleName: result.name },
     }),
     }),
   });
   });

+ 1 - 1
src/features/team/Actions/deleteRole.ts

@@ -28,7 +28,7 @@ export async function deleteRole(roleId: string) {
       action: "role.delete",
       action: "role.delete",
       entity: "Role",
       entity: "Role",
       entityId: result.roleId,
       entityId: result.roleId,
-      message: `Deleted role "${result.roleName}"`,
+      details: { key: "role_delete", params: { name: result.roleName } },
       metadata: { roleId: result.roleId, roleName: result.roleName },
       metadata: { roleId: result.roleId, roleName: result.roleName },
     }),
     }),
   });
   });

+ 1 - 1
src/features/team/Actions/sendInvitation.ts

@@ -108,7 +108,7 @@ export async function sendInvitation(input: unknown) {
     audit: ({ result }) => ({
     audit: ({ result }) => ({
       action: "team.sendInvitation",
       action: "team.sendInvitation",
       entity: "TeamInvitation",
       entity: "TeamInvitation",
-      message: `Sent invitation to ${result.email} as ${result.role}`,
+      details: { key: "team_sendInvitation", params: { email: result.email, role: result.role } },
       metadata: { email: result.email, role: result.role },
       metadata: { email: result.email, role: result.role },
     }),
     }),
   });
   });

+ 3 - 3
src/features/team/Actions/teamActions.ts

@@ -110,7 +110,7 @@ export async function createOrganization(input: unknown) {
       action: "organization.create",
       action: "organization.create",
       entity: "Organization",
       entity: "Organization",
       entityId: result.id,
       entityId: result.id,
-      message: `Created organization "${result.name}"`,
+      details: { key: "organization_create", params: { name: result.name } },
       metadata: { organizationId: result.id },
       metadata: { organizationId: result.id },
     }),
     }),
   });
   });
@@ -201,7 +201,7 @@ export async function updateMemberRole(input: unknown) {
       action: "team.updateRole",
       action: "team.updateRole",
       entity: "OrganizationMember",
       entity: "OrganizationMember",
       entityId: result.memberId,
       entityId: result.memberId,
-      message: `Changed member role to ${result.role}`,
+      details: { key: "team_updateRole", params: { role: result.role } },
       metadata: { memberId: result.memberId, role: result.role },
       metadata: { memberId: result.memberId, role: result.role },
     }),
     }),
   });
   });
@@ -233,7 +233,7 @@ export async function removeMember(memberId: string) {
       action: "team.removeMember",
       action: "team.removeMember",
       entity: "OrganizationMember",
       entity: "OrganizationMember",
       entityId: result.memberId,
       entityId: result.memberId,
-      message: `Removed team member ${result.memberId}`,
+      details: { key: "team_removeMember", params: { id: result.memberId } },
       metadata: { memberId: result.memberId },
       metadata: { memberId: result.memberId },
     }),
     }),
   });
   });

+ 1 - 1
src/features/team/Actions/updateRole.ts

@@ -49,7 +49,7 @@ export async function updateRole(input: unknown) {
       action: "role.update",
       action: "role.update",
       entity: "Role",
       entity: "Role",
       entityId: result.id,
       entityId: result.id,
-      message: `Updated role "${result.name}"`,
+      details: { key: "role_update", params: { name: result.name } },
       metadata: { roleId: result.id, roleName: result.name },
       metadata: { roleId: result.id, roleName: result.name },
     }),
     }),
   });
   });

+ 1 - 1
src/features/telegram/Actions/telegramActions.ts

@@ -84,7 +84,7 @@ export async function sendTelegramToCustomer(input: {
         action: "telegram.send",
         action: "telegram.send",
         entity: "TelegramMessage",
         entity: "TelegramMessage",
         entityId: result.id,
         entityId: result.id,
-        message: `Sent Telegram message to ${result.customerName}`,
+        details: { key: "telegram_send", params: { name: result.customerName } },
         metadata: { messageId: result.id },
         metadata: { messageId: result.id },
       }),
       }),
     },
     },

+ 2 - 3
src/features/tire-hotel/Actions/attachmentActions.ts

@@ -6,7 +6,6 @@ import { db } from '@/lib/db'
 import { withAuth } from '@/lib/with-auth'
 import { withAuth } from '@/lib/with-auth'
 import { PermissionAction, PermissionSubject } from '@/lib/permissions'
 import { PermissionAction, PermissionSubject } from '@/lib/permissions'
 import { requireTireHotel } from '../Lib/tireHotelSettings'
 import { requireTireHotel } from '../Lib/tireHotelSettings'
-import { plural } from '../Lib/auditText'
 
 
 const READ = [{ action: PermissionAction.READ, subject: PermissionSubject.TIRE_HOTEL }]
 const READ = [{ action: PermissionAction.READ, subject: PermissionSubject.TIRE_HOTEL }]
 const UPDATE = [{ action: PermissionAction.UPDATE, subject: PermissionSubject.TIRE_HOTEL }]
 const UPDATE = [{ action: PermissionAction.UPDATE, subject: PermissionSubject.TIRE_HOTEL }]
@@ -113,7 +112,7 @@ export async function addTireSetAttachments(input: unknown) {
         action: 'tire_set.attach',
         action: 'tire_set.attach',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.tireSetId,
         entityId: result.tireSetId,
-        message: `Added ${plural(result.added, 'file')} to tire set ${result.reference ?? result.tireSetId}`,
+        details: { key: 'tire_set_attach', params: { count: result.added, ref: result.reference ?? result.tireSetId } },
       }),
       }),
     }
     }
   )
   )
@@ -177,7 +176,7 @@ export async function deleteTireSetAttachment(id: string) {
         action: 'tire_set.detach',
         action: 'tire_set.detach',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.tireSetId,
         entityId: result.tireSetId,
-        message: `Removed ${result.fileName} from a tire set`,
+        details: { key: 'tire_set_detach', params: { fileName: result.fileName } },
       }),
       }),
     }
     }
   )
   )

+ 1 - 1
src/features/tire-hotel/Actions/messageCustomerActions.ts

@@ -206,7 +206,7 @@ export async function messageCustomerAboutTireSet(input: unknown) {
         action: 'tire_set.message_customer',
         action: 'tire_set.message_customer',
         entity: 'ScheduledMessage',
         entity: 'ScheduledMessage',
         entityId: result.id,
         entityId: result.id,
-        message: `Messaged the customer about tire set ${result.reference ?? result.tireSetId} by ${result.channel}`,
+        details: { key: 'tire_set_message_customer', params: { ref: result.reference ?? result.tireSetId, channel: result.channel } },
         metadata: { reason: result.reason, channel: result.channel },
         metadata: { reason: result.reason, channel: result.channel },
       }),
       }),
     }
     }

+ 11 - 8
src/features/tire-hotel/Actions/storageActions.ts

@@ -14,7 +14,7 @@ import {
 import { buildLocationCode } from '../Lib/tireConstants'
 import { buildLocationCode } from '../Lib/tireConstants'
 import { locationCapacity, warehouseCapacity } from '../Lib/capacity'
 import { locationCapacity, warehouseCapacity } from '../Lib/capacity'
 import { requireTireHotel, getTireHotelSettings } from '../Lib/tireHotelSettings'
 import { requireTireHotel, getTireHotelSettings } from '../Lib/tireHotelSettings'
-import { plural } from '../Lib/auditText'
+import { auditDetails } from '@/lib/audit'
 
 
 const READ = [{ action: PermissionAction.READ, subject: PermissionSubject.TIRE_HOTEL }]
 const READ = [{ action: PermissionAction.READ, subject: PermissionSubject.TIRE_HOTEL }]
 const MANAGE = [{ action: PermissionAction.MANAGE, subject: PermissionSubject.TIRE_HOTEL }]
 const MANAGE = [{ action: PermissionAction.MANAGE, subject: PermissionSubject.TIRE_HOTEL }]
@@ -147,7 +147,7 @@ export async function createWarehouse(input: unknown) {
         action: 'tire_warehouse.create',
         action: 'tire_warehouse.create',
         entity: 'TireWarehouse',
         entity: 'TireWarehouse',
         entityId: result.id,
         entityId: result.id,
-        message: `Created tire warehouse ${result.name}`,
+        details: { key: 'tire_warehouse_create', params: { name: result.name } },
       }),
       }),
     }
     }
   )
   )
@@ -188,7 +188,7 @@ export async function updateWarehouse(input: unknown) {
         action: 'tire_warehouse.update',
         action: 'tire_warehouse.update',
         entity: 'TireWarehouse',
         entity: 'TireWarehouse',
         entityId: result.id,
         entityId: result.id,
-        message: `Updated tire warehouse ${result.name}`,
+        details: { key: 'tire_warehouse_update', params: { name: result.name } },
       }),
       }),
     }
     }
   )
   )
@@ -232,7 +232,10 @@ export async function deleteWarehouse(id: string) {
         action: result.archived ? 'tire_warehouse.archive' : 'tire_warehouse.delete',
         action: result.archived ? 'tire_warehouse.archive' : 'tire_warehouse.delete',
         entity: 'TireWarehouse',
         entity: 'TireWarehouse',
         entityId: result.id,
         entityId: result.id,
-        message: `${result.archived ? 'Archived' : 'Deleted'} tire warehouse ${result.name}`,
+        details: auditDetails(
+          result.archived ? 'tire_warehouse_archive' : 'tire_warehouse_delete',
+          { name: result.name }
+        ),
       }),
       }),
     }
     }
   )
   )
@@ -288,7 +291,7 @@ export async function createLocation(input: unknown) {
         action: 'tire_location.create',
         action: 'tire_location.create',
         entity: 'TireLocation',
         entity: 'TireLocation',
         entityId: result.id,
         entityId: result.id,
-        message: `Created tire storage location ${result.code}`,
+        details: { key: 'tire_location_create', params: { code: result.code } },
       }),
       }),
     }
     }
   )
   )
@@ -359,7 +362,7 @@ export async function createLocationsBulk(input: unknown) {
       audit: ({ result }) => ({
       audit: ({ result }) => ({
         action: 'tire_location.bulk_create',
         action: 'tire_location.bulk_create',
         entity: 'TireLocation',
         entity: 'TireLocation',
-        message: `Created ${plural(result.created, 'tire storage location')}`,
+        details: { key: 'tire_location_bulk_create', params: { count: result.created } },
         metadata: { skipped: result.skipped },
         metadata: { skipped: result.skipped },
       }),
       }),
     }
     }
@@ -430,7 +433,7 @@ export async function updateLocation(input: unknown) {
         action: 'tire_location.update',
         action: 'tire_location.update',
         entity: 'TireLocation',
         entity: 'TireLocation',
         entityId: result.id,
         entityId: result.id,
-        message: `Updated tire storage location ${result.code}`,
+        details: { key: 'tire_location_update', params: { code: result.code } },
       }),
       }),
     }
     }
   )
   )
@@ -463,7 +466,7 @@ export async function deleteLocation(id: string) {
         action: 'tire_location.delete',
         action: 'tire_location.delete',
         entity: 'TireLocation',
         entity: 'TireLocation',
         entityId: result.id,
         entityId: result.id,
-        message: `Deleted tire storage location ${result.code}`,
+        details: { key: 'tire_location_delete', params: { code: result.code } },
       }),
       }),
     }
     }
   )
   )

+ 4 - 4
src/features/tire-hotel/Actions/tireJobActions.ts

@@ -492,7 +492,7 @@ export async function createQuoteFromTireSet(input: unknown) {
         action: 'tire_set.create_quote',
         action: 'tire_set.create_quote',
         entity: 'Quote',
         entity: 'Quote',
         entityId: result.id,
         entityId: result.id,
-        message: `Created quote ${result.quoteNumber} from a tire set`,
+        details: { key: 'tire_set_create_quote', params: { ref: result.quoteNumber ?? result.id } },
         metadata: { tireSetId: result.tireSetId },
         metadata: { tireSetId: result.tireSetId },
       }),
       }),
     }
     }
@@ -624,7 +624,7 @@ export async function createWorkOrderFromTireSet(input: unknown) {
         action: 'tire_set.create_work_order',
         action: 'tire_set.create_work_order',
         entity: 'ServiceRecord',
         entity: 'ServiceRecord',
         entityId: result.id,
         entityId: result.id,
-        message: `Created work order ${result.invoiceNumber ?? result.id} from a tire set`,
+        details: { key: 'tire_set_create_work_order', params: { ref: result.invoiceNumber ?? result.id } },
         metadata: { tireSetId: result.tireSetId },
         metadata: { tireSetId: result.tireSetId },
       }),
       }),
     }
     }
@@ -759,7 +759,7 @@ export async function addTireSetToWorkOrder(input: unknown) {
         action: 'tire_set.add_to_work_order',
         action: 'tire_set.add_to_work_order',
         entity: 'ServiceRecord',
         entity: 'ServiceRecord',
         entityId: result.id,
         entityId: result.id,
-        message: `Added tire set ${result.reference ?? result.tireSetId} to work order ${result.invoiceNumber ?? result.id}`,
+        details: { key: 'tire_set_add_to_work_order', params: { ref: result.reference ?? result.tireSetId, jobRef: result.invoiceNumber ?? result.id } },
         metadata: { tireSetId: result.tireSetId },
         metadata: { tireSetId: result.tireSetId },
       }),
       }),
     }
     }
@@ -816,7 +816,7 @@ export async function unlinkTireSetFromWorkOrder(serviceRecordId: string) {
         action: 'tire_set.unlink_work_order',
         action: 'tire_set.unlink_work_order',
         entity: 'ServiceRecord',
         entity: 'ServiceRecord',
         entityId: result.id,
         entityId: result.id,
-        message: `Unlinked tire set ${result.reference ?? result.tireSetId} from work order ${result.invoiceNumber ?? result.id}`,
+        details: { key: 'tire_set_unlink_work_order', params: { ref: result.reference ?? result.tireSetId, jobRef: result.invoiceNumber ?? result.id } },
         metadata: { tireSetId: result.tireSetId },
         metadata: { tireSetId: result.tireSetId },
       }),
       }),
     }
     }

+ 18 - 9
src/features/tire-hotel/Actions/tireSetActions.ts

@@ -15,7 +15,7 @@ import {
 } from '../Schema/tireHotelSchema'
 } from '../Schema/tireHotelSchema'
 import type { MeasurementInput } from '../Schema/tireHotelSchema'
 import type { MeasurementInput } from '../Schema/tireHotelSchema'
 import { requireTireHotel } from '../Lib/tireHotelSettings'
 import { requireTireHotel } from '../Lib/tireHotelSettings'
-import { plural } from '../Lib/auditText'
+import { auditDetails } from '@/lib/audit'
 
 
 const READ = [{ action: PermissionAction.READ, subject: PermissionSubject.TIRE_HOTEL }]
 const READ = [{ action: PermissionAction.READ, subject: PermissionSubject.TIRE_HOTEL }]
 const CREATE = [{ action: PermissionAction.CREATE, subject: PermissionSubject.TIRE_HOTEL }]
 const CREATE = [{ action: PermissionAction.CREATE, subject: PermissionSubject.TIRE_HOTEL }]
@@ -387,7 +387,7 @@ export async function checkInTireSet(input: unknown) {
         action: 'tire_set.check_in',
         action: 'tire_set.check_in',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.id,
         entityId: result.id,
-        message: `Checked in tire set ${result.reference} to ${result.locationCode}`,
+        details: { key: 'tire_set_check_in', params: { ref: result.reference ?? result.id, code: result.locationCode } },
         metadata: { quantity: result.quantity },
         metadata: { quantity: result.quantity },
       }),
       }),
     }
     }
@@ -618,7 +618,7 @@ export async function returnTireSet(input: unknown) {
         action: 'tire_set.return',
         action: 'tire_set.return',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.id,
         entityId: result.id,
-        message: `Stored tire set ${result.reference} again, on ${result.locationCode}`,
+        details: { key: 'tire_set_return', params: { ref: result.reference ?? result.id, code: result.locationCode } },
         metadata: { quantity: result.quantity },
         metadata: { quantity: result.quantity },
       }),
       }),
     }
     }
@@ -682,7 +682,7 @@ export async function disposeTireSet(input: unknown) {
         action: 'tire_set.dispose',
         action: 'tire_set.dispose',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.id,
         entityId: result.id,
-        message: `Wrote off tire set ${result.reference}`,
+        details: { key: 'tire_set_dispose', params: { ref: result.reference ?? result.id } },
       }),
       }),
     }
     }
   )
   )
@@ -737,7 +737,7 @@ export async function checkOutTireSet(input: unknown) {
         action: 'tire_set.check_out',
         action: 'tire_set.check_out',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.id,
         entityId: result.id,
-        message: `Checked out tire set ${result.reference}`,
+        details: { key: 'tire_set_check_out', params: { ref: result.reference ?? result.id } },
       }),
       }),
     }
     }
   )
   )
@@ -791,7 +791,16 @@ export async function relocateTireSet(input: unknown) {
         action: 'tire_set.relocate',
         action: 'tire_set.relocate',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.id,
         entityId: result.id,
-        message: `Moved tire set ${result.reference} from ${result.fromCode ?? 'unassigned'} to ${result.toCode}`,
+        details: result.fromCode
+          ? auditDetails('tire_set_relocate', {
+              ref: result.reference ?? result.id,
+              from: result.fromCode,
+              to: result.toCode,
+            })
+          : auditDetails('tire_set_relocateUnassigned', {
+              ref: result.reference ?? result.id,
+              to: result.toCode,
+            }),
       }),
       }),
     }
     }
   )
   )
@@ -889,7 +898,7 @@ export async function updateTireSet(input: unknown) {
         action: 'tire_set.update',
         action: 'tire_set.update',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.id,
         entityId: result.id,
-        message: `Updated tire set ${result.reference}`,
+        details: { key: 'tire_set_update', params: { ref: result.reference ?? result.id } },
       }),
       }),
     }
     }
   )
   )
@@ -925,7 +934,7 @@ export async function addMeasurements(input: { tireSetId: string; measurements:
         action: 'tire_measurement.create',
         action: 'tire_measurement.create',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.id,
         entityId: result.id,
-        message: `Recorded ${plural(result.count, 'reading')} on tire set ${result.reference}`,
+        details: { key: 'tire_measurement_create', params: { count: result.count, ref: result.reference ?? result.id } },
       }),
       }),
     }
     }
   )
   )
@@ -955,7 +964,7 @@ export async function deleteTireSet(id: string) {
         action: 'tire_set.delete',
         action: 'tire_set.delete',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.id,
         entityId: result.id,
-        message: `Deleted tire set ${result.reference}`,
+        details: { key: 'tire_set_delete', params: { ref: result.reference ?? result.id } },
       }),
       }),
     }
     }
   )
   )

+ 3 - 4
src/features/tire-hotel/Actions/treatmentActions.ts

@@ -7,7 +7,6 @@ import { withAuth } from '@/lib/with-auth'
 import { PermissionAction, PermissionSubject } from '@/lib/permissions'
 import { PermissionAction, PermissionSubject } from '@/lib/permissions'
 import { TREATMENT_STATUSES, TREATMENT_TYPES } from '../Lib/treatments'
 import { TREATMENT_STATUSES, TREATMENT_TYPES } from '../Lib/treatments'
 import { requireTireHotel } from '../Lib/tireHotelSettings'
 import { requireTireHotel } from '../Lib/tireHotelSettings'
-import { humanise, plural } from '../Lib/auditText'
 
 
 const UPDATE = [{ action: PermissionAction.UPDATE, subject: PermissionSubject.TIRE_HOTEL }]
 const UPDATE = [{ action: PermissionAction.UPDATE, subject: PermissionSubject.TIRE_HOTEL }]
 
 
@@ -91,7 +90,7 @@ export async function setTreatments(input: unknown) {
         action: 'tire_treatment.update',
         action: 'tire_treatment.update',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.id,
         entityId: result.id,
-        message: `Updated prep work on tire set ${result.reference ?? result.id}`,
+        details: { key: 'tire_treatment_update', params: { ref: result.reference ?? result.id } },
         metadata: { added: result.added, removed: result.removed },
         metadata: { added: result.added, removed: result.removed },
       }),
       }),
     }
     }
@@ -137,7 +136,7 @@ export async function markTreatment(input: unknown) {
         action: 'tire_treatment.mark',
         action: 'tire_treatment.mark',
         entity: 'TireTreatment',
         entity: 'TireTreatment',
         entityId: result.id,
         entityId: result.id,
-        message: `Marked ${humanise(result.type)} as ${result.status} on tire set ${result.reference ?? result.tireSetId}`,
+        details: { key: 'tire_treatment_mark', params: { type: result.type, status: result.status, ref: result.reference ?? result.tireSetId } },
         metadata: { tireSetId: result.tireSetId },
         metadata: { tireSetId: result.tireSetId },
       }),
       }),
     }
     }
@@ -170,7 +169,7 @@ export async function completeAllTreatments(tireSetId: string) {
         action: 'tire_treatment.complete_all',
         action: 'tire_treatment.complete_all',
         entity: 'TireSet',
         entity: 'TireSet',
         entityId: result.id,
         entityId: result.id,
-        message: `Completed ${plural(result.count, 'prep job')} on tire set ${result.reference ?? result.id}`,
+        details: { key: 'tire_treatment_complete_all', params: { count: result.count, ref: result.reference ?? result.id } },
       }),
       }),
     }
     }
   )
   )

+ 0 - 29
src/features/tire-hotel/Lib/auditText.ts

@@ -1,29 +0,0 @@
-/**
- * Wording for the audit trail.
- *
- * Audit messages are composed once and stored, so unlike the rest of the app
- * they cannot be translated at read time. They are English by design, which
- * makes it all the more important that they read as English: a stored line is
- * the one thing nobody can fix later without a migration.
- */
-
-/**
- * "1 file", "3 files".
- *
- * Replaces the `file(s)` shorthand, which reads as a placeholder somebody
- * forgot to finish and is wrong in the singular either way.
- */
-export function plural(count: number, noun: string, pluralForm?: string): string {
-  return `${count} ${count === 1 ? noun : (pluralForm ?? `${noun}s`)}`
-}
-
-/**
- * Turns a stored enum into something readable: `wash_tires` to `wash tires`.
- *
- * Not translated, and deliberately not a lookup table. A table would have to
- * be kept in step with TREATMENT_TYPES by hand, and the failure when it drifts
- * is silent: a new treatment type prints as nothing rather than as itself.
- */
-export function humanise(value: string): string {
-  return value.replaceAll('_', ' ')
-}

+ 1 - 1
src/features/vehicles/Actions/archiveVehicle.ts

@@ -28,7 +28,7 @@ export async function archiveVehicle(vehicleId: string, reason?: string) {
         action: 'vehicle.archive',
         action: 'vehicle.archive',
         entity: 'Vehicle',
         entity: 'Vehicle',
         entityId: result.vehicleId,
         entityId: result.vehicleId,
-        message: `Archived vehicle ${result.vehicleId}`,
+        details: { key: 'vehicle_archive', params: { id: result.vehicleId } },
         metadata: { vehicleId: result.vehicleId },
         metadata: { vehicleId: result.vehicleId },
       }),
       }),
     }
     }

+ 2 - 2
src/features/vehicles/Actions/createDraftServiceRecord.ts

@@ -43,7 +43,7 @@ export async function createDraftServiceRecord(
         action: 'service.create',
         action: 'service.create',
         entity: 'ServiceRecord',
         entity: 'ServiceRecord',
         entityId: result.id,
         entityId: result.id,
-        message: `Created draft service record ${result.invoiceNumber || result.id}`,
+        details: { key: 'service_createDraft', params: { ref: result.invoiceNumber || result.id } },
         metadata: { serviceRecordId: result.id, vehicleId: result.vehicleId },
         metadata: { serviceRecordId: result.id, vehicleId: result.vehicleId },
       }),
       }),
     }
     }
@@ -80,7 +80,7 @@ export async function createDraftCounterSale(customerId: string) {
         action: 'service.create',
         action: 'service.create',
         entity: 'ServiceRecord',
         entity: 'ServiceRecord',
         entityId: result.id,
         entityId: result.id,
-        message: `Created counter sale ${result.invoiceNumber || result.id}`,
+        details: { key: 'service_createCounterSale', params: { ref: result.invoiceNumber || result.id } },
         metadata: { serviceRecordId: result.id, customerId: result.customerId },
         metadata: { serviceRecordId: result.id, customerId: result.customerId },
       }),
       }),
     }
     }

+ 4 - 4
src/features/vehicles/Actions/findingActions.ts

@@ -198,7 +198,7 @@ export async function createFinding(input: unknown) {
         action: 'finding.create',
         action: 'finding.create',
         entity: 'VehicleFinding',
         entity: 'VehicleFinding',
         entityId: result.id,
         entityId: result.id,
-        message: `Created finding "${result.description}" on vehicle ${result.vehicleId}`,
+        details: { key: 'finding_create', params: { description: result.description, vehicleId: result.vehicleId } },
         metadata: { findingId: result.id, vehicleId: result.vehicleId },
         metadata: { findingId: result.id, vehicleId: result.vehicleId },
       }),
       }),
     }
     }
@@ -236,7 +236,7 @@ export async function updateFinding(input: unknown) {
         action: 'finding.update',
         action: 'finding.update',
         entity: 'VehicleFinding',
         entity: 'VehicleFinding',
         entityId: result.id,
         entityId: result.id,
-        message: `Updated finding "${result.description}"`,
+        details: { key: 'finding_update', params: { description: result.description } },
         metadata: { findingId: result.id, vehicleId: result.vehicleId },
         metadata: { findingId: result.id, vehicleId: result.vehicleId },
       }),
       }),
     }
     }
@@ -274,7 +274,7 @@ export async function resolveFinding(input: unknown) {
         action: 'finding.resolve',
         action: 'finding.resolve',
         entity: 'VehicleFinding',
         entity: 'VehicleFinding',
         entityId: result.id,
         entityId: result.id,
-        message: `Resolved finding "${result.description}"`,
+        details: { key: 'finding_resolve', params: { description: result.description } },
         metadata: { findingId: result.id, vehicleId: result.vehicleId },
         metadata: { findingId: result.id, vehicleId: result.vehicleId },
       }),
       }),
     }
     }
@@ -305,7 +305,7 @@ export async function deleteFinding(findingId: string) {
         action: 'finding.delete',
         action: 'finding.delete',
         entity: 'VehicleFinding',
         entity: 'VehicleFinding',
         entityId: result.findingId,
         entityId: result.findingId,
-        message: `Deleted finding ${result.findingId}`,
+        details: { key: 'finding_delete', params: { id: result.findingId } },
         metadata: { findingId: result.findingId },
         metadata: { findingId: result.findingId },
       }),
       }),
     }
     }

+ 2 - 2
src/features/vehicles/Actions/noteActions.ts

@@ -70,7 +70,7 @@ export async function createNote(input: unknown) {
         action: 'note.create',
         action: 'note.create',
         entity: 'Note',
         entity: 'Note',
         entityId: result.id,
         entityId: result.id,
-        message: `Created note on vehicle ${result.vehicleId}`,
+        details: { key: 'note_create', params: { vehicleId: result.vehicleId } },
         metadata: { noteId: result.id, vehicleId: result.vehicleId },
         metadata: { noteId: result.id, vehicleId: result.vehicleId },
       }),
       }),
     }
     }
@@ -138,7 +138,7 @@ export async function deleteNote(noteId: string) {
         action: 'note.delete',
         action: 'note.delete',
         entity: 'Note',
         entity: 'Note',
         entityId: result.noteId,
         entityId: result.noteId,
-        message: `Deleted note ${result.noteId}`,
+        details: { key: 'note_delete', params: { id: result.noteId } },
         metadata: { noteId: result.noteId },
         metadata: { noteId: result.noteId },
       }),
       }),
     }
     }

+ 2 - 2
src/features/vehicles/Actions/reminderActions.ts

@@ -86,7 +86,7 @@ export async function createReminder(input: unknown) {
         action: 'reminder.create',
         action: 'reminder.create',
         entity: 'Reminder',
         entity: 'Reminder',
         entityId: result.id,
         entityId: result.id,
-        message: `Created reminder "${result.title}"`,
+        details: { key: 'reminder_create', params: { title: result.title } },
         metadata: { reminderId: result.id, vehicleId: result.vehicleId },
         metadata: { reminderId: result.id, vehicleId: result.vehicleId },
       }),
       }),
     }
     }
@@ -189,7 +189,7 @@ export async function deleteReminder(reminderId: string) {
         action: 'reminder.delete',
         action: 'reminder.delete',
         entity: 'Reminder',
         entity: 'Reminder',
         entityId: result.reminderId,
         entityId: result.reminderId,
-        message: `Deleted reminder ${result.reminderId}`,
+        details: { key: 'reminder_delete', params: { id: result.reminderId } },
         metadata: { reminderId: result.reminderId },
         metadata: { reminderId: result.reminderId },
       }),
       }),
     }
     }

+ 9 - 4
src/features/vehicles/Actions/serviceActions.ts

@@ -502,7 +502,7 @@ export async function createServiceRecord(input: unknown) {
         action: 'service.create',
         action: 'service.create',
         entity: 'ServiceRecord',
         entity: 'ServiceRecord',
         entityId: result.id,
         entityId: result.id,
-        message: `Created service record ${result.invoiceNumber || result.id}`,
+        details: { key: 'service_create', params: { ref: result.invoiceNumber || result.id } },
         metadata: { serviceRecordId: result.id, vehicleId: result.vehicleId },
         metadata: { serviceRecordId: result.id, vehicleId: result.vehicleId },
       }),
       }),
     }
     }
@@ -751,7 +751,7 @@ export async function updateServiceRecord(input: unknown) {
         action: 'service.update',
         action: 'service.update',
         entity: 'ServiceRecord',
         entity: 'ServiceRecord',
         entityId: result.id,
         entityId: result.id,
-        message: `Updated service record ${result.invoiceNumber || result.id}`,
+        details: { key: 'service_update', params: { ref: result.invoiceNumber || result.id } },
         metadata: { serviceRecordId: result.id },
         metadata: { serviceRecordId: result.id },
       }),
       }),
     }
     }
@@ -804,7 +804,12 @@ export async function updateServiceStatus(recordId: string, status: string) {
         action: 'service.status',
         action: 'service.status',
         entity: 'ServiceRecord',
         entity: 'ServiceRecord',
         entityId: result.recordId,
         entityId: result.recordId,
-        message: `Changed service record status to ${result.status}`,
+        // ICU select selectors cannot contain a hyphen, and the stored
+        // statuses do: in-progress, waiting-parts.
+        details: {
+          key: 'service_status',
+          params: { status: result.status.replaceAll('-', '_') },
+        },
         metadata: { serviceRecordId: result.recordId, status: result.status },
         metadata: { serviceRecordId: result.recordId, status: result.status },
       }),
       }),
     }
     }
@@ -1006,7 +1011,7 @@ export async function deleteServiceRecord(recordId: string) {
         action: 'service.delete',
         action: 'service.delete',
         entity: 'ServiceRecord',
         entity: 'ServiceRecord',
         entityId: result.recordId,
         entityId: result.recordId,
-        message: `Deleted service record ${result.recordId}`,
+        details: { key: 'service_delete', params: { ref: result.recordId } },
         metadata: { serviceRecordId: result.recordId },
         metadata: { serviceRecordId: result.recordId },
       }),
       }),
     }
     }

+ 1 - 1
src/features/vehicles/Actions/unarchiveVehicle.ts

@@ -28,7 +28,7 @@ export async function unarchiveVehicle(vehicleId: string) {
         action: 'vehicle.unarchive',
         action: 'vehicle.unarchive',
         entity: 'Vehicle',
         entity: 'Vehicle',
         entityId: result.vehicleId,
         entityId: result.vehicleId,
-        message: `Unarchived vehicle ${result.vehicleId}`,
+        details: { key: 'vehicle_unarchive', params: { id: result.vehicleId } },
         metadata: { vehicleId: result.vehicleId },
         metadata: { vehicleId: result.vehicleId },
       }),
       }),
     }
     }

+ 9 - 3
src/features/vehicles/Actions/vehicleActions.ts

@@ -8,6 +8,7 @@ import { createVehicleSchema, updateVehicleSchema } from '../Schema/vehicleSchem
 import { revalidatePath } from 'next/cache'
 import { revalidatePath } from 'next/cache'
 import { unlink } from 'fs/promises'
 import { unlink } from 'fs/promises'
 import { resolveUploadPath } from '@/lib/resolve-upload-path'
 import { resolveUploadPath } from '@/lib/resolve-upload-path'
+import { auditDetails } from "@/lib/audit";
 
 
 export async function getVehicles() {
 export async function getVehicles() {
   return withAuth(
   return withAuth(
@@ -194,7 +195,7 @@ export async function createVehicle(input: unknown) {
         action: 'vehicle.create',
         action: 'vehicle.create',
         entity: 'Vehicle',
         entity: 'Vehicle',
         entityId: result.id,
         entityId: result.id,
-        message: `Created vehicle ${result.year} ${result.make} ${result.model}`,
+        details: { key: 'vehicle_create', params: { year: result.year, make: result.make, model: result.model } },
         metadata: { vehicleId: result.id },
         metadata: { vehicleId: result.id },
       }),
       }),
     }
     }
@@ -261,7 +262,12 @@ export async function updateVehicle(input: unknown) {
         action: 'vehicle.update',
         action: 'vehicle.update',
         entity: 'Vehicle',
         entity: 'Vehicle',
         entityId: result.id,
         entityId: result.id,
-        message: `Updated vehicle ${result.vehicleDisplay} — changed: ${result.fields.join(', ') || '(no changes)'}`,
+        details: result.fields.length
+        ? auditDetails("vehicle_update", {
+            name: result.vehicleDisplay,
+            fields: result.fields.join(", "),
+          })
+        : auditDetails("vehicle_updateNoChanges", { name: result.vehicleDisplay }),
         metadata: {
         metadata: {
           vehicleId: result.id,
           vehicleId: result.id,
           vehicleDisplay: result.vehicleDisplay,
           vehicleDisplay: result.vehicleDisplay,
@@ -324,7 +330,7 @@ export async function deleteVehicle(vehicleId: string) {
         action: 'vehicle.delete',
         action: 'vehicle.delete',
         entity: 'Vehicle',
         entity: 'Vehicle',
         entityId: result.vehicleId,
         entityId: result.vehicleId,
-        message: `Deleted vehicle ${result.vehicleDisplay}`,
+        details: { key: 'vehicle_delete', params: { name: result.vehicleDisplay } },
         metadata: { vehicleId: result.vehicleId, vehicleDisplay: result.vehicleDisplay },
         metadata: { vehicleId: result.vehicleId, vehicleDisplay: result.vehicleDisplay },
       }),
       }),
     }
     }

+ 1 - 1
src/features/webhooks/Actions/webhookActions.ts

@@ -105,7 +105,7 @@ export async function createWebhook(input: unknown) {
         action: "webhook.create",
         action: "webhook.create",
         entity: "webhook",
         entity: "webhook",
         entityId: result.id,
         entityId: result.id,
-        message: `Created webhook ${result.name}`,
+        details: { key: "webhook_create", params: { name: result.name } },
       }),
       }),
     },
     },
   );
   );

+ 3 - 3
src/features/workboard/Actions/technicianActions.ts

@@ -84,7 +84,7 @@ export async function createTechnician(input: unknown) {
         action: "technician.create",
         action: "technician.create",
         entity: "Technician",
         entity: "Technician",
         entityId: result.id,
         entityId: result.id,
-        message: `Created technician "${result.name}"`,
+        details: { key: "technician_create", params: { name: result.name } },
         metadata: { technicianId: result.id, technicianName: result.name },
         metadata: { technicianId: result.id, technicianName: result.name },
       }),
       }),
     },
     },
@@ -137,7 +137,7 @@ export async function updateTechnician(input: unknown) {
         action: "technician.update",
         action: "technician.update",
         entity: "Technician",
         entity: "Technician",
         entityId: result.technicianId,
         entityId: result.technicianId,
-        message: `Updated technician ${result.technicianId}`,
+        details: { key: "technician_update", params: { id: result.technicianId } },
         metadata: { technicianId: result.technicianId },
         metadata: { technicianId: result.technicianId },
       }),
       }),
     },
     },
@@ -167,7 +167,7 @@ export async function deleteTechnician(id: string) {
         action: "technician.delete",
         action: "technician.delete",
         entity: "Technician",
         entity: "Technician",
         entityId: result.technicianId,
         entityId: result.technicianId,
-        message: `Deleted technician ${result.technicianId}`,
+        details: { key: "technician_delete", params: { id: result.technicianId } },
         metadata: { technicianId: result.technicianId },
         metadata: { technicianId: result.technicianId },
       }),
       }),
     },
     },

+ 74 - 4
src/lib/audit.ts

@@ -1,10 +1,72 @@
 import { db } from "@/lib/db";
 import { db } from "@/lib/db";
+import { createTranslator } from "next-intl";
+import enAudit from "../../messages/en/audit.json";
+
+/**
+ * The readable half of an audit row, as ingredients rather than a sentence.
+ *
+ * A finished sentence can only ever be in one language: it is composed once
+ * and stored, and nothing downstream knows what its words meant. Keeping the
+ * key and its values means the page can say the same thing in whichever
+ * language the reader is using, years after the event.
+ */
+export type AuditDetails = {
+  /** A key under `summary` in messages/<locale>/audit.json. */
+  key: string;
+  params?: Record<string, string | number>;
+};
+
+/**
+ * Names an audit sentence and its values.
+ *
+ * A plain object literal would do, except where the key depends on the data:
+ * two literals in a ternary widen into a union that no longer satisfies the
+ * parameter record, and the error that produces points at the call site
+ * rather than at the cause. This gives both branches the same type.
+ */
+export function auditDetails(
+  key: string,
+  params?: Record<string, string | number>,
+): AuditDetails {
+  return { key, params };
+}
+
+/**
+ * The English sentence, rendered from the same catalogue the UI uses.
+ *
+ * Still written to the row, for three reasons: the audit search runs against
+ * this column, webhook subscribers receive it, and rows written before this
+ * existed have nothing else. Deriving it here rather than at the call site
+ * means the stored English and the translated English cannot drift apart.
+ */
+function renderEnglish(details: AuditDetails): string | null {
+  try {
+    const translate = createTranslator({
+      locale: "en",
+      messages: { audit: enAudit },
+      namespace: "audit.summary",
+    } as Parameters<typeof createTranslator>[0]) as unknown as (
+      key: string,
+      values?: Record<string, string | number>,
+    ) => string;
+    return translate(details.key, details.params);
+  } catch {
+    // An unknown key must not lose the audit row. The key itself is a better
+    // record than nothing, and the missing-key test below catches it in CI.
+    return details.key;
+  }
+}
 
 
 export type AuditEvent = {
 export type AuditEvent = {
   action: string;
   action: string;
   entity?: string;
   entity?: string;
   entityId?: string;
   entityId?: string;
+  /**
+   * Pre-composed English. Only for events with nothing to translate; prefer
+   * `details`, which reads in the viewer's own language.
+   */
   message?: string;
   message?: string;
+  details?: AuditDetails;
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   metadata?: Record<string, any> | null;
   metadata?: Record<string, any> | null;
   ip?: string | null;
   ip?: string | null;
@@ -15,6 +77,14 @@ export async function logAudit(
   ctx: { userId: string; organizationId: string },
   ctx: { userId: string; organizationId: string },
   event: AuditEvent,
   event: AuditEvent,
 ) {
 ) {
+  const message = event.details ? renderEnglish(event.details) : (event.message ?? null);
+  // Carried in metadata rather than its own column, so no existing row has to
+  // be migrated and an older deployment reading the same database still finds
+  // everything it expects.
+  const metadata = event.details
+    ? { ...(event.metadata ?? {}), details: event.details }
+    : event.metadata;
+
   try {
   try {
     await db.auditLog.create({
     await db.auditLog.create({
       data: {
       data: {
@@ -23,8 +93,8 @@ export async function logAudit(
         action: event.action,
         action: event.action,
         entity: event.entity ?? null,
         entity: event.entity ?? null,
         entityId: event.entityId ?? null,
         entityId: event.entityId ?? null,
-        message: event.message ?? null,
-        metadata: event.metadata ?? undefined,
+        message,
+        metadata: metadata ?? undefined,
         ip: event.ip ?? null,
         ip: event.ip ?? null,
         userAgent: event.userAgent ?? null,
         userAgent: event.userAgent ?? null,
       },
       },
@@ -45,8 +115,8 @@ export async function logAudit(
           organizationId: ctx.organizationId,
           organizationId: ctx.organizationId,
           entity: event.entity ?? null,
           entity: event.entity ?? null,
           entityId: event.entityId ?? null,
           entityId: event.entityId ?? null,
-          message: event.message ?? null,
-          data: event.metadata ?? null,
+          message,
+          data: metadata ?? null,
           userId: ctx.userId || null,
           userId: ctx.userId || null,
         }),
         }),
       )
       )