DEV Community

Laravel Mastery
Laravel Mastery

Posted on

Stop Hardcoding Translations in Laravel - Use Translatable

Stop Hardcoding Translations in Laravel - Use Translatable
🤔 The Problem
Building a multilingual Laravel app? You've probably written messy code like this:

// ❌ The nightmare approach
switch($status) {
    case 'draft':
        return $locale === 'es' ? 'Borrador' : 
               ($locale === 'fr' ? 'Brouillon' : 'Draft');
    // ... more chaos
}
Enter fullscreen mode Exit fullscreen mode

There's a much cleaner way.
✨ The Solution: Translatable Enums
Meet the Laravel Enum Translatable package by Osama Sadah.
Install It

composer require osama/laravel-enum-translatable
Enter fullscreen mode Exit fullscreen mode

Create Your Enum

<?php

namespace App\Enums;

use Osama\LaravelEnums\Concerns\EnumTranslatable;

enum ArticleStatus: string
{
    use EnumTranslatable;  // ← Magic happens here

    case DRAFT = 'draft';
    case PUBLISHED = 'published';
    case REJECTED = 'rejected';
}
Enter fullscreen mode Exit fullscreen mode

Add Translations

// lang/en/enums.php
return [
    'article_statuses' => [
        'draft' => 'Draft',
        'published' => 'Published',
        'rejected' => 'Rejected',
    ],
];

// lang/es/enums.php
return [
    'article_statuses' => [
        'draft' => 'Borrador',
        'published' => 'Publicado',
        'rejected' => 'Rechazado',
    ],
];

// lang/fr/enums.php
return [
    'article_statuses' => [
        'draft' => 'Brouillon',
        'published' => 'Publié',
        'rejected' => 'Rejeté',
    ],
];
Enter fullscreen mode Exit fullscreen mode

Use It Everywhere

$status = ArticleStatus::DRAFT;

// Current locale
$label = $status->trans(); 
// "Draft" (en) or "Borrador" (es) or "Brouillon" (fr)

// Specific locale
$spanish = $status->trans('es'); // "Borrador"

// All translations at once
$all = $status->allTrans(); 
// ['en' => 'Draft', 'es' => 'Borrador', 'fr' => 'Brouillon']
Enter fullscreen mode Exit fullscreen mode

🚀 Real Example: Blog API
The Model

use App\Enums\ArticleStatus;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    protected function casts(): array
    {
        return [
            'status' => ArticleStatus::class,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

The Controller

public function show(Article $article)
{
    return response()->json([
        'id' => $article->id,
        'title' => $article->title,
        'status' => [
            'value' => $article->status->value,
            'label' => $article->status->trans(),
            'all_translations' => $article->status->allTrans(),
        ],
    ]);
}
Enter fullscreen mode Exit fullscreen mode

The Response

{
  "id": 1,
  "title": "Getting Started with Laravel",
  "status": {
    "value": "published",
    "label": "Published",
    "all_translations": {
      "en": "Published",
      "es": "Publicado",
      "fr": "Publié"
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Perfect for multi-language frontends! 🌍
*💪 Advanced: Combine with Business Logic
*

enum OrderStatus: string
{
    use EnumTranslatable;

    case PENDING = 'pending';
    case PAID = 'paid';
    case SHIPPED = 'shipped';
    case DELIVERED = 'delivered';

    public function badge(): string
    {
        return match($this) {
            self::PENDING => 'warning',
            self::PAID => 'info',
            self::SHIPPED => 'primary',
            self::DELIVERED => 'success',
        };
    }

    public function canBeCancelled(): bool
    {
        return in_array($this, [self::PENDING, self::PAID]);
    }
}
Enter fullscreen mode Exit fullscreen mode

In Your Blade View

<span class="badge badge-{{ $order->status->badge() }}">
    {{ $order->status->trans() }}
</span>

@if($order->status->canBeCancelled())
    <button class="btn btn-danger">
        {{ __('Cancel Order') }}
    </button>
@endif
Enter fullscreen mode Exit fullscreen mode

Clean, readable, maintainable! ✨
🎯 Why This Rocks
✅ Type-Safe - PHP 8.1+ enum power
✅ DRY - Write translation once, use everywhere
✅ Clean Code - No conditionals scattered around
✅ Scalable - Add languages without code changes
✅ Testable - Easy to unit test
🧪 Quick Test Example

public function test_translations_exist_for_all_locales()
{
    $locales = ['en', 'es', 'fr'];

    foreach (ArticleStatus::cases() as $status) {
        foreach ($locales as $locale) {
            $translation = $status->trans($locale);

            $this->assertNotNull($translation);
            $this->assertNotEmpty($translation);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

📦 Quick Reference

// Get current locale translation
$status->trans()

// Get specific locale
$status->trans('es')

// Get all translations
$status->allTrans()

// In models (automatic casting)
protected $casts = ['status' => ArticleStatus::class];
Enter fullscreen mode Exit fullscreen mode

🎓 Want More?
This is a quick introduction. I've written a comprehensive guide covering:

🏗️ Multi-tenant applications
🔧 Custom translation structures
⚡ Performance optimization
🚀 Production deployment strategies
🧪 Complete testing approaches
📊 Real-world case studies

👉 Read the Full Guide on Medium →

💡 Pro Tips

  1. Always set a fallback:
// config/app.php
'fallback_locale' => 'en',
Enter fullscreen mode Exit fullscreen mode
  1. Dynamic form selects:
$options = collect(ArticleStatus::cases())
    ->map(fn($s) => [
        'value' => $s->value,
        'label' => $s->trans()
    ]);
Enter fullscreen mode Exit fullscreen mode
  1. Localized notifications:
$locale = $user->locale ?? 'en';
$message = "Status changed to: " . $order->status->trans($locale);
Enter fullscreen mode Exit fullscreen mode

🌟 Common Use Cases

E-commerce: Order statuses, payment statuses
CMS: Article statuses, content types
SaaS: Subscription tiers, user roles
Task Management: Priority levels, task states
Multi-tenant Apps: Per-tenant translations

Top comments (7)

Collapse
 
xwero profile image
david duymelinck • Edited

Why is the first solution you go to a third party package, instead of using the build-in functionality?

$status = ArticleStatus::DRAFT;
$label = $status->trans(); 

// versus
__('status.article.'.$status); // can come from the database, an enum, a hardcoded string

// status.php
return [
    'article' => [
      'draft' => 'Daft',
  ],
];
Enter fullscreen mode Exit fullscreen mode

I love enums, but even for me this is going too far.

With the package you need to maintain the translation files and the enums, that is just extra work you gave yourself.

Also the localized notification message example it very limiting.

$message = "Status changed to: " . $order->status->trans($locale);

// versus

$message = __('messages.status.changed', ['status' => __('status.article.'.$status)]);

// messages.php
return [
   'status' => [
      'changed' => 'Status changed to :status',
   ],
];
Enter fullscreen mode Exit fullscreen mode

It looks to me you seen a new toy and you can't wait to test it out, forgetting the solution that already is working.

Collapse
 
laravel_mastery_ffd9d10ec profile image
Laravel Mastery

Good point — Laravel’s built-in translation system is absolutely enough for many cases, and I’m not suggesting this approach as a replacement for it.

The goal of the example wasn’t “to translate strings differently”, but to attach behavior to the enum itself. In larger codebases, having $status->label() / $status->trans() keeps status-related logic (labels, colors, icons, transitions) close to the enum instead of spreading translation keys across the app.

You’re right that this means maintaining both enums and translation files — that’s a trade-off. In my experience, that trade-off pays off when enums are part of the domain model and reused across notifications, APIs, UI components, and business rules.

Also agree that the notification example is simplified. In real applications, the full sentence should always be translated (e.g. __('messages.status.changed', [...])) to avoid mixed-language output.

So this isn’t about a “new toy”, but about encapsulation and consistency in projects where enums are first-class domain concepts.

Collapse
 
xwero profile image
david duymelinck • Edited

spreading translation keys across the app.

It is a good idea.
But the translation keys you are using can be dynamic, as I show in my examples.

For the method you are using the dynamic part always need to be converted to an enum.

enums are part of the domain model and reused across notifications, APIs, UI components, and business rules.

The problem with that statement is that you have tied the enums to the translation of a framework.
Domain enums should be framework independent. That is one of the reasons I commented in my first example that $status could come from an enum.

So this isn’t about a “new toy”, but about encapsulation

The main thing enums are good for is to reduce magic numbers/strings. This means enums are not about encapsulation but about giving context to hard coded values.
Because the translation strings are used in a function, __() or trans(), their context is defined by that.

When you see a variable like $status in a template or a controller you need to have a bigger mental model if it is an enum with translation functionality than when it is a string.
You want the mental model to be small as possible.

The reason I mention it is a shiny new toy is because the library is three weeks old. If it was older I would have believed that you used it in a project and found it the best way to handle translations.

Thread Thread
 
laravel_mastery_ffd9d10ec profile image
Laravel Mastery

Thanks so much for your feedback! I want to make sure I’m getting your point correctly. Could you explain a bit more how you’d handle enums and translations while keeping domain enums framework-independent? I’d love to hear your perspective and discuss it — I think I could learn a lot from your approach.

Really appreciate you taking the time to share!

Cheers

Thread Thread
 
xwero profile image
david duymelinck

The short answer is use basic enums in the domain and use those as the foundation for the translations

The long answer is the use of enums depends. If we take the status example. That can be bound to an domain entity. In that case a class constant is the best solution.
When the value has a more diverse role, then you could use an enum.

There is not one right solution, just try to make the code as simple as possible.

Collapse
 
stas_7702602173bf3ccef914 profile image
Stas

Yep, that's true.

$locale = $user->locale ?? 'en';
$message = "Status changed to: " . $order->status->trans($locale);
Enter fullscreen mode Exit fullscreen mode

Also, what a nice example of a so-called "translated" string, haha :) Just imagine seeing this on your website: `Status changed to: Доставлено"

P.S. It's a shame I can't downvote posts... this one deserves it.

Collapse
 
laravel_mastery_ffd9d10ec profile image
Laravel Mastery • Edited

Fair point — that example is intentionally simplified and I agree it shouldn’t be used as-is in a real UI.

In practice, the full sentence should always be translated, e.g.:

__('messages.status.changed', [
    'status' => $order->status->trans($locale),
]);

Enter fullscreen mode Exit fullscreen mode

The purpose of the example was to show how enum-related behavior (like labels) can live on the enum itself, not to promote mixed-language strings.

Appreciate the feedback — even if we disagree on tone