# Laravel Translatable Laravel Translatable is a package that provides a `HasTranslations` trait to make Eloquent models translatable in Laravel applications. Translations are stored as JSON directly in database columns, eliminating the need for separate translation tables. The package requires PHP 8.0+, Laravel 9+, and MySQL 5.7+ (or MariaDB 10.2.3+) for JSON column support. The package offers an intuitive API for setting and getting translations per locale, querying models based on translations, handling missing translations with fallbacks, and supporting nested JSON key translations. It automatically integrates with Laravel's locale system, so accessing a translated attribute returns the value for the current application locale, making internationalization seamless. ## Installation Install the package via Composer and configure your model with the `HasTranslations` trait. ```bash composer require spatie/laravel-translatable ``` ```php use Illuminate\Database\Eloquent\Model; use Spatie\Translatable\HasTranslations; class NewsItem extends Model { use HasTranslations; // Define which attributes should be translatable public array $translatable = ['name', 'description']; } ``` ## setTranslation Sets a translation for a specific attribute and locale. Returns the model instance for method chaining. The translation is stored in JSON format in the database column. ```php $newsItem = new NewsItem(); // Chain multiple translations and save $newsItem ->setTranslation('name', 'en', 'Name in English') ->setTranslation('name', 'nl', 'Naam in het Nederlands') ->setTranslation('name', 'fr', 'Nom en français') ->save(); // Setting translation directly via property (uses current app locale) app()->setLocale('es'); $newsItem->name = 'Nombre en español'; $newsItem->save(); ``` ## setTranslations Sets multiple translations for an attribute at once using an associative array of locale => value pairs. ```php $newsItem = new NewsItem(); // Set all translations at once $translations = [ 'en' => 'Hello World', 'nl' => 'Hallo Wereld', 'de' => 'Hallo Welt', 'fr' => 'Bonjour le monde' ]; $newsItem->setTranslations('name', $translations); $newsItem->save(); // Or assign array directly to the attribute $newsItem->name = [ 'en' => 'Breaking News', 'nl' => 'Belangrijk Nieuws' ]; $newsItem->save(); ``` ## getTranslation Retrieves a translation for a specific locale. Supports fallback to default locale when the requested translation doesn't exist. ```php $newsItem = NewsItem::first(); // Get translation for specific locale $englishName = $newsItem->getTranslation('name', 'en'); // Returns: 'Name in English' // Get translation with fallback enabled (default) $germanName = $newsItem->getTranslation('name', 'de', useFallbackLocale: true); // Returns fallback locale value if 'de' doesn't exist // Get translation without fallback $germanName = $newsItem->getTranslation('name', 'de', useFallbackLocale: false); // Returns empty string if 'de' doesn't exist // Access via property (uses current app locale) app()->setLocale('nl'); echo $newsItem->name; // Returns: 'Naam in het Nederlands' // Alias method $translation = $newsItem->translate('name', 'fr'); ``` ## getTranslations Returns all translations for an attribute or optionally filtered by specific locales. ```php $newsItem = NewsItem::first(); // Get all translations for a specific attribute $allNameTranslations = $newsItem->getTranslations('name'); // Returns: ['en' => 'Name in English', 'nl' => 'Naam in het Nederlands', 'fr' => 'Nom en français'] // Get translations for specific locales only $selectedTranslations = $newsItem->getTranslations('name', ['en', 'fr']); // Returns: ['en' => 'Name in English', 'fr' => 'Nom en français'] // Get all translations for all translatable attributes $allTranslations = $newsItem->getTranslations(); // Returns: ['name' => [...], 'description' => [...]] // Using the translations accessor $translations = $newsItem->translations; // Returns array of all translations for all translatable attributes ``` ## forgetTranslation Removes a translation for a specific attribute and locale. ```php $newsItem = NewsItem::first(); // Remove Dutch translation for name $newsItem->forgetTranslation('name', 'nl'); $newsItem->save(); // Verify removal $translations = $newsItem->getTranslations('name'); // Returns: ['en' => 'Name in English', 'fr' => 'Nom en français'] // Dutch translation is now removed ``` ## forgetAllTranslations Removes all translations for a specific locale across all translatable attributes. ```php $newsItem = NewsItem::first(); // Before: has translations in en, nl, fr for both 'name' and 'description' // Remove all Dutch translations from the model $newsItem->forgetAllTranslations('nl'); $newsItem->save(); // After: 'nl' translations removed from both 'name' and 'description' $newsItem->getTranslation('name', 'nl'); // Returns fallback or empty $newsItem->getTranslation('description', 'nl'); // Returns fallback or empty ``` ## replaceTranslations Replaces all existing translations for an attribute with new ones, removing any locales not in the new array. ```php $newsItem = NewsItem::first(); // Original translations $newsItem->setTranslations('name', [ 'en' => 'Hello', 'nl' => 'Hallo', 'de' => 'Hallo', 'fr' => 'Bonjour' ]); // Replace with only English and Spanish (removes nl, de, fr) $newsItem->replaceTranslations('name', [ 'en' => 'Hello World', 'es' => 'Hola Mundo' ]); $newsItem->save(); $newsItem->getTranslations('name'); // Returns: ['en' => 'Hello World', 'es' => 'Hola Mundo'] // Previous nl, de, fr translations are removed ``` ## hasTranslation Checks if a translation exists for a specific locale. ```php $newsItem = NewsItem::first(); // Check if English translation exists if ($newsItem->hasTranslation('name', 'en')) { echo "English translation available"; } // Check for current locale (if locale not specified) app()->setLocale('de'); if (!$newsItem->hasTranslation('name')) { echo "No German translation available"; } ``` ## locales Returns an array of all locales that have translations across all translatable attributes. ```php $newsItem = new NewsItem(); $newsItem->setTranslations('name', ['en' => 'Hello', 'nl' => 'Hallo']); $newsItem->setTranslations('description', ['en' => 'Desc', 'fr' => 'Desc Fr']); $newsItem->save(); $availableLocales = $newsItem->locales(); // Returns: ['en', 'nl', 'fr'] ``` ## getTranslatedLocales Returns the locales that have translations for a specific attribute. ```php $newsItem = NewsItem::first(); $localesWithName = $newsItem->getTranslatedLocales('name'); // Returns: ['en', 'nl', 'fr'] $localesWithDescription = $newsItem->getTranslatedLocales('description'); // Returns: ['en', 'de'] ``` ## Nested JSON Key Translations Translate nested keys within JSON columns using the `->` notation. ```php use Illuminate\Database\Eloquent\Model; use Spatie\Translatable\HasTranslations; class Product extends Model { use HasTranslations; // Add nested keys to translatable array public array $translatable = ['name', 'meta->description', 'meta->keywords']; } $product = new Product(); // Set nested translations $product ->setTranslation('meta->description', 'en', 'Product description in English') ->setTranslation('meta->description', 'nl', 'Productbeschrijving in het Nederlands') ->setTranslation('meta->keywords', 'en', 'shoes, running, sports') ->setTranslation('meta->keywords', 'nl', 'schoenen, hardlopen, sport') ->save(); // Access nested translations $attributeKey = 'meta->description'; echo $product->$attributeKey; // Returns translation for current locale echo $product->getTranslation('meta->description', 'nl'); // Returns: 'Productbeschrijving in het Nederlands' ``` ## whereLocale Query Scope Query models that have a translation for a specific locale. ```php // Get all news items that have an English name translation $itemsWithEnglish = NewsItem::whereLocale('name', 'en')->get(); // Combine with other query conditions $recentEnglishItems = NewsItem::whereLocale('name', 'en') ->where('published', true) ->orderBy('created_at', 'desc') ->get(); ``` ## whereLocales Query Scope Query models that have translations in any of the specified locales. ```php // Get all news items with name in English OR Dutch $items = NewsItem::whereLocales('name', ['en', 'nl'])->get(); // Get items with translations in multiple European languages $europeanItems = NewsItem::whereLocales('name', ['en', 'de', 'fr', 'nl', 'es']) ->where('region', 'europe') ->get(); ``` ## whereJsonContainsLocale Query Scope Query models where a specific locale has a specific value. ```php // Find items where English name equals exactly 'Breaking News' $items = NewsItem::whereJsonContainsLocale('name', 'en', 'Breaking News')->get(); // Use LIKE operator for partial matching $items = NewsItem::whereJsonContainsLocale('name', 'en', 'Breaking%', 'like')->get(); // Combine with other conditions $items = NewsItem::whereJsonContainsLocale('name', 'en', 'News%', 'like') ->where('published', true) ->orderBy('created_at', 'desc') ->get(); ``` ## whereJsonContainsLocales Query Scope Query models where any of the specified locales contains a specific value. ```php // Find items where English OR Dutch name contains 'News' $items = NewsItem::whereJsonContainsLocales('name', ['en', 'nl'], 'News%', 'like')->get(); // Exact match in multiple locales $items = NewsItem::whereJsonContainsLocales('name', ['en', 'de', 'fr'], 'Hello World')->get(); ``` ## Direct JSON Querying Query translations using MySQL JSON syntax directly. ```php // MySQL 5.7+ syntax $items = NewsItem::where('name->en', 'Name in English')->get(); // Multiple conditions $items = NewsItem::where('name->en', 'like', '%News%') ->where('description->en', '!=', '') ->get(); // MariaDB syntax $items = NewsItem::whereRaw("JSON_EXTRACT(name, '$.en') = 'Name in English'")->get(); ``` ## setLocale and usingLocale Override the locale for a specific model instance without changing the application locale. ```php // Set locale on existing model instance $newsItem = NewsItem::first(); $newsItem->setLocale('de'); echo $newsItem->name; // Returns German translation // Create model instance with specific locale $newsItem = NewsItem::usingLocale('fr')->first(); echo $newsItem->name; // Returns French translation // Chain with other operations $newsItem = NewsItem::usingLocale('es') ->where('published', true) ->first(); ``` ## Fallback Configuration Configure fallback behavior for missing translations using the Translatable facade in a service provider. ```php // In a service provider (e.g., AppServiceProvider) use Spatie\Translatable\Facades\Translatable; public function boot() { // Set a specific fallback locale Translatable::fallback( fallbackLocale: 'en', ); // Allow falling back to any available translation Translatable::fallback( fallbackAny: true, ); // Custom callback when translation is missing Translatable::fallback( missingKeyCallback: function ( $model, string $translationKey, string $locale, string $fallbackTranslation, string $fallbackLocale ) { Log::warning("Missing translation", [ 'key' => $translationKey, 'locale' => $locale, 'model' => get_class($model), 'model_id' => $model->id, ]); // Optionally return a custom fallback string return "Translation missing for {$locale}"; } ); } ``` ## Model-Level Fallback Configuration Define fallback behavior directly on the model. ```php use Illuminate\Database\Eloquent\Model; use Spatie\Translatable\HasTranslations; class NewsItem extends Model { use HasTranslations; public $fillable = ['name', 'fallback_locale']; public array $translatable = ['name']; // Disable fallback for this model protected $useFallbackLocale = false; // Or define custom fallback locale per model instance public function getFallbackLocale(): string { return $this->fallback_locale ?? 'en'; } } // Usage $item = new NewsItem(); $item->fallback_locale = 'de'; $item->save(); // This model will fallback to German instead of app default ``` ## Creating Models with Translations Set translations directly when creating models. ```php // Create with translations array $newsItem = NewsItem::create([ 'name' => [ 'en' => 'Breaking News', 'nl' => 'Belangrijk Nieuws', 'de' => 'Aktuelle Nachrichten' ], 'description' => [ 'en' => 'This is the description', 'nl' => 'Dit is de beschrijving' ], 'published' => true, ]); // Or using firstOrCreate $item = NewsItem::firstOrCreate( ['slug' => 'my-article'], [ 'name' => ['en' => 'My Article', 'nl' => 'Mijn Artikel'], 'description' => ['en' => 'Article content'] ] ); ``` ## Factory Helper for Testing Use the translations helper in model factories for testing. ```php use Illuminate\Database\Eloquent\Factories\Factory; class NewsItemFactory extends Factory { public function definition(): array { return [ // Single locale 'name' => $this->translations('en', 'English Name'), // Output: ['en' => 'English Name'] // Multiple locales with same value 'slug' => $this->translations(['en', 'nl'], 'my-slug'), // Output: ['en' => 'my-slug', 'nl' => 'my-slug'] // Multiple locales with different values 'description' => $this->translations( ['en', 'nl'], ['English description', 'Dutch description'] ), // Output: ['en' => 'English description', 'nl' => 'Dutch description'] ]; } } // Usage in tests $newsItem = NewsItem::factory()->create(); // Outside factories $translations = Factory::translations(['en', 'de'], ['Hello', 'Hallo']); ``` ## TranslationHasBeenSetEvent An event is fired whenever a translation is set, allowing you to hook into translation changes. ```php use Spatie\Translatable\Events\TranslationHasBeenSetEvent; // In EventServiceProvider protected $listen = [ TranslationHasBeenSetEvent::class => [ TranslationChangeListener::class, ], ]; // Listener implementation class TranslationChangeListener { public function handle(TranslationHasBeenSetEvent $event): void { Log::info('Translation updated', [ 'model' => get_class($event->model), 'model_id' => $event->model->id, 'key' => $event->key, 'locale' => $event->locale, 'old_value' => $event->oldValue, 'new_value' => $event->newValue, ]); // Trigger cache invalidation, sync to external service, etc. } } ``` ## Custom toArray Method Customize how translated attributes are serialized when converting the model to an array or JSON. ```php namespace App\Traits; use Spatie\Translatable\HasTranslations as BaseHasTranslations; trait HasTranslations { use BaseHasTranslations; public function toArray(): array { $attributes = $this->attributesToArray(); // Filter translatable attributes to only those selected by query $translatables = array_filter( $this->getTranslatableAttributes(), fn($key) => array_key_exists($key, $attributes) ); // Replace JSON translations with current locale value foreach ($translatables as $field) { $attributes[$field] = $this->getTranslation($field, app()->getLocale()); } return array_merge($attributes, $this->relationsToArray()); } } // Usage: API responses will return translated strings instead of JSON objects // GET /api/news/1 with locale=nl // Returns: {"name": "Naam in het Nederlands", ...} ``` ## Summary Laravel Translatable is ideal for applications requiring multilingual content management such as CMSs, e-commerce platforms, blogs, and any Laravel application serving international audiences. The package excels in scenarios where you need to store translations without database schema changes, query content by locale, and provide seamless locale-switching for users. Integration follows Laravel conventions with a simple trait-based approach. Models only need the `HasTranslations` trait and a `$translatable` array to define which attributes support translations. The package works seamlessly with Eloquent's query builder, JSON column casting, model events, and factories, making it a drop-in solution for adding internationalization to existing Laravel applications with minimal refactoring.