A simple, elegant and powerful translation package for Laravel
emrane23/laravel-translatable is a Laravel package for a simple, elegant and powerful translation package for laravel.
It currently has 0 GitHub stars and 23 downloads on Packagist (latest version v1.0.9).
Install it with composer require emrane23/laravel-translatable.
Discover more Laravel packages by emrane23
or browse all Laravel packages to compare alternatives.
Last updated
Zero-config translations for Laravel — SPA, monolith, and everything in between.
Most translation packages store all languages in separate columns or JSON fields. This package takes a different approach:
translations table — clean and scalable$model->name, it returns the right language automaticallyThis package works seamlessly across all Laravel architectures:
The middleware locale source is fully configurable — you pick the detection mechanism that fits your project.
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"
// config/translatable.php
return [
'default_locale' => env('APP_LOCALE', 'fr'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'supported_locales' => ['fr', 'en', 'ar', 'es'],
// Pick the one that matches your project — see Middleware section below
'locale_source' => 'header',
];
APP_LOCALE=fr
APP_FALLBACK_LOCALE=en
use Emrane23\Translatable\Traits\Translatable;
class Product extends Model
{
use Translatable;
protected $fillable = ['name', 'description', 'price'];
protected $translatable = ['name', 'description'];
}
// App locale is 'fr' → returns "Ordinateur portable" (from column, no join)
// 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;
No controller changes. No view changes. It just works.
The TranslationMiddleware detects the current locale automatically from the source you configure.
An InvalidArgumentException is thrown if an invalid source value is provided.
bootstrap/app.phpuse Emrane23\Translatable\Middleware\TranslationMiddleware;
->withMiddleware(function (Middleware $middleware) {
$middleware->appendToGroup('api', TranslationMiddleware::class);
})
app/Http/Kernel.phpuse Emrane23\Translatable\Middleware\TranslationMiddleware;
protected $middlewareGroups = [
'api' => [
TranslationMiddleware::class,
],
];
| Value | How it works | Best for |
|---|---|---|
| header | Reads X-Locale request header | SPA, API, mobile |
| query | Reads ?locale= URL parameter | Direct URLs, emails |
| session | Reads session()->get('locale') | Classic monolith |
| cookie | Reads cookie('locale') | Persistent preference |
| user | Reads from authenticated user | Per-user preference |
// config/translatable.php
'locale_source' => 'header', // SPA / API
'locale_source' => 'session', // Classic monolith
'locale_source' => 'cookie', // Persistent preference
'locale_source' => 'query', // URL parameter
'locale_source' => 'user', // Per-user preference
axios.defaults.headers.common['X-Locale'] = 'ar';
preferredLocale()The user source uses preferredLocale() if available (Laravel's HasLocalePreference interface),
then falls back to a direct locale attribute. You can adapt it to your own mechanism:
// Option 1 — HasLocalePreference (recommended)
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Authenticatable implements HasLocalePreference
{
public function preferredLocale(): string
{
return $this->locale ?? config('app.locale');
}
}
// Option 2 — Custom attribute (adapt to your own mechanism)
public function getLocaleAttribute(): string
{
return $this->settings['language'] ?? config('app.locale');
}
Implement HasLocalePreference on your User model to send emails in each 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.
// Current app locale (automatic)
$product->name;
// Explicit locale
$product->getTranslatedAttribute('name', 'en');
// Without fallback
$product->getTranslatedAttribute('name', 'es', false);
// With full metadata — returns [value, locale_used, found]
[$value, $locale, $found] = $product->getTranslatedAttributeMeta('name', 'en');
$product->setAttributeTranslations('name', [
'fr' => 'Ordinateur portable', // saved to column directly
'en' => 'Laptop', // saved to translations table
'ar' => 'حاسوب محمول',
'es' => 'Portátil',
]);
// Save immediately
$product->setAttributeTranslations('name', ['en' => 'Laptop'], save: true);
Product::withTranslation()->get(); // current locale + fallback
Product::withTranslation('en')->get(); // specific locale
Product::withTranslation('en', false)->get(); // no fallback
For large applications with heavy traffic, eager load translations globally:
// In your model
protected $with = ['translations'];
$product->deleteAttributeTranslation('name', 'en');
$product->deleteAttributeTranslation('name', ['en', 'es']);
$product->deleteAttributeTranslations(['name', 'description'], ['en', 'es']);
$product->deleteAttributeTranslations(['name', 'description']); // all locales
$product->translatable(); // true
$product->getTranslatableAttributes(); // ['name', 'description']
bulkSeedThe simplest way. One bulk query for all translations.
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']);
}
}
prepare + flushUseful when seeding multiple models in one shot.
$translations = [];
foreach ($products as $product) {
$translations = array_merge($translations,
TranslationSeeder::prepare('products', $product->id, 'name', [
'en' => 'Laptop',
'ar' => 'حاسوب محمول',
])
);
}
foreach ($rewards as $reward) {
$translations = array_merge($translations,
TranslationSeeder::prepare('rewards', $reward->id, 'name', [
'en' => 'Gold Trophy',
'ar' => 'كأس ذهبي',
])
);
}
TranslationSeeder::flush($translations); // single query for everything
┌──────────────────────┐ ┌──────────────────────────────────────┐
│ products │ │ translations │
├──────────────────────┤ ├──────────────────────────────────────┤
│ id → 1 │────▶│ table_name → products │
│ name → "Ordi..." │ │ foreign_key → 1 │
│ (default locale) │ │ column_name → name │
└──────────────────────┘ │ locale → en │
│ value → "Laptop" │
└──────────────────────────────────────┘
The default language is stored directly in the model column — no joins needed for the most common case.
Other languages are fetched only when requested. One translations table serves all your models
with no extra migrations needed.
Fallback chain: requested locale → fallback locale → default column
All models share the same translations table:
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'];
}
git clone https://github.com/Emrane23/laravel-translatable
cd laravel-translatable
composer install
composer test
See CHANGELOG for recent changes.
MIT. See LICENSE.
Emrane Klaai — @Emrane23 — Built from Tunisia
"The best architecture is the one that solves real problems elegantly."