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.
php artisan boost:add-skill spatie/laravel-translatable
Save this content to: .claude/skills/translatable-development/SKILL.md
# 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.
```php
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:
```php
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.
```php
Schema::create('news_items', function (Blueprint $table) {
$table->id();
$table->json('name')->nullable();
$table->json('description')->nullable();
$table->timestamps();
});
```
## Setting translations
```php
// Current locale (via app()->getLocale())
$newsItem->name = 'Hello';
// Specific locale
$newsItem->setTranslation('name', 'en', 'Hello');
$newsItem->setTranslation('name', 'nl', 'Hallo');
// Multiple locales at once
$newsItem->setTranslations('name', ['en' => 'Hello', 'nl' => 'Hallo']);
// Mass assignment with a per-locale array
NewsItem::create([
'name' => ['en' => 'Hello', 'nl' => 'Hallo'],
]);
$newsItem->save();
```
## Getting translations
```php
// Current locale
$newsItem->name;
// Specific locale (falls back to the configured fallback locale by default)
$newsItem->getTranslation('name', 'nl');
// Without fallback
$newsItem->getTranslationWithoutFallback('name', 'nl');
// All translations for one attribute
$newsItem->getTranslations('name'); // ['en' => 'Hello', 'nl' => 'Hallo']
// All translations for all translatable attributes
$newsItem->translations;
// All locales this model has translations in
$newsItem->locales();
```
## Querying by locale
```php
// Models that have a translation for the given locale
NewsItem::query()->whereLocale('name', 'en')->get();
// Models that have a translation in any of the given locales
NewsItem::query()->whereLocales('name', ['en', 'nl'])->get();
// Filter by translated value
NewsItem::query()->whereJsonContainsLocale('name', 'en', 'Hello')->get();
```
## Forgetting translations
```php
$newsItem->forgetTranslation('name', 'nl'); // one locale of one attribute
$newsItem->forgetTranslations('name'); // all locales of one attribute
$newsItem->forgetAllTranslations('nl'); // one locale across all translatable attributes
```
## Fallback behavior
By default, `getTranslation()` falls back to `config('app.fallback_locale')` when a translation is missing. Override per model:
```php
class NewsItem extends Model
{
use HasTranslations;
public $useFallbackLocale = false; // disable fallback for this model
public function getFallbackLocale(): ?string
{
return 'en'; // custom fallback
}
}
```
## Testing
```php
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.
Translations are stored as JSON on the model itself. No extra tables, no pivot.
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.
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();
});
// Current locale (via app()->getLocale())
$newsItem->name = 'Hello';
// Specific locale
$newsItem->setTranslation('name', 'en', 'Hello');
$newsItem->setTranslation('name', 'nl', 'Hallo');
// Multiple locales at once
$newsItem->setTranslations('name', ['en' => 'Hello', 'nl' => 'Hallo']);
// Mass assignment with a per-locale array
NewsItem::create([
'name' => ['en' => 'Hello', 'nl' => 'Hallo'],
]);
$newsItem->save();
// Current locale
$newsItem->name;
// Specific locale (falls back to the configured fallback locale by default)
$newsItem->getTranslation('name', 'nl');
// Without fallback
$newsItem->getTranslationWithoutFallback('name', 'nl');
// All translations for one attribute
$newsItem->getTranslations('name'); // ['en' => 'Hello', 'nl' => 'Hallo']
// All translations for all translatable attributes
$newsItem->translations;
// All locales this model has translations in
$newsItem->locales();
// Models that have a translation for the given locale
NewsItem::query()->whereLocale('name', 'en')->get();
// Models that have a translation in any of the given locales
NewsItem::query()->whereLocales('name', ['en', 'nl'])->get();
// Filter by translated value
NewsItem::query()->whereJsonContainsLocale('name', 'en', 'Hello')->get();
$newsItem->forgetTranslation('name', 'nl'); // one locale of one attribute
$newsItem->forgetTranslations('name'); // all locales of one attribute
$newsItem->forgetAllTranslations('nl'); // one locale across all translatable attributes
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; // disable fallback for this model
public function getFallbackLocale(): ?string
{
return 'en'; // custom fallback
}
}
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');
});
json (or text) in the migration. A string column will throw on JSON casting.array or json cast on translatable columns in $casts. HasTranslations handles the JSON encoding.$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.How can I help you explore Laravel packages today?