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
}
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
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';
}
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é',
],
];
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']
🚀 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,
];
}
}
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(),
],
]);
}
The Response
{
"id": 1,
"title": "Getting Started with Laravel",
"status": {
"value": "published",
"label": "Published",
"all_translations": {
"en": "Published",
"es": "Publicado",
"fr": "Publié"
}
}
}
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]);
}
}
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
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);
}
}
}
📦 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];
🎓 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
- Always set a fallback:
// config/app.php
'fallback_locale' => 'en',
- Dynamic form selects:
$options = collect(ArticleStatus::cases())
->map(fn($s) => [
'value' => $s->value,
'label' => $s->trans()
]);
- Localized notifications:
$locale = $user->locale ?? 'en';
$message = "Status changed to: " . $order->status->trans($locale);
🌟 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)
Why is the first solution you go to a third party package, instead of using the build-in functionality?
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.
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.
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.
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.
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
$statuscould come from an enum.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,
__()ortrans(), their context is defined by that.When you see a variable like
$statusin 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.
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
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.
Yep, that's true.
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.
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.:
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