| name | translatable-development |
| description | Use when working with spatie/laravel-translatable. Trigger when the query mentions translatable models, translations, multi-language attributes, the HasTranslations trait, the #[Translatable] attribute, or storing translations as JSON on Eloquent models. Tasks include making a model translatable, writing migrations for translatable columns, setting translations for specific locales, getting translations (with or without fallbacks), querying by locale, handling missing translations, and testing translatable behavior. |
| license | MIT |
| metadata | {"author":"spatie"} |
spatie/laravel-translatable
Translations are stored as JSON on the model itself. No extra tables, no pivot.
Making a model translatable
Declare translatable attributes via the #[Translatable] PHP attribute (preferred) and add the HasTranslations trait.
use Illuminate\Database\Eloquent\Model;
use Spatie\Translatable\Attributes\Translatable;
use Spatie\Translatable\HasTranslations;
#[Translatable('name', 'description')]
class NewsItem extends Model
{
use HasTranslations;
}
The attribute accepts a variadic list of column names.
The $translatable property is also supported:
class NewsItem extends Model
{
use HasTranslations;
public $translatable = ['name', 'description'];
}
When both are present, their values are merged and deduplicated.
Migration
Translatable columns must be json (or text if the database does not support json). One column per translatable attribute — the column holds all locales as a JSON object.
Schema::create('news_items', function (Blueprint $table) {
$table->id();
$table->json('name')->nullable();
$table->json('description')->nullable();
$table->timestamps();
});
Setting translations
$newsItem->name = 'Hello';
$newsItem->setTranslation('name', 'en', 'Hello');
$newsItem->setTranslation('name', 'nl', 'Hallo');
$newsItem->setTranslations('name', ['en' => 'Hello', 'nl' => 'Hallo']);
NewsItem::create([
'name' => ['en' => 'Hello', 'nl' => 'Hallo'],
]);
$newsItem->save();
Getting translations
$newsItem->name;
$newsItem->getTranslation('name', 'nl');
$newsItem->getTranslationWithoutFallback('name', 'nl');
$newsItem->getTranslations('name');
$newsItem->translations;
$newsItem->locales();
Querying by locale
NewsItem::query()->whereLocale('name', 'en')->get();
NewsItem::query()->whereLocales('name', ['en', 'nl'])->get();
NewsItem::query()->whereJsonContainsLocale('name', 'en', 'Hello')->get();
Forgetting translations
$newsItem->forgetTranslation('name', 'nl');
$newsItem->forgetTranslations('name');
$newsItem->forgetAllTranslations('nl');
Fallback behavior
By default, getTranslation() falls back to config('app.fallback_locale') when a translation is missing. Override per model:
class NewsItem extends Model
{
use HasTranslations;
public $useFallbackLocale = false;
public function getFallbackLocale(): ?string
{
return 'en';
}
}
Testing
use Spatie\Translatable\Facades\Translatable;
it('stores a translation', function () {
$newsItem = NewsItem::create(['name' => ['en' => 'Hello']]);
expect($newsItem->getTranslation('name', 'en'))->toBe('Hello');
});
it('falls back when a translation is missing', function () {
config()->set('app.fallback_locale', 'en');
$newsItem = NewsItem::create(['name' => ['en' => 'Hello']]);
expect($newsItem->getTranslation('name', 'nl'))->toBe('Hello');
});
Common pitfalls
- Translatable columns must be
json (or text) in the migration. A string column will throw on JSON casting.
- Setting a plain string on a translatable attribute only writes the current locale. Pass an associative array to set multiple locales at once.
- Do not register an
array or json cast on translatable columns in $casts. HasTranslations handles the JSON encoding.
- The
$translatable property and #[Translatable] attribute are class-level. Runtime mutations of $this->translatable work but are uncommon.
getTranslation() returns the fallback locale's value when the requested locale is missing. Use getTranslationWithoutFallback() for a strict lookup.