Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Translit Laravel Package

denismitr/translit

denismitr/translit is a lightweight PHP/Laravel transliteration package that converts Cyrillic text to Latin script using configurable character maps, making it useful for slugs, URLs, and search-friendly output. It offers a simple API for integrating transliteration into Laravel applications.

Deep Wiki
Context7

Getting Started

Install and confirm the wiring

composer require denismitr/translit

Check vendor/denismitr/translit/composer.json first — the package predates the widespread use of extra.laravel.providers, so depending on the version you may need to register the service provider manually in config/app.php and alias a facade. If auto-discovery is present, the facade (Translit) and/or a global translit() helper will already be available.

Where to look first

  1. src/ — find the main transliteration class and read the character map. That map is the package; everything else is a thin wrapper. You want to know exactly how ё, й, ъ, ь, ы, щ, ю, я, ц, х are handled before you trust it with production data.
  2. tests/ — the fixtures are the fastest way to see intended input/output pairs, including punctuation and mixed Latin/Cyrillic strings.
  3. composer.json require block — confirm the PHP and illuminate/support constraints against your app. A 2020 release means PHP 7.x-era constraints; on PHP 8.2+ you should validate with a spike branch before committing.

First use case: a slug from a Russian title

use Denismitr\Translit\Facades\Translit;

$title = 'Привет, мир! Как дела?';
$slug  = Translit::make($title); // "privet-mir-kak-dela" (verify exact casing/separators)

Do not assume the package produces a URL-safe slug on its own. In most setups you want:

$slug = Str::slug(Translit::make($title));

Str::slug handles lowercasing, collapsing separators, and stripping stray punctuation the transliterator passes through. Run this pair against ten real titles from your own data before you build anything on top of it.

Implementation Patterns

Wrap it, don't call it everywhere. Put the package behind one small class so you can swap rules or add collision handling without touching 40 call sites:

final class Slugger
{
    public function slug(string $value): string
    {
        return Str::limit(
            Str::slug(Translit::make($value)),
            80,
            ''
        );
    }

    public function uniqueSlug(string $value, string $column = 'slug'): string
    {
        $base = $this->slug($value) ?: 'n-a';
        $slug = $base;
        $i = 2;

        while (Model::where($column, $slug)->exists()) {
            $slug = "{$base}-{$i}";
            $i++;
        }

        return $slug;
    }
}

Auto-slug on write via an observer or trait. Keep the generating concern out of the controller:

protected static function booted(): void
{
    static::creating(function (Post $post) {
        $post->slug ??= app(Slugger::class)->uniqueSlug($post->title);
    });

    static::updating(function (Post $post) {
        if ($post->isDirty('title') && ! $post->isDirty('slug')) {
            $post->slug = app(Slugger::class)->uniqueSlug($post->title);
        }
    });
}

Expose the slug field in the admin form so editors can override it — transliteration produces competent but occasionally ugly slugs, and a human will sometimes want kak-dela instead of a literal rendering.

Form Request validation for user-supplied slugs. If you accept a slug from the user, validate it after transliteration and enforce a pattern (/^[a-z0-9-]+$/) rather than trying to sanitize by regex alone — otherwise a user typing Latin a into a Cyrillic word creates visually identical, distinct URLs.

Backfill as an artisan command, chunked. When you add slugs to an existing table:

Post::whereNull('slug')->chunkById(500, function ($posts) {
    foreach ($posts as $post) {
        $post->slug = app(Slugger::class)->uniqueSlug($post->title);
        $post->saveQuietly(); // skip observers/events for the backfill
    }
});

Add the unique index after the backfill completes, not before. Doing it the other way around means every chunk fights the index while it is still half-populated.

Route model binding. Once slugs exist, getRouteKeyName() returning 'slug' plus Route::get('/posts/{post:slug}', ...) is the payoff. Keep the numeric ID in the URL as a fallback (/posts/{post:slug} with a resolver that tries slug then ID) during the transition period so old bookmarks do not 404.

Testing. Add a data provider of real titles — including ones with quotes, ellipses, English brand names embedded in Russian, and all-caps — and assert the exact slug string. Treat the slug format as part of your public API contract; a package update that changes ё handling will silently break inbound links.

Gotchas and Tips

The package is effectively unmaintained. Last release November 2020, 12 stars. That is not a reason to avoid it — transliteration is a solved, static problem — but it does mean the compatibility risk sits entirely with you. Pin the version, and write a test suite of a few hundred titles before you put it in front of users. If it breaks on a future PHP/Laravel bump, you are on the hook for a fork.

"Yandex rules" ≠ official standards. Yandex's transliteration differs from GOST 7.79 and from BGN/PCGN in several places, notably ё, й, ъ/ь, and the digraph choices for ж, х, ц, щ, ю, я. This matters if slugs are consumed by a partner system, imported into another CMS, or expected to look a certain way to Russian-speaking users. Document the choice in your README; don't discover it during an SEO migration.

Only Russian is covered. Ukrainian і/ї/є/ґ, Belarusian ў, and Kazakh-specific Cyrillic characters are very likely passed through verbatim or dropped. Str::slug will then strip them or leave them intact depending on your configuration, producing either empty strings or raw UTF-8 in URLs. If your dataset is not purely Russian, add a fallback map of your own before calling the package.

Mixed Latin/Cyrillic input. Product titles like iPhone 15 Pro для фотографов should produce iphone-15-pro-dlya-fotografov. Test this explicitly — a naive character-map implementation may mishandle the boundary between scripts, and Str::slug's language parameter can interact surprisingly with already-transliterated text.

Idempotency is not guaranteed. translit(translit('Щука')) will generally not equal translit('Щука'), because the second pass sees Latin input. Never transliterate a value twice — for instance, a model observer plus a controller that both generate the slug. Pick one place and enforce it.

Empty and degenerate slugs. A title consisting only of punctuation, emoji, or a single ь can produce an empty or single-character string. Always coalesce to a stable fallback ('n-a', or the model ID) and enforce a minimum length before saving, or you will get https://example.com/posts/ and route-binding failures that are miserable to debug in production.

Length and truncation. Str::limit($slug, 80, '') can cut mid-word and, worse, cut a slug down to something that collides with an existing one. Run uniqueness checks after truncation, not on the untruncated base.

Slug changes break links. Once a post is live, changing its slug silently 404s every inbound link. Handle updating by storing the old slug in a slug_history table and adding a redirect middleware, or simply freeze the slug after first publish. Decide this early — retrofitting it later means crawling your access logs for dead URLs.

Extension points. The character map is the natural extension point: if you need custom mappings (brand names, specific digraphs), extend the class or copy the map into your own service rather than patching the vendor directory. If the class exposes static or configurable mappings, prefer config-driven overrides so the behavior is visible in version control and testable.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings