imliamxo / shush
A configurable profanity filter for Laravel. Block or censor profanity with multi-language support.
Requires
- php: ^8.1
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
README
A configurable, multi-language profanity filter for Laravel with severity tiers, per-language normalisers, and evasion-resistant detection.
Catches shit, sh!t, sh1t, $h!t, shiiit, sshit, s.h.i.t — and everything in between. Ships with extensive English and Polish dictionaries out of the box.
Installation
composer require imliamxo/shush
php artisan vendor:publish --tag=shush-config
php artisan vendor:publish --tag=shush-dictionaries # optional
Quick Start
use ImLiaMxo\Shush\Facades\Shush; Shush::check('Hello world'); // false Shush::check('What the fuck'); // true Shush::check('What the f*ck'); // true Shush::check('What the f.u.c.k'); // true Shush::check('What the fuuuck'); // true Shush::check('No kurwa mać'); // true (Polish, if enabled) Shush::censor('What the fuck'); // "What the ****" Shush::clean($text); // censors or throws, per config $hits = Shush::detect('Shit and kurwa'); // [ // ['word' => 'shit', 'tier' => 'moderate', 'language' => 'en', 'match' => 'Shit'], // ['word' => 'kurwa', 'tier' => 'severe', 'language' => 'pl', 'match' => 'kurwa'], // ]
Strictness Levels
Words are categorised into three severity tiers. The strictness setting controls which tiers are active:
| Strictness | Tiers caught | Use case |
|---|---|---|
relaxed |
severe only | Catch slurs & hate speech, allow swearing |
normal |
severe + moderate | Block strong profanity (default) |
strict |
severe + moderate + mild | Family-friendly, zero tolerance |
// Global 'strictness' => 'strict', // Runtime Shush::setStrictness('relaxed'); // Per-route Route::post('/kids', ...)->middleware('shush:strict'); Route::post('/forum', ...)->middleware('shush:relaxed');
Evasion Detection
Each language has a dedicated normaliser that catches evasion techniques:
| Technique | Example | Caught? |
|---|---|---|
| Leet-speak symbols | sh!t, $h1t, b@stard |
✅ |
| Number substitution | sh1t, a55, f4g |
✅ |
| Repeated characters | shiiit, fuuuck, sshit |
✅ |
| Separator tricks | s.h.i.t, f-u-c-k, s h i t |
✅ |
| Mixed evasion | $h!iit, f.u.c.k |
✅ |
| Unicode homoglyphs | Cyrillic а/е/о lookalikes | ✅ |
| Zero-width characters | Hidden unicode between letters | ✅ |
| Diacritic stripping | Polish gówno → gowno |
✅ |
| Diacritic substitution | ą → a, ł → l |
✅ |
Per-Language Normalisers
Each language can have its own normaliser that understands its character set and evasion patterns:
- English — handles standard leet-speak, homoglyphs, basic Latin diacritics
- Polish — handles
ą↔a,ć↔c,ę↔e,ł↔l,ń↔n,ó↔o,ś↔s,ź↔z,ż↔zstripping
Adding a Custom Normaliser
<?php namespace App\Normalisers; use ImLiaMxo\Shush\Normalisers\BaseNormaliser; class GermanNormaliser extends BaseNormaliser { public function getLanguage(): string { return 'de'; } protected function getExtraSubstitutions(): array { return [ 'ä' => 'a', 'ö' => 'o', 'ü' => 'u', 'ß' => 'ss', ]; } protected function stripDiacritics(string $text): string { return strtr($text, [ 'ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'ß' => 'ss', 'Ä' => 'ae', 'Ö' => 'oe', 'Ü' => 'ue', ]); } protected function getDiacriticAlternates(string $char): array { return match ($char) { 'a' => ['ä'], 'o' => ['ö'], 'u' => ['ü'], default => [], }; } }
Register it in config/shush.php:
'normalisers' => [ 'de' => \App\Normalisers\GermanNormaliser::class, ],
Or at runtime:
Shush::registerNormaliser('de', \App\Normalisers\GermanNormaliser::class);
Built-in False-Positive Whitelist
Shush ships with an extensive whitelist of 200+ words that contain profanity substrings but aren't profane. This prevents false positives on words like:
Place names: Scunthorpe, Penistone, Cockermouth, Middlesex, Sussex, Essex, Effingham
Common words: class, classic, assess, assessment, asset, assign, assist, associate, assume, assembly, assault, bass, brass, glass, grass, mass, pass, passport, passion, passive, embarrass, harassment
Compound words: cocktail, cockatoo, cockerel, cockpit, peacock, Hitchcock, shuttlecock
Other: therapist, grapefruit, skyscraper, dictionary, predict, constitution, document, accumulate, circumstance, Mississippi
Polish false positives are also covered: kurier, kurort, kurczak, duplikat.
// Remove a built-in whitelist entry if needed Shush::disallow(['cocktail']); // Add to whitelist Shush::allow(['mycustomword']);
Adding Languages
Create a folder in resources/shush/dictionaries/:
resources/shush/dictionaries/
├── en/
│ └── words.php
├── pl/
│ └── words.php
└── de/
├── insults.php
└── slurs.php
Each file returns an array keyed by tier:
<?php return [ 'severe' => ['schlimmsteswort'], 'moderate' => ['scheiße', 'arschloch', 'wichser'], 'mild' => ['mist', 'verdammt', 'kacke'], ];
Flat arrays (no tier keys) are treated as moderate. All .php files in a language folder are merged.
Enable in config:
'languages' => ['en', 'pl', 'de'],
Modes
Censor (default)
Shush::censor('What the fuck'); // "What the ****" Shush::setMask('#')->censor('...'); // "What the ####" // Full-word replacement (config: mask_behaviour = 'full') // → "What the [REDACTED]"
Block
try { Shush::clean($input); } catch (ProfanityDetectedException $e) { $e->getDetectedWords(); // ['fuck'] }
Middleware
// bootstrap/app.php (Laravel 11+) ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'shush' => \ImLiaMxo\Shush\Middleware\ShushMiddleware::class, ]); }) Route::post('/comment', ...)->middleware('shush'); Route::post('/kids', ...)->middleware('shush:strict'); Route::post('/forum', ...)->middleware('shush:relaxed');
Validation Rule
use ImLiaMxo\Shush\Rules\NoProfanity; $request->validate([ 'comment' => ['required', 'string', new NoProfanity], ]);
Runtime API
Shush::setStrictness('strict'); Shush::setMode('block'); Shush::setMask('#'); Shush::setNormalise(false); Shush::addWords(['customword']); // moderate, first language Shush::addWords(['slur'], 'severe', 'en'); // specific tier + language Shush::allow(['damn']); // whitelist Shush::disallow(['cocktail']); // un-whitelist Shush::registerNormaliser('de', GermanNormaliser::class); // custom normaliser Shush::getWords(); // all dictionaries Shush::getWords('severe', 'en'); // English severe words Shush::getActiveWords(); // words at current strictness Shush::getWhitelist(); // full whitelist Shush::getStrictness(); // 'relaxed' | 'normal' | 'strict' Shush::getLanguages(); // ['en', 'pl']
Configuration Reference
| Key | Default | Description |
|---|---|---|
mode |
censor |
censor or block |
strictness |
normal |
relaxed, normal, or strict |
normalise |
true |
Enable evasion detection |
normalisers |
[] |
Custom normaliser class map ['de' => MyClass::class] |
mask |
* |
Mask character for censor mode |
mask_behaviour |
character |
character or full |
mask_replacement |
*** |
Replacement when mask_behaviour is full |
languages |
['en'] |
Dictionary folders to load |
extra_words |
[] |
Additional words (string or {word, tier, language}) |
whitelist |
[] |
Extra words to never flag (merged with built-in whitelist) |
middleware_fields |
[] |
Request fields to scan (empty = all strings) |
Architecture
src/
├── Shush.php # Core engine
├── ShushServiceProvider.php # Laravel auto-discovery
├── Facades/Shush.php # Facade
├── Exceptions/
│ └── ProfanityDetectedException.php
├── Middleware/
│ └── ShushMiddleware.php # Route middleware
├── Rules/
│ └── NoProfanity.php # Validation rule
├── Normalisers/
│ ├── NormaliserInterface.php # Contract
│ ├── BaseNormaliser.php # Shared logic
│ ├── NormaliserFactory.php # Registry / factory
│ ├── EnglishNormaliser.php # English-specific
│ └── PolishNormaliser.php # Polish-specific
└── Dictionaries/
├── en/
│ └── words.php # ~350 entries, 3 tiers
└── pl/
└── words.php # ~450 entries, 3 tiers
License
MIT