---
title: "Names, slugs and translations — Tags for Laravel | Roundly"
description: "One tag per meaning — normalised names, per-locale slugs along the locale chain, duplicates and reserved names, stable and manual slugs, colours and icons."
url: https://roundly-consulting.com/open-source/docs/tags-for-laravel/names-and-slugs
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Tags for Laravel](https://roundly-consulting.com/open-source/docs/tags-for-laravel.md)

# Names, slugs and translations

A string names the tag whose slug, in some locale of the chain, equals that locale’s slug of the string — computed with sluggable-for-laravel’s own pipeline. So spelling, case and punctuation never make a second tag:

```php
$rock = Tags::type('label')->findOrCreate('Rock');

Tags::type('label')->findOrCreate('ROCK!')->is($rock);   // true — one tag per slug
Tags::type('label')->findOrCreate('  rock.  ')->is($rock); // true
Tags::type('label')->find('rock')->is($rock);            // true — a slug names its tag

// The dictionary keeps these three tags apart: c, c-plus-plus, c-sharp
Tags::type('label')->findOrCreate('C');
Tags::type('label')->findOrCreate('C++');
Tags::type('label')->findOrCreate('C#');
```

- A name is trimmed, control and invisible characters are removed and whitespace collapses to one space. A blank name, or one longer than tags.names.max\_length, throws InvalidTagException; so does a name with nothing to build a slug from.
- The locale chain is the current locale, then the fallback locale, then every other supported one. Each locale slugs with its own transliteration — Über is ueber in German, uber in English.
- The same slug in two locales may be two tags (gift is a present in English and poison in German): the earlier locale of the chain wins.
- Slugs are unique per type, scope and locale, deleted tags included, and never suffixed: a taken slug is the same tag, or a duplicate — never rock-2.

## Every locale

A plain string is the current locale; a locale map sets several at once. Malformed locale keys throw, and keys outside translatable.locales are dropped — a package cannot know the app’s locales. The current locale must be a supported one, or a plain string cannot name a tag:

```php
// A plain string is the current locale; a map sets several at once
$music = Tags::type('genre')->make(['en' => 'Music', 'sk' => 'Hudba'])->create();

$music->name;                        // 'Music' in en, 'Hudba' in sk
$music->slugFor('sk');               // 'hudba'
$music->getTranslations('name');     // ['en' => 'Music', 'sk' => 'Hudba']

app()->setLocale('sk');
Tags::type('genre')->find('music')->is($music);   // true — the chain falls back to en
Tags::type('genre')->find('Hudba')->is($music);   // true

// A rename changes only the locales it names; each slug follows its own name
Tags::type('genre')->rename($music, ['sk' => 'Hudobné žánre']);
```

## Duplicates and reserved names

```php
use RoundlyConsulting\Tags\DataTransferObjects\TagData;

$pop = Tags::type('label')->make('Pop')->create();
Tags::type('label')->make('POP')->create();
// DuplicateTagException: A tag with the slug [pop] in [en] already exists.

$rock = Tags::type('label')->make('Rock')->color('#000000')->create();
Tags::type('label')->delete($rock);

// A deleted tag keeps its name: creating it again restores that tag, with the new attributes
$again = Tags::type('label')->create(new TagData(name: 'Rock', color: '#ffffff'));
$again->is($rock);                    // true — TagRestored fires, and it carries no old taggings

// Renaming another tag onto a deleted tag's slug is refused
Tags::type('label')->rename($pop, 'Rock');
// DuplicateTagException: A deleted tag holds the slug [rock] in [en]: restore that tag instead.
```

A deleted tag keeps its name. Using the name again restores that tag instead of making a new one — always without its old taggings, so old records are never relabelled. What it comes back as depends on the call:

```php
use RoundlyConsulting\Tags\DataTransferObjects\TagData;

$genres = Tags::type('genre');
$genres->delete($jazz);       // Jazz sat under Music, with a colour, an icon and a description

// Any one of these brings the deleted Jazz back, without its old taggings:
$genres->findOrCreate('jazz');                                 // as it was — names, colour, icon, description, under Music
$genres->make('Jazz')->color('#ffffff')->findOrCreate();       // still as it was: of the builder's data only a parent applies
$genres->make('Jazz')->under($rock)->findOrCreate();           // as it was, moved under Rock
$genres->create(new TagData(name: 'Jazz', color: '#ffffff'));  // with exactly these attributes (no icon, no description), under Music
$genres->make('Jazz')->under(null)->create();                  // with these attributes, as a root

// With Music deleted too, create() and findOrCreate() refuse (InvalidTagHierarchyException,
// "restore it first") unless a parent is given; a free-form name comes back as a root instead
$article->attachTags('Physics', 'topic');                      // 'topic' is hierarchical()->freeForm()
```

- findOrCreate() — a name, a locale map, the builder or a TagData — and a free-form name in attachTags() or syncTags() bring it back as it was: every name, its description, slugs, colour and icon, under its parent’s current place. Of the builder’s data only a parent given with under() applies.
- create() applies its own attributes — names, description, manual slugs, colour and icon, so what it leaves out is cleared — and keeps the tag where it was unless a parent is given (under(null), or parent: null, asks for a root); a given position applies too.
- If its parent is deleted as well, create() and findOrCreate() refuse with InvalidTagHierarchyException (“restore it first”) unless a parent is given, while a free-form name brings it back as a root, like every free-form tag of a tree.
- Either way the type’s depth cap holds, its deleted descendants’ paths follow it, and it never goes under one of its own children (a plain $tag->delete() can leave live ones). A create whose locales are held by two different deleted tags is refused.

## Stable and manual slugs

Without stableSlugs(), a locale’s slug follows its name on every rename — the old slug stops naming the tag. With stableSlugs() (curated types only), slugs never regenerate, so a category URL never moves — only a locale named for the first time gets a slug, once; the new name’s slug names the tag only once a manual slug says so. A rename onto a deleted tag’s slug points to restore() on a stable-slug type too:

```php
use RoundlyConsulting\Tags\DataTransferObjects\UpdateTagData;

Tags::define('genre')->hierarchical()->stableSlugs();
$jazz = Tags::type('genre')->make('Jazz')->create();

$renamed = Tags::type('genre')->rename($jazz, 'Jazz music');
$renamed->name;                                     // 'Jazz music'
$renamed->slug;                                     // 'jazz' — /events?category=jazz still works

// Curated types also take a manual slug, already in slug form
$blues = Tags::type('genre')->make('Blues and soul')->slug('blues')->create();
Tags::type('genre')->update($blues, new UpdateTagData(slug: 'blues-soul'));
```

A manual slug is for curated types only (InvalidTagException on a free-form type) and must already be in slug form — lowercase letters, digits and separators, at most tags.slugs.max\_length. A slug another tag holds is a DuplicateTagException.

## Descriptions, colours and icons

```php
$music = Tags::type('genre')
    ->make(['en' => 'Music', 'sk' => 'Hudba'])
    ->description(['en' => 'Concerts and festivals.', 'sk' => 'Koncerty a festivaly.'])
    ->color('#F24E29')          // a hex colour, stored lower-case: #f24e29
    ->icon('music')             // an identifier (music, fa-guitar) or an emoji, at most 64 characters
    ->create();

Tags::type('genre')->edit($music)->color(null)->icon(null)->save();   // null clears
```

- A description is plain text — no markup, no control characters other than line breaks and tabs — of at most tags.descriptions.max\_length characters per locale. Blank locales are dropped.
- A colour is a #rrggbb hex value, stored lower-case.
- An icon is an identifier (music, fa-guitar) or an emoji: at most 64 characters, no whitespace, control characters, quotes or angle brackets.
- A tag’s type and owner never change: updating them throws InvalidTagException. Moving a tag between scopes is not supported.

## 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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/tags-for-laravel.md)
