astrotomic/laravel-translatable
Laravel package for translatable Eloquent models. Store model translations in the database and automatically fetch/save multilingual attributes based on locale, reducing boilerplate when working with multi-language content.
Installation:
composer require astrotomic/laravel-translatable
php artisan vendor:publish --tag=translatable
Configure locales in config/translatable.php (e.g., ['en', 'fr']).
Database Migration:
Create a translation table (e.g., post_translations) with:
id, post_id (foreign key), locale, and translated fields (e.g., title).[post_id, locale].Model Setup:
// Post.php
use Astrotomic\Translatable\Translatable;
class Post extends Model
{
use Translatable;
public $translatedAttributes = ['title', 'content'];
protected $fillable = ['author'];
}
// PostTranslation.php
class PostTranslation extends Model
{
public $timestamps = false;
protected $fillable = ['title', 'content'];
}
Retrieve and update translations dynamically:
// Fetch a post and access translations
$post = Post::find(1);
echo $post->translate('en')->title; // English title
// Update a translation
$post->translate('en')->title = 'Updated Title';
$post->save();
// Create with translations
$post = Post::create([
'author' => 'John Doe',
'en' => ['title' => 'Hello'],
'fr' => ['title' => 'Bonjour'],
]);
App::setLocale() to switch contexts:
App::setLocale('fr');
echo $post->title; // Automatically fetches French translation
fallback_locale in config/translatable.php (e.g., 'en') to default to English if a translation is missing.translations_wrapper config (e.g., 'translations_wrapper' => 'translations') to nest translations:
$post = Post::create([
'author' => 'Jane Doe',
'translations' => [
'en' => ['title' => 'Hello'],
'fr' => ['title' => 'Bonjour'],
],
]);
$post->translate('en')->title = 'New Title';
$post->translate('fr')->title = 'Nouveau Titre';
$post->save(); // Saves all translations at once
whereHas with translate():
$posts = Post::whereHas('translate', function ($query) {
$query->where('title', 'like', '%Hello%');
})->get();
$posts = Post::with(['translate' => function ($query) {
$query->where('locale', 'en');
}])->get();
$clone = $post->replicateWithTranslations();
$clone->save();
getTranslationsArray() for API responses:
return response()->json($post->getTranslationsArray());
Missing Fallback Locale:
fallback_locale is not set, translate('it') returns null instead of falling back to en.fallback_locale in config/translatable.php.Translation Not Saved:
$post->save() after updating translations.Locale Mismatch:
config/translatable.php (e.g., 'it' when only ['en', 'fr'] are configured).Single Table Inheritance (STI):
ChildPost), explicitly set $translationForeignKey to avoid ambiguity:
protected $translationForeignKey = 'post_id';
Mass Assignment Risks:
$fillable by default. Explicitly whitelist them in PostTranslation:
protected $fillable = ['title', 'content'];
Check Translation Existence:
Use hasTranslation() to verify:
if (!$post->hasTranslation('fr')) {
// Create or handle missing translation
}
Inspect Translation Data: Dump the translations array:
dd($post->getTranslationsArray());
Query Debugging:
Add ->toSql() to debug whereHas queries:
$query = Post::whereHas('translate', function ($q) {
return $q->where('title', 'like', '%Hello%')->toSql();
});
Custom Translation Model:
Override the default PostTranslation by setting $translationModel in the parent model:
protected $translationModel = CustomPostTranslation::class;
Dynamic Translated Attributes: Use a getter to dynamically define translated attributes:
public function getTranslatedAttributes()
{
return ['title', 'content', 'description'];
}
Event Hooks:
Listen for translatable.saving or translatable.saved events to log or validate translations:
Event::listen('translatable.saving', function ($model) {
// Custom logic before save
});
Custom Storage:
Extend the package to support non-database storage (e.g., Redis) by overriding the getTranslation() method.
Eager Loading: Always eager load translations to avoid N+1 queries:
$posts = Post::with(['translate' => function ($query) {
$query->whereIn('locale', ['en', 'fr']);
}])->get();
Indexing:
Ensure locale and post_id are indexed in the translations table for faster lookups.
Caching: Cache translations for read-heavy applications:
$translation = Cache::remember("post.{$post->id}.{$locale}", now()->addHours(1), function () use ($post, $locale) {
return $post->translate($locale);
});
Default Locale:
The default_locale in config/translatable.php is per-model. Override it dynamically:
$post->setDefaultLocale('fr');
Translations Wrapper:
If using translations_wrapper, ensure the input array matches the expected structure:
// Correct:
'translations' => ['en' => ['title' => 'Hello']]
// Incorrect:
'en' => ['title' => 'Hello'] // Only works without wrapper
Locale Format:
The package supports custom locale formats (e.g., 'eng' instead of 'en'), but stick to one format across the app to avoid inconsistencies.
How can I help you explore Laravel packages today?