Browse documentationOpen

byrcsc/laravel-approval · 1.x

Notification content.

Reword, translate, or restructure the built-in approval notifications by publishing the translations and the mail views.

The shipped notifications say sensible but generic things — "You have been asked to approve Purchase Order #41." Changing that does not require replacing the notification classes. There are three ways in, and the cheapest one that fits is the right one:

You want toPublish
Reword or translate the messagesapproval-translations
Restructure the emails, add a link into your UIapproval-views
Change who receives them, or the whole messageNeither — see Notifications

Nothing here is required. The package ships working English messages and mail views, and an application that never publishes either still sends mail.

Rewording and translating

Every user-facing string lives in one file:

php artisan vendor:publish --tag="approval-translations"

That writes lang/vendor/approval/en/approval.php:

return [
    'requested' => [
        'subject' => 'Approval needed: :subject',
        'body' => 'You have been asked to approve :subject.',
    ],
    'approved' => [
        'subject' => 'Approved: :subject',
        'body' => ':subject has been approved.',
    ],
    // rejected, returned, assigned, overdue, conflicted

    'reason' => 'Reason: :reason',
    'reference' => 'Reference: :reference.',
    'deleted_record' => 'a deleted record',
];

Each event has a subject and a body. conflicted also has an unresolved line, and reason, reference, and deleted_record are shared across messages.

Edit it in place to reword, or copy the directory to es, fr, and so on to translate. Messages are resolved at send time through trans(), so they follow the application locale — a request routed to a French-speaking approver reads in French with no work here, provided lang/vendor/approval/fr/approval.php exists.

Placeholders may sit anywhere in the line, which is the point of translating through this file rather than concatenating strings:

'requested' => [
    'subject' => 'Approbation requise : :subject',
    'body' => 'Il vous est demandé d\'approuver :subject.',
],

:subject is a readable name for the record — its name, title, reference, or label attribute, falling back to ClassName #key. See Notifications for the full resolution order and what a deleted record reads as.

A key with no translation renders as the key itself rather than throwing, so a half-finished locale file produces visibly wrong mail instead of a failed queue job.

Restructuring the emails

To add a button into your own UI, change the layout, or drop a line entirely:

php artisan vendor:publish --tag="approval-views"

That writes resources/views/vendor/approval/mail/, one Blade file per message:

approved.blade.php     rejected.blade.php     conflicted.blade.php
requested.blade.php    returned.blade.php     overdue.blade.php
assigned.blade.php

Each is an ordinary markdown mail view. The shipped files carry only the message and reference lines; a published copy can add whatever your UI needs — a review button, say:

<x-mail::message>
{{ __('approval::approval.requested.body', ['subject' => $subject]) }}

<x-mail::button :url="route('approvals.show', $request)">
{{ __('Review it') }}
</x-mail::button>

{{ __('approval::approval.reference', ['reference' => $reference]) }}
</x-mail::message>

What each view receives

Variable
$subjectThe readable record name used in the subject line.
$referenceThe request's public ULID.
$requestThe ApprovalRequest.
$approvableThe record, or null if it has been deleted since.
$notificationThe notification instance, for anything else you need.

The rejected and returned views also receive $reason, which is nullable — both shipped views guard it with @if ($reason !== null). Keep that guard, or a decision recorded without a comment renders an empty "Reason:" line.

$approvable deserves the same care. A record hard-deleted between the decision and the send leaves the notification with a request but no record, so route('orders.show', $approvable) in a published view will fail on exactly the mail you least want to fail.

Publishing one does not freeze the other

The shipped views read their wording through __('approval::approval.…') rather than hardcoding English. Publish the views and the translations still apply; publish the translations and the views pick them up. Publishing both is fine too — they meet at the translation key.

This only holds if your published views keep reading translations. A view that inlines its text works, but takes that message out of the locale system, so mail to a French approver will be part French and part whatever you typed.

Replacing a message with your own

When the audience is wrong and not just the words, point the event at your own notification class — see Notifications for the map and the recipient helpers.

A replacement class does not lose access to the published content. Extend ApprovalNotification and call the protected mailFor(), which builds the MailMessage from the same translation keys and views:

use ByRcsc\LaravelApproval\Events\ApprovalEvent;
use ByRcsc\LaravelApproval\Notifications\ApprovalNotification;
use Illuminate\Notifications\Messages\MailMessage;

final class PurchaseOrderNeedsApproval extends ApprovalNotification
{
    public static function recipients(ApprovalEvent $event): iterable
    {
        return self::activeApprovers($event);
    }

    public function toMail(object $notifiable): MailMessage
    {
        return $this->mailFor('requested');
    }
}

recipients() is abstract on the base class, so every replacement declares its audience — that is the point.

mailFor($key, $with = []) reads approval::approval.{$key}.subject for the subject, renders approval::mail.{$key}, and passes the five variables above. Anything in $with is merged in, which is how the shipped rejected and returned notifications supply $reason:

return $this->mailFor('rejected', ['reason' => $this->reason()]);

So a class that only needs different recipients can keep the published wording entirely, and one that needs a different message can pass extra variables to a view of its own.

Both of these are the mail channel only

Publishing translations and views changes what the mail channel sends. Neither reaches the others.

database and broadcast render toArray(), which carries structured data rather than prose:

[
    'request' => '01JG3…',   // the public ULID
    'state' => 'pending',
    'subject' => 'Purchase Order #41',
]

There is nothing to translate there — subject is the record's readable name, not a translated line — so rewording approval.php leaves a database notification exactly as it was. Applications rendering an in-app inbox from these rows write their own wording against state, which is where it belongs: the row is read long after it was written, and should read in the locale of whoever is looking at it rather than the one in effect when it was stored.

To change the payload itself, override toArray() on a replacement class.