flobbos/laravel-translatable-db
Store model translations in a dedicated database table with Laravel. flobbos/laravel-translatable-db adds a simple translatable trait, helpers, and query support so you can persist and retrieve localized fields cleanly without JSON columns.
Installation Requires Laravel 10+ and PHP 8.1+. Install via Composer:
composer require flobbos/laravel-translatable-db
Publish the config and migrations:
php artisan vendor:publish --provider="Flobbos\LaravelTranslatable\LaravelTranslatableServiceProvider"
php artisan migrate
Basic Model Setup
Extend your model with Flobbos\LaravelTranslatable\Translatable trait and define translatable fields in $translatable:
use Flobbos\LaravelTranslatable\Translatable;
class Post extends Model
{
use Translatable;
protected $translatable = ['title', 'content'];
}
First Usage Create a post with translations:
$post = Post::create([
'title' => 'English Title',
'content' => 'English Content',
]);
$post->translate('es', [
'title' => 'Título en Español',
'content' => 'Contenido en Español',
]);
Retrieving Translations Fetch a translation by locale:
$post->getTranslation('es')->title; // "Título en Español"
Or use the localize() helper:
$post->localize('es')->title; // Same result
Dynamic Translation Handling
Use translatable() to dynamically add/remove translatable fields:
$model->translatable(['new_field']); // Add
$model->translatable(null); // Reset
Locale Fallbacks
Configure fallbacks in config/translatable.php (now with stricter defaults):
'fallback_locale' => 'en',
'fallbacks' => [
'es' => 'en',
'fr' => 'en',
],
Then fetch translations with fallback:
$post->localize('es')->title; // Falls back to 'en' if 'es' missing
Scopes for Localized Queries
Use the translated() scope to filter by translations:
Post::translated('title', 'like', '%English%')->get();
API Responses
Note: to_array_always_loads_translations is now false by default. Explicitly eager-load translations:
return Post::withTranslations()->get();
Form Requests Validate and bind translations in form requests:
public function rules()
{
return [
'title' => 'required',
'translations.es.title' => 'required',
];
}
public function prepareForValidation()
{
$this->merge([
'translations' => json_decode($this->input('translations'), true),
]);
}
toSearchableArray() to include translations.withTranslations() in queries for admin panels.translatable.* rules for nested validation.middleware_default is now false. Manually add middleware if needed:
Route::middleware(['translatable'])->group(function () {
// Routes requiring translation middleware
});
Migration Conflicts
translations table, ensure it matches the package’s schema:
Schema::create('translations', function (Blueprint $table) {
$table->id();
$table->string('locale')->index();
$table->json('attributes');
$table->foreignId('translatable_id')->constrained()->onDelete('cascade');
$table->unique(['translatable_id', 'locale']);
});
Mass Assignment Risks
$fillable or use $guarded.protected $fillable = ['title', 'content'];
Locale-Specific Queries
JSON Field Limitations
attributes column uses JSON, which may cause issues with very large translations. Consider splitting into separate columns if needed.Model Events
savingTranslation, savedTranslation, etc. Listen carefully to avoid infinite loops:
$model->setAttribute('title', 'New Title');
$model->save(); // Triggers translation events
Middleware Defaults
class Post extends Model
{
use Translatable;
protected $middleware = ['translatable'];
}
Check for Missing Translations
Use hasTranslation('es') to verify:
if (!$post->hasTranslation('es')) {
// Handle missing translation
}
Inspect Raw Translations
Dump the translations relationship:
dd($post->translations);
Enable Query Logging
Temporarily enable debug mode in config/translatable.php:
'debug' => true,
Clear Cached Compiled Views If translations aren’t updating, run:
php artisan view:clear
Custom Storage
Override getTranslationsTable() in your model:
protected function getTranslationsTable()
{
return 'custom_translations';
}
Custom Attribute Handling Extend the trait to modify how attributes are stored:
use Flobbos\LaravelTranslatable\Translatable as BaseTranslatable;
trait CustomTranslatable
{
use BaseTranslatable;
protected function getTranslatableAttributes()
{
return array_merge($this->$translatable, ['custom_field']);
}
}
Add Scopes Extend the query builder in your model:
public function scopeActiveTranslations($query)
{
return $query->whereHas('translations', function ($q) {
$q->where('locale', app()->getLocale());
});
}
Custom Serialization
Override toArray() or toJson() to format translations:
public function toArray()
{
return array_merge(parent::toArray(), [
'translations' => $this->translations->pluck('attributes', 'locale'),
]);
}
Non-ID Primary Keys Fixed in 2.0.0: Ensure foreign key detection works for non-id primary keys by explicitly defining:
public function getTranslatableKey()
{
return $this->uuid; // If using UUID as primary key
}
withTranslations() when fetching models with translations.locale and translatable_id are indexed in the translations table.updateTranslations() for bulk updates:
$posts->each->updateTranslations('es', ['title' => 'Updated']);
toArray(). Use withTranslations() or explicitly load them:
$post->load('translations');
How can I help you explore Laravel packages today?