Skip to main content

Multi-language profanity detection and filtering engine.

Project description

terlik

Note: This is the Python port of the original terlik.js project.

Multi-language profanity detection and filtering engine for Python. Designed Turkish-first and extensible to any language. Not a naive blacklist — a multi-layered normalization and pattern engine that catches what simple string matching misses.

Ships with Turkish (flagship, full coverage), English, Spanish, and German built-in. Add any language easily, or extend at runtime via extend_dictionary.

Turkçe: Türkçe öncelikli, her dile genişletilebilir küfür tespit ve filtreleme motoru. Leet speak, karakter tekrarı, ayırıcı karakterler ve Türkçe ek sistemi desteği ile yaratıcı küfür denemelerini yakalar. Typed, hızlı ve modern Python tasarımı.

Features

  • Extensible to any language — ships with TR/EN/ES/DE, add more via language packs
  • Catches leet speak, separators, char repetition, mixed case, zero-width chars
  • Turkish suffix engine (83 suffixes, ~10,000+ detectable forms from 147 roots)
  • Three detection modes: strict, balanced, loose (with fuzzy matching)
  • Fully typed with Python type hints for great IDE support
  • Lazy compilation: regex patterns are compiled on first use
  • ReDoS-safe regex patterns with timeout safety net
  • Background thread warmup support available

Why terlik?

Turkish profanity evasion is creative. Users write s2k, $1kt1r, s.i.k.t.i.r, SİKTİR, siiiiiktir, i8ne, or*spu, pu$ttt, 6öt — and expect to get away with it. Turkish is agglutinative — a single root like sik spawns dozens of forms: siktiler, sikerim, siktirler, sikimsonik. Manually listing every variant doesn't scale.

terlik catches all of these with a suffix engine that automatically recognizes Turkish grammatical suffixes on profane roots. Here's what a single call handles:

from terlik import Terlik

terlik = Terlik()
cleaned = terlik.clean("s2mle yüzle$ g0t_v3r3n o r o s p u pezev3nk i8ne pu$ttt or*spu")
print(cleaned)
# "***** yüzle$ ********* *********** ******** **** ****** ******"

Install

pip install terlik

(Note: Requires Python 3.9 or higher)

Quick Start

from terlik import Terlik
from terlik.types import TerlikOptions

# Turkish (default)
tr = Terlik()
tr.contains_profanity("siktir git")  # True
tr.clean("siktir git burdan")        # "****** git burdan"

# English
en = Terlik(TerlikOptions(language="en"))
en.contains_profanity("what the fuck") # True
en.contains_profanity("siktir git")    # False (Turkish not loaded in English setup)

# Spanish & German
es = Terlik(TerlikOptions(language="es"))
de = Terlik(TerlikOptions(language="de"))
es.contains_profanity("hijo de puta")  # True
de.contains_profanity("scheiße")       # True

What It Catches

Evasion technique Example Detected as
Plain text siktir sik
Turkish İ/I SİKTİR sik
Leet speak $1kt1r, @pt@l sik, aptal
Visual leet (TR) 8ok, 6öt, i8ne, s2k bok, göt, ibne, sik
Turkish number words s2mle (s+iki+mle) sik (sikimle)
Separators s.i.k.t.i.r, s_i_k sik
Spaces o r o s p u orospu
Char repetition siiiiiktir, pu$ttt sik, puşt
Mixed punctuation or*spu, g0t_v3r3n orospu, göt
Combined $1kt1r g0t_v3r3n both caught
Suffix forms siktiler, orospuluk, gotune sik, orospu, göt
Suffix + evasion s.i.k.t.i.r.l.e.r, $1kt1rler sik
Suffix chaining siktirler (sik+tir+ler) sik
Deep agglutination siktiğimin, sikermisiniz, siktirmişcesine sik
Zero-width chars s\u200Bi\u200Bk\u200Bt\u200Bi\u200Br (ZWSP/ZWNJ/ZWJ) sik

What It Doesn't Catch (on purpose)

Whitelist prevents false positives on legitimate words:

terlik.contains_profanity("Amsterdam")   # False
terlik.contains_profanity("sikke")       # False (Ottoman coin)
terlik.contains_profanity("ambulans")    # False
terlik.contains_profanity("siklet")      # False (boxing weight class)
terlik.contains_profanity("memur")       # False

Options

Configure Terlik with TerlikOptions:

from terlik import Terlik
from terlik.types import TerlikOptions

options = TerlikOptions(
    language="tr",                # built-in: "tr" | "en" | "es" | "de" (default: "tr")
    mode="balanced",              # "strict" | "balanced" | "loose"
    mask_style="stars",           # "stars" | "partial" | "replace"
    replace_mask="[***]",         # mask text for "replace" style
    custom_list=["customword"],   # additional words to detect
    whitelist=["safeword"],       # additional words to whitelist
    enable_fuzzy=False,           # enable fuzzy matching
    fuzzy_threshold=0.8,          # similarity threshold (0-1)
    fuzzy_algorithm="levenshtein",# "levenshtein" | "dice"
    max_length=10000,             # truncate input beyond this characters
    background_warmup=False,      # compile patterns in background thread
    extend_dictionary=None,       # DictionaryData object to merge
    disable_leet_decode=False,    # skip leet-speak decoding
    disable_compound=False,       # skip CamelCase decompounding pass
)

terlik = Terlik(options)

Detection Modes

Mode What it does Best for
strict Normalize + exact match only Minimum false positives
balanced Normalize + pattern matching with separator/leet tolerance General use (default)
loose Pattern + fuzzy matching (Levenshtein or Dice) Maximum coverage, typo tolerance

Dictionary Strategy

terlik ships with a deliberately narrow dictionary — the goal is to minimize false positives while catching real-world evasion patterns. A root like sik with 83 possible suffixes, leet decoding, separator tolerance, and repeat collapse produces thousands of detectable surface forms.

# Add domain-specific words
terlik.add_words(["customSlang", "anotherWord"])

# Or at construction time
terlik = Terlik(TerlikOptions(
    custom_list=["customSlang", "anotherWord"],
    whitelist=["legitimateWord"],
))

# Remove a built-in word if it causes false positives in your domain
terlik.remove_words(["damn"])

API

terlik.contains_profanity(text: str, options: Optional[DetectOptions] = None) -> bool

Quick boolean check. Runs full detection internally and returns True if any match exists.

terlik.get_matches(text: str, options: Optional[DetectOptions] = None) -> List[MatchResult]

Returns all matches with details:

# MatchResult DataClass Details:
# match.word      (str) -> matched text from original input
# match.root      (str) -> dictionary root word
# match.index     (int) -> position in original text
# match.severity  (Severity) -> "high" | "medium" | "low"
# match.category  (Category) -> "sexual" | "insult" | "slur" | "general"
# match.method    (MatchMethod) -> "exact" | "pattern" | "fuzzy"

terlik.clean(text: str, options: Optional[CleanOptions] = None) -> str

Returns text with profanity masked. Three styles:

terlik.clean("siktir git")                                            # "****** git"
terlik.clean("siktir git", CleanOptions(mask_style="partial"))        # "s****r git"
terlik.clean("siktir git", CleanOptions(mask_style="replace", replace_mask="[küfür]")) # "[küfür] git"

Terlik.warmup(languages: Optional[List[str]] = None, base_options: Optional[TerlikOptions] = None) -> Dict[str, Terlik]

Static method. Creates and warm-ups instances for multiple languages at once.

cache = Terlik.warmup(["tr", "en", "es", "de"])
cache["en"].contains_profanity("fuck") # True — instantly ready without cold starts

Testing & Contributing

If you'd like to contribute word lists or logic:

# Set up a venv and install dependencies
python -m venv venv
source venv/bin/activate
pip install -e ".[dev]"

# Run tests
pytest

Pre-commit hooks and type checking (via mypy) ensure code quality.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

terlik-0.1.0.tar.gz (44.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

terlik-0.1.0-py3-none-any.whl (33.1 kB view details)

Uploaded Python 3

File details

Details for the file terlik-0.1.0.tar.gz.

File metadata

  • Download URL: terlik-0.1.0.tar.gz
  • Upload date:
  • Size: 44.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.4

File hashes

Hashes for terlik-0.1.0.tar.gz
Algorithm Hash digest
SHA256 778d58bf136a4e0386a97c4ffe5908f9ab04160432f444144359a80d3ec9becb
MD5 1fd7f469e23d4f1bcec41853dc4320b2
BLAKE2b-256 4bf3880ba4277a784af00aa540c633a74978f9517d3c0528f8e5924ade3fb5ef

See more details on using hashes here.

File details

Details for the file terlik-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: terlik-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 33.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.4

File hashes

Hashes for terlik-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 634e8d25d0ff310cf715affb252ec501c6ece7cbb6089ec48216378e54de2a0d
MD5 499324d9ce4b948563964b8756b2f60b
BLAKE2b-256 c4c54f8b13f9765e3f6903ea60261551218ac6589b7d1d38362bb329335a868d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page