Use the HasTranslatableSlug trait together with spatie/laravel-translatable to store one slug per locale.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Sluggable\HasTranslatableSlug;
use Spatie\Sluggable\SlugOptions;
use Spatie\Translatable\HasTranslations;
class Article extends Model
{
use HasTranslations;
use HasTranslatableSlug;
public $translatable = ['title', 'slug'];
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('title')
->saveSlugsTo('slug');
}
}
The slug column must hold a JSON value; every translatable attribute in $translatable (including the slug itself) is stored as a locale-keyed object.
##Generating slugs from a callable per locale
When using a callable, instantiate SlugOptions with createWithLocales(). The callable receives the model and the current locale.
public function getSlugOptions(): SlugOptions
{
return SlugOptions::createWithLocales(['en', 'nl'])
->generateSlugsFrom(fn (Article $article, string $locale) => "{$locale} {$article->id}")
->saveSlugsTo('slug');
}
##Routing
Use Laravel's implicit route model binding, pointing at the slug column.
Route::get('/articles/{article:slug}', fn (Article $article) => $article);
HasTranslatableSlug handles the locale-aware JSON lookup in resolveRouteBindingQuery().
##Finding models by slug
HasTranslatableSlug ships its own findBySlug() that searches the current locale and the application fallback locale.
$article = Article::findBySlug('my-article');
##Self-healing URLs
selfHealing() works with the translatable trait. The route key uses the slug for the current locale; stale slugs trigger the same redirect flow described in Self-healing URLs.
##Getting the route key for a specific locale
Use getLocalizedRouteKey() to retrieve the route key for a given locale without permanently changing the model's active locale.
$article->getLocalizedRouteKey('nl');
The method swaps in the requested locale, calls getRouteKey(), and restores the original locale via try/finally, so the model is always left as it was found, including when getRouteKey() throws.