amdadulhaq / bangla-slug-laravel
Readable Banglish URL slugs from Bangla text for Laravel: phonetic transliteration, English loanword and acronym detection.
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- driftingly/rector-laravel: ^2.0
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-arch: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Turn Bangla titles into clean, readable Banglish slugs — the way people in Bangladesh actually write them.
Why?
Laravel's Str::slug() transliterates Bangla one letter at a time. It drops the vowel every Bangla consonant carries, and it has no idea that হাসপাতাল is just "hospital":
| Title | Str::slug() |
Str::banglaSlug() |
|---|---|---|
| সোনার তরী | sonar-tree |
sonar-tari |
| পহেলা বৈশাখ | phela-boisakh |
pahela-boishakh |
| ঢাকা বিশ্ববিদ্যালয় | dhaka-biswbidzalz |
dhaka-university |
| ঢাকা মেডিকেল কলেজ হাসপাতাল | dhaka-medikel-klej-haspatal |
dhaka-medical-college-hospital |
| কক্সবাজার সমুদ্র সৈকত | kksbajar-smudr-soikt |
coxs-bazar-samudra-soikat |
| মোবাইল ব্যাংকিং | mobail-bzangking |
mobile-banking |
This package reads Bangla phonetically, knows ~830 place names and English loanwords, detects acronyms, and leaves English text alone.
Quick Start
1. Install via Composer
composer require amdadulhaq/bangla-slug-laravel
2. Generate slugs
use Illuminate\Support\Str; Str::banglaSlug('আমার সোনার বাংলা'); // amar-sonar-bangla
That's it — the service provider, Str macro and facade are auto-discovered.
Features
- Phonetic Transliteration - Keeps or drops the inherent vowel the way Bangla is spoken (
কলম→kalam,করবে→karbe) - Conjunct Aware - Handles ya/ba/ra-phala, doubled consonants,
ক্ষ,জ্ঞ,ঙ্গ, anusvara and visarga - English Loanwords - Bangla-spelled English words come out in English (
ক্রিকেট→cricket,কম্পিউটার→computer) - Acronym Detection - Words spelled from English letter names become acronyms (
বিটিভি→btv,আইসিইউ→icu) - Dotted Initials -
এ. পি. জে. আবদুল কালাম→a-p-j-abdul-kalam - Suffix Handling - Known words keep their spelling with common suffixes (
ঢাকার→dhakar) - Unicode Safe - Normalizes nukta variants, split
ো/ৌ, zero-width joiners and Bangla digits, and drops malformed UTF-8 bytes without losing the text around them - English Untouched - Text without Bangla gives exactly the same result as
Str::slug() - Length Limit - Long headlines are cut at a word boundary, never mid-word
- Configurable - Add your own words without touching code
- Developer Tools - Pint, Pest, Rector, and Larastan included
Support & Sponsorship
Building and maintaining high-quality open-source packages takes hundreds of hours of dedicated time. If this package saves you time, please consider supporting the project.
Sponsor the Project Ensure the package stays actively maintained, receives rapid bug fixes, and continuous feature updates by becoming a monthly sponsor.
Table of Contents
- Installation
- Usage
- Recipes
- Configuration
- How It Works
- Examples
- Limitations
- API Reference
- Troubleshooting
- Development
- FAQ
Installation
Requirements
- PHP: 8.3, 8.4, or 8.5
- Laravel: 12.x or 13.x
- Extensions:
mbstring(already required by Laravel)
Install via Composer
composer require amdadulhaq/bangla-slug-laravel
No migrations, no setup. Publishing the config is only needed if you want to add your own words.
Usage
Three Ways to Call It
All three give the same result.
// 1. Str macro — shortest, works anywhere use Illuminate\Support\Str; Str::banglaSlug('পদ্মা সেতু'); // padma-setu // 2. Facade use AmdadulHaq\BanglaSlug\Facades\BanglaSlug; BanglaSlug::generate('একুশে ফেব্রুয়ারি'); // ekushe-february // 3. Dependency injection — easiest to fake in tests use AmdadulHaq\BanglaSlug\BanglaSlug; public function __construct(private BanglaSlug $slugs) {} $this->slugs->generate('অনলাইন শপিং'); // online-shopping
The service is a singleton, so the word list is prepared once per request no matter how many slugs you generate.
Unique Slugs
unique() appends a random number from 1 to 999999, so two posts with the same title get different slugs:
BanglaSlug::unique('ঈদ মোবারক'); // eid-mobarak-482913 BanglaSlug::unique('ঈদ মোবারক'); // eid-mobarak-70215
On its own, the random suffix makes a collision very unlikely, not impossible. Pass a closure that says whether a slug is already taken, and unique() retries with a new number until it finds a free one:
$slug = BanglaSlug::unique( $request->validated('title'), fn (string $slug): bool => Post::withTrashed()->where('slug', $slug)->exists(), );
If 100 attempts in a row are all taken — which in practice means the closure always returns true — a RuntimeException is thrown instead of looping forever. Keep a unique index on the column either way, since two requests at the same instant can still race.
Custom Separator
BanglaSlug::generate('আমার সোনার বাংলা', '_'); // amar_sonar_bangla Str::banglaSlug('আমার সোনার বাংলা', '_'); // amar_sonar_bangla
Recipes
Eloquent Models
Fill the slug automatically when a model is created, and leave it alone on update so published URLs never change by accident:
use AmdadulHaq\BanglaSlug\Facades\BanglaSlug; use Illuminate\Database\Eloquent\Model; class Post extends Model { protected static function booted(): void { static::creating(function (Post $post): void { $post->slug ??= BanglaSlug::unique( $post->title, fn (string $slug): bool => Post::query()->where('slug', $slug)->exists(), ); }); } public function getRouteKeyName(): string { return 'slug'; } }
With getRouteKeyName(), Route::get('/posts/{post}', ...) resolves /posts/pahela-boishakh-4521 directly.
Controllers and APIs
use AmdadulHaq\BanglaSlug\BanglaSlug; public function store(StorePostRequest $request, BanglaSlug $slugs): JsonResponse { $post = Post::create([ ...$request->validated(), 'slug' => $slugs->unique( $request->validated('title'), fn (string $slug): bool => Post::query()->where('slug', $slug)->exists(), ), ]); return PostResource::make($post)->response()->setStatusCode(201); }
Filament Forms
Fill the slug as the title is typed, only when creating:
use Filament\Forms\Components\TextInput; use Filament\Schemas\Components\Utilities\Set; use Illuminate\Support\Str; TextInput::make('title') ->required() ->live(onBlur: true) ->afterStateUpdated(fn (Set $set, ?string $state, string $operation) => $operation === 'create' ? $set('slug', Str::banglaSlug($state ?? '')) : null), TextInput::make('slug') ->required() ->unique(ignoreRecord: true) ->alphaDash(),
On the edit page, add a suffix action to the slug field so editors can regenerate it on purpose:
use Filament\Actions\Action; use Filament\Schemas\Components\Utilities\Get; TextInput::make('slug') ->suffixAction( Action::make('regenerateSlug') ->icon('heroicon-m-arrow-path') ->visible(fn (string $operation): bool => $operation !== 'view') ->action(fn (Get $get, Set $set) => $set('slug', Str::banglaSlug((string) $get('title')))), ),
Guaranteed-Unique Slugs
When you want a clean slug with no number, and a counter only on collision (khela, khela-2, khela-3):
use Illuminate\Support\Str; function uniqueSlug(string $title, ?int $ignoreId = null): string { $base = Str::banglaSlug($title) ?: 'item'; $slug = $base; for ($counter = 2; Category::query() ->where('slug', $slug) ->when($ignoreId, fn ($query) => $query->whereKeyNot($ignoreId)) ->exists(); $counter++) { $slug = "{$base}-{$counter}"; } return $slug; }
Regenerating Existing Slugs
Moving from Str::slug()? Rebuild old slugs in a one-off command, keeping any numeric suffix:
use AmdadulHaq\BanglaSlug\Facades\BanglaSlug; Post::query()->lazyById()->each(function (Post $post): void { $suffix = preg_match('/-(\d+)$/', $post->slug, $matches) ? '-'.$matches[1] : ''; $slug = BanglaSlug::generate($post->title).$suffix; if ($slug !== $post->slug) { $post->timestamps = false; $post->forceFill(['slug' => $slug])->saveQuietly(); } });
Heads up: changing slugs changes public URLs. Old links, search results and anything a mobile app cached by slug will stop matching. Consider redirects from old slugs.
Configuration
Publish the config file:
php artisan vendor:publish --tag="bangla-slug-config"
config/bangla-slug.php has three keys:
| Key | Default | Purpose |
|---|---|---|
words |
~830 entries | Whole Bangla words with a fixed spelling |
max_length |
80 |
Slugs are cut at the last whole word within this many characters; 0 disables the limit |
suffixes |
ের, এর, তে, কে, র, ে |
Endings still matched after a known word |
Adding Your Own Words
Open the published file and add entries to words:
'words' => [ // ... the package's defaults ... // Your project 'মিরপুর' => 'mirpur', 'জরুরি' => 'emergency', 'ফুডপান্ডা' => 'foodpanda', ],
Note: Laravel merges package config one level deep, so a published
wordsarray replaces the package defaults instead of adding to them. The published file already contains every default word, so keep them in place and add yours below. After upgrading the package, re-publish (--force) or copy any new default words across.
Values may contain hyphens for multi-word spellings, for example 'বাসস্ট্যান্ড' => 'bus-stand'.
Writing Good Entries
- One word per key. Titles are matched word by word, so a key with a space (
'উচ্চ বিদ্যালয়') never matches; add each word on its own. Entries also match whole words only, so'কাল' => 'kal'will never breakকালিয়াকৈর. - Skip suffixed forms. Add
ঢাকাonce;ঢাকার,ঢাকাতেandঢাকাকেare matched throughsuffixes. - Add each common spelling. Bangla often has more than one spelling (
একাডেমি/একাডেমী,গ্রিন/গ্রীন). Add each one you see in your data. - Don't worry about Unicode forms.
য়,ড়,ঢ়typed as one character or as letter plus nukta, andঅ্যাvsএ্যা, are normalized before lookup. - Leave acronyms out.
বিটিভি,আইসিইউand similar are detected automatically.
Length Limit
Slugs longer than max_length are cut at the last separator that fits, so no word is left half-spelled:
config(['bangla-slug.max_length' => 20]); Str::banglaSlug('বাংলাদেশ ক্রিকেট দলের নতুন অধিনায়ক'); // bangladesh-cricket
The random suffix from unique() is added after the limit.
How It Works
Digits are converted and Unicode is normalized first. Then each Bangla word goes through these steps; the first match wins:
- Dotted initial — a single letter name followed by a dot (
আর.→r) - Known word — the
wordsconfig, with or without a known suffix - Acronym — a word made only of two or more English letter names (
বিটিভি→btv). A lone letter name likeআরorকেstays a word, since those are real Bangla words too - Phonetic transliteration — the word is split into syllables, then the inherent vowel is kept or dropped:
- kept at the start of a word (
কলম→ kalam) - dropped at the end of a word (
কলম→ kalam) - dropped between two voiced syllables (
করবে→karbe) - kept at the end after a phala or doubled consonant (
কেন্দ্র→kendra,অন্ন→anna) - kept before a conjunct (
কর্মকর্তা→karmakarta)
- kept at the start of a word (
The result then goes through Laravel's Str::slug() and the length limit. English text never reaches steps 1–4.
Examples
| Bangla | Slug |
|---|---|
| আমার সোনার বাংলা | amar-sonar-bangla |
| পহেলা বৈশাখ | pahela-boishakh |
| একুশে ফেব্রুয়ারি | ekushe-february |
| ঈদ মোবারক ২০২৬ | eid-mobarak-2026 |
| রবীন্দ্রনাথ ঠাকুর | rabindranath-thakur |
| কাজী নজরুল ইসলাম | kazi-nazrul-islam |
| পদ্মা সেতু | padma-setu |
| কক্সবাজার সমুদ্র সৈকত | coxs-bazar-samudra-soikat |
| ঢাকা বিশ্ববিদ্যালয় | dhaka-university |
| বাংলা একাডেমি | bangla-academy |
| বাংলাদেশ ক্রিকেট দল | bangladesh-cricket-dal |
| মোবাইল ব্যাংকিং | mobile-banking |
| জাতীয় পরিচয়পত্র | jatiyo-parichayapatra |
| বিটিভি | btv |
| এ. পি. জে. আবদুল কালাম | a-p-j-abdul-kalam |
| Hello বাংলাদেশ! | hello-bangladesh |
Limitations
- Transliteration, not translation. Bangla words stay Banglish (
জরুরি→jaruri). Only words inwordsget an English spelling. - English brand names need entries. A Bangla-spelled brand not in
wordscomes out phonetically (ফুডপান্ডা→fudpanda, notfoodpanda). No rule can recover the original English spelling. - One spelling per vowel. The inherent vowel is always written
a(কলম→kalam). Words written withoin everyday Banglish (খবর→khobor) come from the word list. - Rule-based. Bangla pronunciation has exceptions; words that come out oddly can be fixed with an entry in
words.
API Reference
BanglaSlug::generate(string $text, string $separator = '-'): string
Builds a slug from Bangla, English or mixed text. Text without Bangla gives the same result as Str::slug(). Returns an empty string when there is nothing to slug (empty text, only punctuation or emoji).
BanglaSlug::unique(string $text, ?Closure $exists = null): string
Same as generate() plus - and a random number from 1 to 999999. If the text gives an empty slug (only punctuation or emoji), the result is just the number. With $exists, retries with a new number while $exists($slug) returns true; throws RuntimeException after 100 taken attempts.
Str::banglaSlug(string $text, string $separator = '-'): string
Macro for generate().
Troubleshooting
Call to undefined method Illuminate\Support\Str::banglaSlug()
The service provider isn't loaded. If you disabled package discovery, register it in bootstrap/providers.php:
AmdadulHaq\BanglaSlug\BanglaSlugServiceProvider::class,
My new word has no effect
Clear the config cache so the published file is read again:
php artisan config:clear
Also make sure the key is the whole word as it appears in the title, without surrounding punctuation.
New default words are missing after an upgrade
Your published config/bangla-slug.php replaces the package's word list. Re-publish with --force (then re-add your own words), or copy the new entries across.
Development
Code Quality Tools
# Rector (code refactoring) composer refactor composer refactor:check # Laravel Pint (code style) composer lint composer lint:check # Pest (testing) composer test composer test-coverage # Larastan (static analysis) composer analyse
Adding a Word to the Package
Add the entry to config/bangla-slug.php under the matching section (places, institutions, loanwords…). If it fixes a rule rather than a single word, add a case to tests/BanglaSlugTest.php.
FAQ
Why not just use Str::slug()?
Str::slug() transliterates letter by letter and ignores the inherent vowel, so কলম becomes klm. This package reads Bangla phonetically. See Why?.
Will it change my existing English slugs?
No. Text without Bangla characters produces exactly the same slug as Str::slug().
Does it translate Bangla words to English?
No. Bangla words are transliterated (জরুরি → jaruri). Only words in the words config get a fixed English spelling.
Can I use it for non-slug text, like filenames?
Yes. Pass _ or - as the separator; the output is always lowercase a-z, 0-9 and the separator.
Is it fast enough for bulk imports?
Yes. It is pure string work with no database or network calls, and the word list is prepared once per request.
Contributing
We welcome contributions! Please see CONTRIBUTING for details.
Changelog
See CHANGELOG for recent changes.
Security
Please review our security policy for reporting vulnerabilities.
Credits
License
The MIT License (MIT). See License File for details.
Made with ❤️ for the Laravel community