spatie/nova-translatable
Make any Laravel Nova field translatable with a simple Translatable wrapper. Works with spatie/laravel-translatable to store per-locale values in a JSON column, rendering locale tabs for editing. Requires Nova 4/5 and MySQL 5.7.8+.
Installation:
composer require spatie/nova-translatable
Publish the config (if needed):
php artisan vendor:publish --provider="Spatie\NovaTranslatable\NovaTranslatableServiceProvider"
First Use Case:
Wrap any Nova field(s) in Translatable::make() within your Nova resource's fields() method:
use Spatie\NovaTranslatable\Translatable;
public function fields(Request $request)
{
return [
ID::make(),
Translatable::make([
Text::make('title', 'Title'),
Text::make('description', 'Description'),
]),
];
}
Database Migration:
Ensure your model uses Spatie\Translatable\HasTranslations trait and has a translations table. Example migration:
php artisan make:migration create_post_translations_table
Schema::create('post_translations', function (Blueprint $table) {
$table->string('locale')->index();
$table->text('title')->nullable();
$table->text('description')->nullable();
$table->unsignedBigInteger('post_id');
$table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
});
Model Setup:
use Spatie\Translatable\HasTranslations;
class Post extends Model
{
use HasTranslations;
public $translatable = ['title', 'description'];
}
Basic Translation Handling:
Use Translatable::make() to group fields that need translation. The package automatically handles:
translations table.Translatable::make([
Text::make('name'),
Textarea::make('bio'),
BelongsTo::make('Author'), // Non-translatable fields can coexist
]);
Conditional Translations:
Use onlyOnDetail() or onlyOnIndex() to control when translations appear:
Translatable::make([
Text::make('title'),
])->onlyOnDetail();
Default Locale:
Set a default locale in config/nova-translatable.php or per-resource:
Translatable::make([...])->defaultLocale('en');
Nested Translatable Fields:
Combine with other Spatie packages (e.g., nova-relationships) for nested translations:
Translatable::make([
BelongsTo::make('Category', 'category'),
Text::make('translated_name'),
]);
Customizing the UI: Override the default locale selector or add custom logic via service provider:
Nova::serving(function () {
Nova::translatable()->localeField('language_code');
});
Bulk Actions:
Use Translatable::make() with NovaTable for bulk editing translations:
public function fields(Request $request)
{
return [
Translatable::make([
Text::make('title'),
])->bulkActions(['edit']),
];
}
Laravel Scout: If using Scout for search, ensure translations are indexed:
public function toSearchableArray()
{
return array_merge(
parent::toSearchableArray(),
$this->getTranslations()->pluck('title')->toArray()
);
}
Nova Filters: Filter translatable fields by locale:
public function filters(Request $request)
{
return [
new Filter(
Nova::translatable()->availableLocales(),
'locale'
),
];
}
Nova Actions: Create actions to export translations:
use Spatie\NovaTranslatable\Actions\ExportTranslations;
public function actions(Request $request)
{
return [
(new ExportTranslations)->withLocales(['en', 'es']),
];
}
Nova Tool Integration:
Extend existing tools (e.g., NovaTool) to support translations:
public function tool()
{
return new class extends NovaTool
{
public function fields()
{
return [
Translatable::make([
Text::make('content'),
]),
];
}
};
}
Missing Translations Table:
Column not found: 1054 Unknown column 'locale' in 'where clause'.translations table and ensure the locale column exists.Locale Mismatch:
config/app.php locales match those in the translations table. Use:
Nova::translatable()->locales(['en', 'es', 'fr']);
Caching Issues:
php artisan nova:cache-clear
Non-Translatable Fields in Group:
BelongsTo or HasMany may not render correctly.Translatable::make([
Text::make('title'),
])->except([
BelongsTo::make('Author'), // This will be ignored
]);
Mass Assignment Risks:
protected $fillable = ['title', 'description'];
public $translatable = ['title', 'description'];
Check Database:
Inspect the translations table for missing or malformed data:
SELECT * FROM translations WHERE post_id = 1;
Log Locale Selection: Add debug logs to track locale changes:
Nova::translatable()->onLocaleSelected(function ($locale) {
\Log::info("Locale selected: {$locale}");
});
Validate Field Names:
Ensure field names in Translatable::make() match the $translatable array in your model.
Test with a Single Locale: Temporarily restrict to one locale to isolate issues:
Translatable::make([...])->locales(['en']);
Custom Locale Provider: Override the default locale provider in your service provider:
Nova::translatable()->useLocaleProvider(function () {
return ['en', 'de', 'ja'];
});
Dynamic Field Resolution: Use closures to dynamically resolve translatable fields:
Translatable::make(function () {
return [
Text::make('title'),
$this->showCustomField() ? Text::make('custom_field') : null,
];
});
Custom Storage Engine: Extend the package to use a different storage backend (e.g., Redis) by publishing and modifying the service provider.
Nova Field Extensions: Add custom methods to existing Nova fields for translation-specific logic:
Text::make('title')->withMeta([
'translatable' => true,
'fallback_locale' => 'en',
]);
Event Listeners: Listen for translation events to trigger side effects:
Nova::translatable()->onSaving(function ($resource, $locale, $fields) {
// Log or process fields before save
});
Fallback Logic: Implement custom fallback behavior for missing translations:
Nova::translatable()->fallbackLocale('en');
How can I help you explore Laravel packages today?