NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Enums for Laravel

Labels from translation files

Available from 1.1.0. Put the #[TranslatedLabels] attribute on an enum that uses Helpers, and each label is read from a translation group instead of the headline — the layout most apps already keep in lang/<locale>/enums.php:

use RoundlyConsulting\Enums\Attributes\TranslatedLabels;
use RoundlyConsulting\Enums\Helpers;

#[TranslatedLabels]                 // keys: enums.order_status.<value>
enum OrderStatus: string
{
    use Helpers;

    case Pending = 'pending';
    case Shipped = 'shipped';
}
// lang/sk/enums.php
return [
    'order_status' => [
        'pending' => 'Čaká na platbu',
        'shipped' => 'Na ceste',
    ],
];
app()->setLocale('sk');

OrderStatus::Pending->label();      // 'Čaká na platbu'
OrderStatus::toArray();             // ['pending' => 'Čaká na platbu', 'shipped' => 'Na ceste']
OrderStatus::options();             // EnumOption DTOs with the same labels, shape unchanged
OrderStatus::fromLabel('Na ceste'); // OrderStatus::Shipped

Every list built on labels — labels(), toOptions(), toArray(), options(), presentations(), fromLabel() and tryFromLabel() — translates with it, and options() keeps its shape. Enums without the attribute behave exactly as before.

The group

#[TranslatedLabels]                                // enums.order_status.<value>
#[TranslatedLabels('enums.checkout_status')]       // an explicit group
#[TranslatedLabels('enums.http_method')]           // HTTPMethod would snake to enums.h_t_t_p_method
#[TranslatedLabels('billing::enums.order_status')] // a package's own translation files
  • With no argument the group is enums. plus the snake_case class name: OrderStatus → enums.order_status, ToolAuth → enums.tool_auth.
  • An acronym class name snakes letter by letter (HTTPMethod → enums.h_t_t_p_method), so pass such a group explicitly.
  • A package labels its own enums from its own files with a namespaced group: 'billing::enums.order_status' reads resources/lang/<locale>/enums.php registered under the billing translation namespace.
  • A blank group, or one that starts or ends with whitespace, . or :, can only ever miss, so it throws EnumException::invalidLabelGroup() on first use rather than falling back quietly.
  • The attribute goes on the enum itself and does nothing on an enum without the Helpers trait.

Keys and resolution order

The key is <group>.<value>: the backed value for string and int enums (enums.priority.5, enums.priority.-1) and the case name for pure enums (enums.visibility.Public). For each label, readable() tries:

  • First, the grouped key in the current locale, then in fallback_locale — Laravel’s translator does both. A line wins.
  • Otherwise, the headline behaviour, unchanged: the headline (Pending), looked up as a JSON or group line, then the raw headline.

So adding the attribute never breaks an enum whose keys aren’t filled in yet: missing cases keep their headline labels while you add the lines. A key that resolves to a nested array instead of a line counts as a miss. A missing-key handler registered with Lang::handleMissingKeysUsing() fires on a grouped miss just as it does for __(), and its return value becomes the label.

Values holding a . can never match: the translator reads the dot as nesting, so the key enums.release.v1.0 looks for ['release']['v1']['0'], never the line keyed 'v1.0'. Give such enums a readable() override; untranslated() reports them.

Pinning every label translated

untranslated(?string $locale = null) returns the cases whose grouped key has no line in exactly that locale (default: the current one), in declaration order — made for a test that fails the moment a case ships untranslated:

it('translates every order status', function (string $locale) {
    expect(OrderStatus::untranslated($locale))->toBeEmpty();
})->with(['en', 'sk']);
  • The check is strict per locale: a line only in fallback_locale is still reported, because users of that locale would see the fallback text.
  • With a translator other than Laravel’s (one without hasForLocale()), it falls back to get(), so a line in that translator’s fallback locale counts.
  • A key that resolves to a nested array, and a value holding a ., are reported, since readable() can’t use them either.
  • On an enum without the attribute it throws EnumException::labelsNotTranslated().

The attribute is read once per enum class and cached for the life of the process. An attribute can’t change at runtime, so this is safe under Octane.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.