Locale maps
A slug column becomes a locale map — one slug per language in a single json/jsonb column — when the model implements ProvidesLocaleMaps for it, when the column has an array/json/object/collection cast, or when you call ->localized(). First list the locales your site serves (the default is app.locale + app.fallback_locale):
// config/sluggable.php — the locales your site serves (default: app.locale + app.fallback_locale)
'locales' => ['supported' => ['en', 'sk', 'de']],final class Topic extends Model implements Sluggable
{
use HasSlug;
protected $fillable = ['name', 'slug'];
protected $casts = ['name' => 'array', 'slug' => 'array'];
}
$topic = Topic::create(['name' => ['en' => 'Investing', 'sk' => 'Investovanie', 'de' => 'Straße']]);
$topic->slug; // ['en' => 'investing', 'sk' => 'investovanie', 'de' => 'strasse']- Each locale is transliterated in its own language (de: ß → ss).
- Locale-map sources are read raw, never through getAttribute() — translation traits return the current-locale string there — so the sk slug is always built from the sk name.
- Slugs are generated only in locales a lookup can find — the current and fallback locale, SlugLocales::supported() and an explicit locales([...]) list — so every slug, and every route key, resolves.
- Blank and JSON-null values are ignored; locales sluggable does not generate are preserved.
- A model exposing isLocaleMapAttribute() (e.g. through HasTranslations) without implementing ProvidesLocaleMaps is rejected (localeMapContractMissing) instead of being silently treated as a string column.
Reading a locale map
$topic->currentSlug(); // current locale, then the chain (fallback, then every resolvable locale)
$topic->slugFor('sk'); // exact locale, no fallback
$topic->slugMap(); // ['en' => …, 'sk' => …] (string column: ['*' => …])Choosing locales and fallbacks
use RoundlyConsulting\Sluggable\Enums\LocaleFallback;
use RoundlyConsulting\Sluggable\Enums\TargetLocales;
SlugDefinition::for('slug')->from('name')
->localized() // force locale-map storage
->locales(TargetLocales::Supported) // or ['en', 'sk'] / fn (Model $m): list<string>
->fallback(LocaleFallback::Fallback)
->fallbackLocale('en')
->uniqueAcrossLocales(); // application-level only| locales() | Generates |
|---|---|
TargetLocales::Source | Default. The source locales among the resolvable ones; a source written only in other locales still gets a slug, in the current locale. |
TargetLocales::Supported | Every SlugLocales::supported() locale (missing sources fall back). |
TargetLocales::Current | Only the request locale. |
['en', 'sk'] | The locales given — lookups search them like the supported ones. |
fn (Model $m): list<string> | The closure’s locales, limited to the resolvable set. |
fallback() sets the reading and binding chain: LocaleFallback::None (current locale only), Fallback (current, then the fallback locale) or Any (the default — then every resolvable locale, so a URL in any of them still resolves). fallbackLocale() overrides the fallback locale itself.
A string slug built from a locale-map source uses sourceLocale(); without it, the fallback locale, then the current locale, then the first value by key:
SlugDefinition::for('slug')->from('name')->sourceLocale('en'); // string slug from the en nameUniqueness per locale
perLocaleUniqueness() (the default) checks each locale separately, so red-chair may exist once per language. uniqueAcrossLocales() makes a value unique across every locale — enforced by the application only, as no index can express it.
Where locales come from
Supported, fallback and current locales come from the SlugLocales contract. Sluggable binds a config-driven default (sluggable.locales.*, else app.locale and app.fallback_locale) with bindIf(); translatable-for-laravel rebinds it to its own locale source; your own binding wins over both:
use RoundlyConsulting\Sluggable\Contracts\SlugLocales;
final class AppSlugLocales implements SlugLocales
{
public function supported(): array
{
return ['en', 'sk', 'de'];
}
public function fallback(): ?string
{
return 'en';
}
public function current(): string
{
return app()->getLocale();
}
}
// AppServiceProvider::register()
$this->app->singleton(SlugLocales::class, AppSlugLocales::class);The ProvidesLocaleMaps contract
Implement ProvidesLocaleMaps when a translation layer owns the map — sluggable then reads and writes through it instead of the raw attribute:
namespace RoundlyConsulting\Sluggable\Contracts;
interface ProvidesLocaleMaps
{
public function isLocaleMapAttribute(string $key): bool;
/** @return array<string, string> blank values already dropped */
public function getLocaleMap(string $key): array;
/** @param array<string, string> $map */
public function setLocaleMap(string $key, array $map): static;
}For the column itself, the localizedSlug() macro picks the right type per engine:
$table->localizedSlug('slug'); // jsonb (pgsql) / json (mysql) / text (sqlite)Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.