novius/laravel-translatable
Make Laravel Eloquent models translatable using locale and locale_parent_id. Includes migration macro, Translatable trait, relations for translations (with/without soft-deleted), translate() and getTranslation(), plus withLocale() query scope. Supports Laravel 10–13, PHP 8.2–8.5.
Installation:
composer require novius/laravel-translatable
Publish language files (optional):
php artisan vendor:publish --provider="Novius\LaravelTranslatable\LaravelTranslatableServiceProvider" --tag=lang
Migration Setup:
Add the translatable() macro to your table schema:
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->translatable(); // Adds `locale` and `locale_parent_id` columns
$table->string('title');
$table->text('content');
$table->timestamps();
});
Model Integration:
Use the Translatable trait in your Eloquent model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Novius\LaravelTranslatable\Traits\Translatable;
class Post extends Model
{
use Translatable;
}
First Translation: Create a model instance and translate it to a new locale:
$post = new Post(['title' => 'Titre en Français']);
$post->save();
$post->translate('en', ['title' => 'English Title']);
translatable() macro for column requirements.translate(), getTranslation(), and translations relation for core functionality.Localizing a Blog Post:
// Create a post in French
$post = Post::create(['title' => 'Mon Article']);
// Add English translation
$post->translate('en', ['title' => 'My Article']);
// Retrieve English translation
$englishPost = $post->getTranslation('en');
// Query posts with English translations
$englishPosts = Post::withLocale('en')->get();
Basic Translation Workflow:
getTranslation($locale) or translations relation.translate() method.SoftDeletes) require handling translationsWithDeleted.Querying Translations:
withLocale($locale) for query builder:
$posts = Post::withLocale('es')->get();
with('translations'):
$posts = Post::with('translations')->get();
Customizing Behavior:
translatableConfig():
public function translatableConfig(): TranslatableModelConfig
{
return new TranslatableModelConfig(
['en', 'fr', 'es'], // Allowed locales
'language_code', // Custom locale column name
'parent_id' // Custom parent ID column name
);
}
translateAttributes():
protected function translateAttributes($parent): void
{
$this->slug = Str::slug($parent->title . ' ' . $this->locale);
}
Handling Fallbacks:
getTranslation() or use a service layer:
public function getFallbackTranslation(Post $post, string $locale)
{
return $post->getTranslation(config('app.fallback_locale')) ?? $post;
}
Content Management System (CMS):
translatable for localized pages, menus, or media.Page::with('translations')->find($id) to fetch all language versions.E-Commerce:
$product = Product::find($id);
$product->translate('de', ['name' => 'Deutscher Name']);
User-Generated Content:
$comment = new Comment(['body' => 'Comment in User\'s Locale']);
$comment->translate($user->locale, ['body' => $translatedBody]);
API Resources:
public function toArray($request)
{
return [
'id' => $this->id,
'translations' => $this->translations->map(fn($t) => [
'locale' => $t->locale,
'title' => $t->title,
]),
];
}
Form Requests:
public function rules()
{
return [
'title_en' => 'required|string',
'title_fr' => 'required|string',
];
}
Observers:
Post::observe(TranslatableObserver::class);
class TranslatableObserver
{
public function saved(Post $post)
{
if ($post->wasRecentlyCreated) {
$post->translate('en', $post->attributes);
}
}
}
Testing:
public function testTranslationCreation()
{
$post = Post::create(['title' => 'Original']);
$post->translate('es', ['title' => 'Traducción']);
$this->assertDatabaseHas('posts', [
'title' => 'Traducción',
'locale' => 'es',
'locale_parent_id' => $post->id,
]);
}
Table Bloat:
Orphaned Translations:
SoftDeletes.delete() to handle translations:
public function delete()
{
$this->translations->each->delete();
parent::delete();
}
Locale Conflicts:
locale_parent_id values can cause ambiguity.locale_parent_id is unique per locale or use composite keys.Query Performance:
with('translations') can be slow for large datasets.with(['translations' => function($query) { $query->where('locale', $locale); }]) to limit translations.Soft Deletes:
translationsWithDeleted may return soft-deleted records unexpectedly.$translations = $post->translationsWithDeleted()->whereNull('deleted_at');
Missing Translations:
$translation = Post::where('locale_parent_id', $post->id)
->where('locale', 'es')
->first();
translate() is called with correct attributes.Locale Parent ID Mismatch:
locale_parent_id points to the correct parent:
$parent = Post::find($post->locale_parent_id);
locale_parent_id if corrupted:
$post->translations->each(function($t) {
$t->locale_parent_id = $post->id;
$t->save();
});
Query Builder Issues:
toSql() to inspect generated queries:
$query = Post::withLocale('fr');
dd($query->toSql(), $query->getBindings());
withLocale scope is correctly implemented in the trait.Default Locale:
AppServiceProvider:
public function boot()
{
Translatable::setDefaultLocale('en');
}
Column Naming:
locale, locale_parent_id) may conflict with existing columns.translatableConfig() to use custom names.Attribute Whitelisting:
How can I help you explore Laravel packages today?