laravel-translatable maintained by emrane23
🌍 laravel-translatable
A simple, elegant and powerful translation package for Laravel — by Emrane Klaai
✨ Philosophy
Most translation packages store ALL languages in separate columns or JSON fields.
This package takes a different approach:
- ✅ Default language lives directly in the model column (fast, native SQL)
- ✅ Other languages live in a separate
translationstable (clean, scalable) - ✅ Automatic fallback — if a translation is missing, returns the default language
- ✅ Magic getter — just call
$model->name, it returns the right language automatically - ✅ Zero code change in controllers or views
📋 Requirements
- PHP 8.1+
- Laravel 10.x / 11.x / 12.x / 13.x
- Any database supported by Laravel (MySQL, PostgreSQL, SQLite)
📦 Installation
composer require emrane23/laravel-translatable
Publish and run the migration:
php artisan vendor:publish --tag="translatable-migrations"
php artisan migrate
Optionally publish the config:
php artisan vendor:publish --tag="translatable-config"
⚙️ Configuration
// config/translatable.php
return [
'default_locale' => env('APP_LOCALE', 'fr'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'supported_locales' => ['fr', 'en', 'ar', 'es'],
];
And in your .env:
APP_LOCALE=fr
APP_FALLBACK_LOCALE=en
🚀 Quick Start
1. Add the Trait to your Model
use Emrane23\Translatable\Traits\Translatable;
class Product extends Model
{
use Translatable;
protected $fillable = ['name', 'description', 'price'];
// Columns that can be translated
protected $translatable = ['name', 'description'];
}
2. That's it. It just works. ✨
// App locale is 'fr' → returns "Ordinateur portable" (from column)
// App locale is 'en' → returns "Laptop" (from translations table)
// App locale is 'ar' → returns "حاسوب محمول" (from translations table)
// App locale is 'de' → returns "Ordinateur portable" (fallback to default)
$product->name;
🌐 Middleware — Auto-detect locale from frontend
The TranslationMiddleware reads the X-Locale header from your frontend and sets the application locale automatically.
Laravel 11, 12, 13 — bootstrap/app.php
use Emrane23\Translatable\Middleware\TranslationMiddleware;
->withMiddleware(function (Middleware $middleware) {
$middleware->appendToGroup('api', TranslationMiddleware::class);
})
Laravel 10 — app/Http/Kernel.php
use Emrane23\Translatable\Middleware\TranslationMiddleware;
protected $middlewareGroups = [
'api' => [
// ...
TranslationMiddleware::class,
],
];
Frontend usage
// Axios — Vue.js / React / any SPA
axios.defaults.headers.common['X-Locale'] = 'en';
The middleware reads the X-Locale header and sets the application locale automatically.
Carbon dates, validation messages, error responses — everything follows. 🎯
📧 Email Locale — HasLocalePreference
Add this to your User model to send emails in the user's preferred language:
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Authenticatable implements HasLocalePreference
{
public function preferredLocale(): string
{
return $this->locale ?? config('app.locale');
}
}
Laravel will automatically use the user's locale when sending notifications. 🚀
📚 Available Methods
Get a translated attribute
// Uses current app locale automatically
$product->name;
// Explicit locale
$product->getTranslatedAttribute('name', 'en');
// With fallback control
$product->getTranslatedAttribute('name', 'es', false); // No fallback
Get translation with metadata
// Returns [value, locale_used, found]
[$value, $locale, $found] = $product->getTranslatedAttributeMeta('name', 'en');
// $value → "Laptop"
// $locale → "en"
// $found → true
Set translations
$product->setAttributeTranslations('name', [
'fr' => 'Ordinateur portable', // Saved directly to column
'en' => 'Laptop', // Saved to translations table
'ar' => 'حاسوب محمول', // Saved to translations table
'es' => 'Portátil', // Saved to translations table
]);
// Save immediately
$product->setAttributeTranslations('name', [
'en' => 'Laptop',
], save: true);
Eager load translations (avoid N+1)
// Uses current app locale + fallback
Product::withTranslation()->get();
// Specific locale
Product::withTranslation('en')->get();
// Without fallback
Product::withTranslation('en', false)->get();
Delete translations
// Delete one locale for one attribute
$product->deleteAttributeTranslation('name', 'en');
// Delete multiple locales for one attribute
$product->deleteAttributeTranslation('name', ['en', 'es']);
// Delete multiple attributes and locales
$product->deleteAttributeTranslations(['name', 'description'], ['en', 'es']);
// Delete all translations for given attributes
$product->deleteAttributeTranslations(['name', 'description']);
Check translatability
$product->translatable(); // → true
$product->getTranslatableAttributes(); // → ['name', 'description']
🌱 Seeder Pattern (Bulk Insert)
The recommended way to seed translations — one query for everything:
Method 1 — bulkSeed (simplest)
use Emrane23\Translatable\Helpers\TranslationSeeder;
class ProductSeeder extends Seeder
{
public function run(): void
{
$p1 = Product::create(['name' => 'Ordinateur portable', 'price' => 999.99]);
$p2 = Product::create(['name' => 'Souris sans fil', 'price' => 29.99]);
TranslationSeeder::bulkSeed(Product::class, [
[
'id' => $p1->id,
'name' => ['fr' => 'Ordinateur portable', 'en' => 'Laptop', 'ar' => 'حاسوب محمول', 'es' => 'Portátil'],
'description' => ['fr' => 'Puissant et léger', 'en' => 'Powerful light', 'ar' => 'قوي وخفيف', 'es' => 'Potente y ligero'],
],
[
'id' => $p2->id,
'name' => ['fr' => 'Souris sans fil', 'en' => 'Wireless Mouse', 'ar' => 'فأرة لاسلكية', 'es' => 'Ratón inalámbrico'],
'description' => ['fr' => 'Ergonomique', 'en' => 'Ergonomic', 'ar' => 'مريح', 'es' => 'Ergonómico'],
],
], ['name', 'description']);
}
}
Method 2 — prepare + flush (multiple models at once)
use Emrane23\Translatable\Helpers\TranslationSeeder;
$translations = [];
// Prepare products
foreach ($products as $product) {
$translations = array_merge($translations,
TranslationSeeder::prepare('products', $product->id, 'name', [
'en' => 'Laptop',
'ar' => 'حاسوب محمول',
])
);
}
// Prepare rewards
foreach ($rewards as $reward) {
$translations = array_merge($translations,
TranslationSeeder::prepare('rewards', $reward->id, 'name', [
'en' => 'Gold Trophy',
'ar' => 'كأس ذهبي',
])
);
}
// Single bulk insert for everything! 🚀
TranslationSeeder::flush($translations);
🗄️ Database Structure
┌─────────────────────────────┐ ┌──────────────────────────────────────┐
│ products │ │ translations │
├─────────────────────────────┤ ├──────────────────────────────────────┤
│ id → 1 │────▶│ table_name → products │
│ name → "Ordi..." │ │ foreign_key → 1 │
│ (default FR) │ │ column_name → name │
│ ... │ │ locale → en │
└─────────────────────────────┘ │ value → "Laptop" │
└──────────────────────────────────────┘
Why this structure?
- Default language queries are native SQL — no joins needed → maximum performance
- Other languages are fetched only when needed → lazy by design
- One
translationstable for ALL models → simple schema - Automatic fallback chain:
requested locale → fallback locale → default column
🔧 Advanced Usage
Multiple translatable models
class Product extends Model
{
use Translatable;
protected $translatable = ['name', 'description'];
}
class Reward extends Model
{
use Translatable;
protected $translatable = ['name', 'description'];
}
class SeasonChallenge extends Model
{
use Translatable;
protected $translatable = ['title', 'description'];
}
All share the same translations table. Zero extra migrations needed.
🤝 Contributing
Contributions are welcome! Please read the contributing guide first.
git clone https://github.com/emrane23/laravel-translatable
cd laravel-translatable
composer install
composer test
📝 Changelog
Please see CHANGELOG for more information on what has changed recently.
🔒 License
The MIT License (MIT). Please see License File for more information.
👨💻 Author
Emrane Klaai
- GitHub: @Emrane23
- Built with 💖 from Tunisia 🇹🇳
"The best architecture is the one that solves real problems elegantly."