astrotomic/laravel-translatable
Laravel package for translatable Eloquent models. Store model translations in the database and automatically fetch/save the correct locale with minimal code. Simplifies retrieving and persisting multilingual attributes across your app.
composer require astrotomic/laravel-translatable
php artisan vendor:publish --tag=translatable
config/translatable.php:
'locales' => ['en', 'fr', 'de'],
post_translations):
Schema::create('post_translations', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->string('locale')->index();
$table->string('title');
$table->text('content');
$table->unique(['post_id', 'locale']);
});
use Astrotomic\Translatable\Translatable;
class Post extends Model
{
use Translatable;
public $translatedAttributes = ['title', 'content'];
protected $fillable = ['author'];
}
$post = Post::find(1);
echo $post->translate('en')->title; // English title
App::setLocale('fr');
echo $post->title; // French title (auto-fallback)
$post = Post::create([
'author' => 'John Doe',
'en' => ['title' => 'Hello'],
'fr' => ['title' => 'Bonjour'],
]);
$post->translate('en')->title = 'Updated Title';
$post->save();
$post->fillTranslations([
'en' => ['title' => 'New Title'],
'fr' => ['title' => 'Nouveau Titre'],
]);
$post->save();
$post->translate('it'); // Returns null if 'it' missing
$post->translate('it', true); // Falls back to default locale
$post->setDefaultLocale('fr');
$post->translate()->title; // Uses 'fr' by default
$posts = Post::whereTranslation('title', 'like', '%Hello%')->get();
$posts = Post::withTranslation('en')->get();
$clone = $post->replicateWithTranslations();
$clone->save();
$post->toArray(); // Includes translations in nested structure
$post->getTranslationsArray(); // Returns flat array
Missing Locale Config:
config/translatable.php. Undefined locales will cause errors.$post->translate('es', true); // Force fallback
Translation Not Saved:
$model->save() after updating translations.fillTranslations() + save() or leverage model events.STI (Single Table Inheritance) Issues:
child_post_id) require explicit configuration:
protected $translationForeignKey = 'post_id';
Mass Assignment Risks:
$fillable if not guarded.$translatedAttributes.Performance with Large Datasets:
withTranslation()) avoids N+1 queries but increases memory usage.with(['translation' => function($query) { ... }]) for scoped queries.Check Translation Existence:
if (!$post->hasTranslation('es')) {
// Handle missing translation
}
Inspect Translation Data:
dd($post->getTranslationsArray());
Log Fallback Behavior:
$post->translate('it', true); // Logs fallback to default locale
Validate Locale Format:
'es_MX' vs. 'es'). Use config('translatable.locales') to verify.Custom Translation Models:
PostTranslation class by setting $translationModel in the model:
protected $translationModel = CustomPostTranslation::class;
Dynamic Attribute Handling:
Translatable trait to add custom logic for specific attributes:
use Astrotomic\Translatable\Translatable as BaseTranslatable;
class Post extends Model
{
use BaseTranslatable {
translate as private baseTranslate;
}
public function translate($locale = null)
{
$translation = $this->baseTranslate($locale);
if ($translation && $locale === 'en') {
$translation->title = strtoupper($translation->title);
}
return $translation;
}
}
Event Hooks:
translatable.saving or translatable.saved events to modify translations before/after save:
Event::listen('translatable.saving', function ($model) {
$model->translate('en')->title = 'Prefix: ' . $model->translate('en')->title;
});
Custom Query Scopes:
public function scopeWithTitleLike($query, $search)
{
return $query->whereHas('translation', function($q) use ($search) {
$q->where('title', 'like', "%{$search}%");
});
}
Localization Middleware:
public function handle($request, Closure $next)
{
$locale = $request->input('locale', config('app.locale'));
App::setLocale($locale);
return $next($request);
}
translations_wrapper:
'translations_wrapper' => 'i18n', // Accepts `i18n.en.title`
fill() to recognize the wrapper.fallback_locale:
protected $fallbackLocale = 'en';
disableOriginalAttributeSync:
title) when translations are updated:
'disableOriginalAttributeSync' => true,
disableDefaultLocaleSync:
'disableDefaultLocaleSync' => true,
Cache Translations:
getTranslationsArray() for read-heavy applications:
$translations = Cache::remember("post.{$post->id}.translations", now()->addHours(1), function() use ($post) {
return $post->getTranslationsArray();
});
Selective Loading:
$post->loadTranslation('en', ['title']); // Loads only 'title' for 'en'
Database Indexes:
locale and post_id are indexed in the translations table for faster lookups.Batch Operations:
updateTranslations() for bulk updates:
Post::where('author', 'John')->updateTranslations([
'en' => ['title' => 'Batch Updated'],
]);
How can I help you explore Laravel packages today?