{"id":46106428,"url":"https://github.com/badursun/terlik.js","last_synced_at":"2026-03-06T01:00:54.708Z","repository":{"id":341201522,"uuid":"1168844136","full_name":"badursun/terlik.js","owner":"badursun","description":"Ultra-fast multi-language profanity filter, designed Turkish-first and extensible to any language. Catches leet speak, agglutination \u0026 evasion patterns. Zero deps, TypeScript, 35 KB.","archived":false,"fork":false,"pushed_at":"2026-03-03T12:11:55.000Z","size":1317,"stargazers_count":26,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-04T00:38:04.310Z","etag":null,"topics":["agglutination","bad-words","censor","chat-filter","content-moderation","edge-computing","extensible","javascript","moderation","multi-language","nodejs","nsfw","profanity-detection","profanity-filter","regex","text-filter","turkish","typescript","word-filter","zero-dependency"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/terlik.js","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/badursun.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"badursun"}},"created_at":"2026-02-27T21:32:49.000Z","updated_at":"2026-03-03T14:34:45.000Z","dependencies_parsed_at":"2026-03-02T22:00:56.013Z","dependency_job_id":null,"html_url":"https://github.com/badursun/terlik.js","commit_stats":null,"previous_names":["badursun/terlik.js"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/badursun/terlik.js","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/badursun%2Fterlik.js","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/badursun%2Fterlik.js/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/badursun%2Fterlik.js/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/badursun%2Fterlik.js/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/badursun","download_url":"https://codeload.github.com/badursun/terlik.js/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/badursun%2Fterlik.js/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30101617,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-04T23:59:36.199Z","status":"ssl_error","status_checked_at":"2026-03-04T23:56:48.556Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agglutination","bad-words","censor","chat-filter","content-moderation","edge-computing","extensible","javascript","moderation","multi-language","nodejs","nsfw","profanity-detection","profanity-filter","regex","text-filter","turkish","typescript","word-filter","zero-dependency"],"created_at":"2026-03-01T21:03:04.495Z","updated_at":"2026-03-05T00:00:25.399Z","avatar_url":"https://github.com/badursun.png","language":"TypeScript","funding_links":["https://github.com/sponsors/badursun"],"categories":[],"sub_categories":[],"readme":"# terlik.js\n\n![terlik.js](assets/social-preview.png)\n\n[![CI](https://github.com/badursun/terlik.js/actions/workflows/ci.yml/badge.svg)](https://github.com/badursun/terlik.js/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/terlik.js.svg)](https://www.npmjs.com/package/terlik.js)\n[![npm downloads](https://img.shields.io/npm/dm/terlik.js.svg)](https://www.npmjs.com/package/terlik.js)\n[![npm bundle size](https://img.shields.io/bundlephobia/minzip/terlik.js)](https://bundlephobia.com/package/terlik.js)\n[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)\n[![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)]()\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nMulti-language profanity detection and filtering engine, 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.\n\nShips with **Turkish** (flagship, full coverage), **English**, **Spanish**, and **German** built-in. Add any language with a folder and two files, or extend at runtime via `extendDictionary`.\n\n\u003e **Turkce:** Turkce oncelikli, her dile genisletilebilir kufur tespit ve filtreleme motoru. Leet speak, karakter tekrari, ayirici karakterler ve Turkce ek sistemi destegi ile yaratici kufur denemelerini yakalar. Sifir bagimlilik, TypeScript, 35 KB.\n\n## Features\n\n- **Extensible to any language** — ships with TR/EN/ES/DE, add more via language packs or `extendDictionary`\n- Catches leet speak, separators, char repetition, mixed case, zero-width chars\n- Turkish suffix engine (83 suffixes, ~3,000+ detectable forms from 25 roots)\n- Three detection modes: strict, balanced, loose (with fuzzy matching)\n- Zero dependencies, **35 KB** gzipped\n- ESM + CJS — works in Node.js, Bun, Deno, browsers, Cloudflare Workers, Edge runtimes\n- Lazy compilation: ~1.5ms construction, \u003c1ms per check after warmup\n- ReDoS-safe regex patterns with timeout safety net\n- Full TypeScript support with exported types\n\n## Why terlik.js?\n\nTurkish 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.\n\nterlik.js catches all of these with a **suffix engine** that automatically recognizes Turkish grammatical suffixes on profane roots. Here's what a single call handles:\n\n```ts\nimport { Terlik } from \"terlik.js\";\nconst terlik = new Terlik();\n\nterlik.clean(\"s2mle yüzle$ g0t_v3r3n o r o s p u pezev3nk i8ne pu$ttt or*spu\");\n// \"***** yüzle$ ********* *********** ******** **** ****** ******\"\n// 7 matches, 0 false positives, \u003c2ms\n```\n\n## Install\n\n```bash\nnpm install terlik.js\n# or\npnpm add terlik.js\n# or\nyarn add terlik.js\n```\n\n## Quick Start\n\n```ts\nimport { Terlik } from \"terlik.js\";\n\n// Turkish (default)\nconst tr = new Terlik();\ntr.containsProfanity(\"siktir git\");  // true\ntr.clean(\"siktir git burdan\");       // \"****** git burdan\"\n\n// English\nconst en = new Terlik({ language: \"en\" });\nen.containsProfanity(\"what the fuck\"); // true\nen.containsProfanity(\"siktir git\");    // false (Turkish not loaded)\n\n// Spanish \u0026 German\nconst es = new Terlik({ language: \"es\" });\nconst de = new Terlik({ language: \"de\" });\nes.containsProfanity(\"hijo de puta\");  // true\nde.containsProfanity(\"scheiße\");       // true\n```\n\n## What It Catches\n\n| Evasion technique | Example | Detected as |\n|---|---|---|\n| Plain text | `siktir` | sik |\n| Turkish İ/I | `SİKTİR` | sik |\n| Leet speak | `$1kt1r`, `@pt@l` | sik, aptal |\n| Visual leet (TR) | `8ok`, `6öt`, `i8ne`, `s2k` | bok, göt, ibne, sik |\n| Turkish number words | `s2mle` (s+iki+mle) | sik (sikimle) |\n| Separators | `s.i.k.t.i.r`, `s_i_k` | sik |\n| Spaces | `o r o s p u` | orospu |\n| Char repetition | `siiiiiktir`, `pu$ttt` | sik, puşt |\n| Mixed punctuation | `or*spu`, `g0t_v3r3n` | orospu, göt |\n| Combined | `$1kt1r g0t_v3r3n` | both caught |\n| **Suffix forms** | `siktiler`, `orospuluk`, `gotune` | sik, orospu, göt |\n| **Suffix + evasion** | `s.i.k.t.i.r.l.e.r`, `$1kt1rler` | sik |\n| **Suffix chaining** | `siktirler` (sik+tir+ler) | sik |\n| **Deep agglutination** | `siktiğimin`, `sikermisiniz`, `siktirmişcesine` | sik |\n| **Zero-width chars** | `s\\u200Bi\\u200Bk\\u200Bt\\u200Bi\\u200Br` (ZWSP/ZWNJ/ZWJ) | sik |\n\n### What It Doesn't Catch (on purpose)\n\nWhitelist prevents false positives on legitimate words:\n\n```ts\nterlik.containsProfanity(\"Amsterdam\");    // false\nterlik.containsProfanity(\"sikke\");        // false (Ottoman coin)\nterlik.containsProfanity(\"ambulans\");     // false\nterlik.containsProfanity(\"siklet\");       // false (boxing weight class)\nterlik.containsProfanity(\"memur\");        // false\nterlik.containsProfanity(\"malzeme\");      // false\nterlik.containsProfanity(\"ama\");          // false (conjunction)\nterlik.containsProfanity(\"amir\");         // false\nterlik.containsProfanity(\"dolmen\");       // false\n```\n\n## How It Works\n\nSix-stage normalization pipeline (language-aware), then pattern matching:\n\n```\ninput\n  → lowercase (locale-aware: \"tr\", \"en\", \"es\", \"de\")\n  → char folding (language-specific: İ→i, ñ→n, ß→ss, ä→a, ...)\n  → number expansion (optional, e.g. Turkish: s2k → sikik)\n  → leet speak decode (0→o, 1→i, @→a, $→s, ...)\n  → punctuation removal (between letters: s.i.k → sik)\n  → repeat collapse (siiiiik → sik)\n  → pattern matching (dynamic regex with language-specific char classes)\n  → whitelist filtering\n  → result\n```\n\nEach language has its own char map, leet map, char classes, and optional number expansions. The engine is language-agnostic — only the data is language-specific. This means **any language can be added** without modifying the core engine.\n\nFor suffixable roots, the engine appends an optional suffix group (up to 2 chained suffixes). Turkish has 83 suffixes (including question particles and adverbial forms), English has 8, Spanish has 13, German has 8.\n\n### Language Packs\n\nCommunity contributions to existing language packs (new words, variants, whitelist entries) and entirely new language packs are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for step-by-step instructions.\n\nEach language lives in its own folder under `src/lang/`:\n\n```\nsrc/lang/\n  tr/\n    config.ts           ← charMap, leetMap, charClasses, locale\n    dictionary.json     ← entries, suffixes, whitelist\n  en/\n    config.ts\n    dictionary.json\n  ...\n```\n\nDictionary format (community-friendly JSON, no TypeScript needed):\n\n```json\n{\n  \"version\": 1,\n  \"suffixes\": [\"ing\", \"ed\", \"er\", \"s\"],\n  \"entries\": [\n    { \"root\": \"fuck\", \"variants\": [\"fucking\", \"fucker\"], \"severity\": \"high\", \"category\": \"sexual\", \"suffixable\": true }\n  ],\n  \"whitelist\": [\"assassin\", \"class\", \"grass\"]\n}\n```\n\nCategories: `sexual`, `insult`, `slur`, `general`. Severity: `high`, `medium`, `low`.\n\n### Adding a New Language\n\n1. Create `src/lang/xx/` folder\n2. Add `dictionary.json` (entries, suffixes, whitelist)\n3. Add `config.ts` (locale, charMap, leetMap, charClasses)\n4. Register in `src/lang/index.ts` (one import line)\n5. Write tests, build, done\n\n## Dictionary Strategy\n\nterlik.js ships with a **deliberately narrow dictionary** — the goal is to **minimize false positives** while catching real-world evasion patterns. The dictionary is not a massive word list; it's a curated set of roots + variants that the pattern engine expands through normalization, leet decoding, separator tolerance, and suffix chaining.\n\n### Coverage\n\n| Language | Status | Roots | Explicit Variants | Suffixes | Whitelist | Effective Forms |\n|---|---|---|---|---|---|---|\n| Turkish | Flagship | 25 | 88 | 83 | 52 | ~3,000+ |\n| English | Community | 23 | 106 | 8 | 42 | ~700+ |\n| Spanish | Community | 19 | 73 | 13 | 15 | ~500+ |\n| German | Community | 18 | 48 | 8 | 3 | ~300+ |\n\n\"Effective forms\" = roots × normalization variants × suffix combinations × evasion patterns. A root like `sik` with 83 possible suffixes, leet decoding, separator tolerance, and repeat collapse produces thousands of detectable surface forms.\n\n\u003e **Add your language!** The engine is language-agnostic. See [Adding a New Language](#adding-a-new-language) or use [`extendDictionary`](#extenddictionary-option) for runtime extension.\n\n### What IS Covered\n\n- **Core profanity roots** per language (high-severity sexual, insults, slurs)\n- **Grammatical inflections** via suffix engine (Turkish agglutination, English -ing/-ed, etc.)\n- **Evasion patterns**: leet speak, separators, repetition, mixed case, number words (TR)\n- **Compound forms**: `orospucocugu`, `motherfucker`, `hijoputa`, `hurensohn`\n\n### What is NOT Covered (by design)\n\n- **Slang / regional variants** that change rapidly — better handled with `customList`\n- **Context-dependent words** that are profane only in certain contexts\n- **Phonetic substitutions** beyond leet (e.g., \"phuck\") — add via `customList`\n- **New coinages** — use `addWords()` at runtime\n\n### Why Narrow?\n\nA large dictionary maximizes recall but tanks precision. In production chat systems, **false positives are worse than false negatives** — blocking \"class\" or \"grass\" because the dictionary is too broad erodes user trust. terlik.js defaults to high precision and lets you widen coverage per your needs:\n\n\u003e **The `sık`/`sik` paradox:** Turkish `sık` (frequent/tight) normalizes to `sik` because `ı→i` char folding is required to catch evasions like `s1kt1r`. Making `sik` suffix-aware would flag `sıkıntı` (trouble), `sıkma` (squeeze), `sıkı` (tight) — extremely common words. Instead, deep agglutination forms like `siktiğimin` and `sikermisiniz` are added as explicit variants. This is a deliberate precision-over-recall tradeoff.\n\n```ts\n// Add domain-specific words\nterlik.addWords([\"customSlang\", \"anotherWord\"]);\n\n// Or at construction time\nconst terlik = new Terlik({\n  customList: [\"customSlang\", \"anotherWord\"],\n  whitelist: [\"legitimateWord\"],\n});\n\n// Remove a built-in word if it causes false positives in your domain\nterlik.removeWords([\"damn\"]);\n```\n\n## Performance\n\n### Lazy Compilation\n\nterlik.js uses **lazy compilation** — `new Terlik()` is near-instant (~1.5ms). Regex patterns are compiled on the first `detect()` call, not at construction time. This eliminates startup cost when creating multiple instances.\n\n| Phase | Cost | When |\n|---|---|---|\n| `new Terlik()` | **~1.5ms** | Construction (lookup tables only) |\n| First `detect()` | ~200-700ms | Lazy regex compilation + V8 JIT warmup |\n| Subsequent calls | **\u003c1ms** | Patterns cached, JIT optimized |\n\n**Where do you want to pay the compilation cost?**\n\n```ts\n// Option A: Background warmup (recommended for servers)\n// Construction is instant. Patterns compile in the next event loop tick.\n// If a request arrives before warmup finishes, it compiles synchronously.\nconst terlik = new Terlik({ backgroundWarmup: true });\n\napp.post(\"/chat\", (req, res) =\u003e {\n  const cleaned = terlik.clean(req.body.message); // \u003c1ms (warmup already done)\n});\n```\n\n```ts\n// Option B: Explicit warmup at startup\nconst terlik = new Terlik();\nterlik.containsProfanity(\"warmup\"); // Forces compilation here\n\napp.post(\"/chat\", (req, res) =\u003e {\n  const cleaned = terlik.clean(req.body.message); // \u003c1ms\n});\n```\n\n```ts\n// Option C: Lazy (pay on first request)\nconst terlik = new Terlik(); // ~1.5ms\n\napp.post(\"/chat\", (req, res) =\u003e {\n  const cleaned = terlik.clean(req.body.message); // First call: ~500ms, then \u003c1ms\n});\n```\n\n```ts\n// Option D: Multi-language warmup\nconst cache = Terlik.warmup([\"tr\", \"en\", \"es\", \"de\"]);\n\napp.post(\"/chat\", (req, res) =\u003e {\n  const lang = req.body.language;\n  const cleaned = cache.get(lang)!.clean(req.body.message); // \u003c1ms\n});\n```\n\n\u003e **Important:** Never create `new Terlik()` per request. A single cached instance handles requests in microseconds.\n\n\u003e **Serverless (Lambda, Vercel, Cloudflare Workers):** Do NOT use `backgroundWarmup`. The `setTimeout` callback may never fire because serverless runtimes freeze the process between invocations. Use explicit warmup instead: `const t = new Terlik(); t.containsProfanity(\"warmup\");` at module scope.\n\n### Throughput\n\nBenchmark results (Apple Silicon, single core, msgs/sec):\n\n| Scenario | msgs/sec |\n|---|---|\n| Clean messages (no matches) | ~193,000 |\n| Mixed messages (balanced mode) | ~151,000 |\n| Suffixed dirty messages | ~142,000 |\n| Strict mode | ~390,000 |\n| Loose mode (with fuzzy) | ~8,400 |\n\n\u003e **Note:** Loose/fuzzy mode is ~18x slower than balanced mode due to O(n*m) similarity computation. Use it only when typo tolerance is critical, not as a default.\n\n### Accuracy\n\nMeasured on a labeled corpus of 388 samples across 4 languages (profane + clean + whitelist + edge cases):\n\n| Language | Mode | Precision | Recall | F1 | FPR | FNR |\n|---|---|---|---|---|---|---|\n| TR | strict | 100.0% | 88.6% | 93.9% | 0.0% | 11.4% |\n| TR | **balanced** | **100.0%** | **100.0%** | **100.0%** | **0.0%** | **0.0%** |\n| TR | loose | 99.1% | 100.0% | 99.5% | 1.6% | 0.0% |\n| EN | strict | 100.0% | 95.5% | 97.7% | 0.0% | 4.5% |\n| EN | **balanced** | **100.0%** | **98.5%** | **99.2%** | **0.0%** | **1.5%** |\n| EN | loose | 98.5% | 98.5% | 98.5% | 2.0% | 1.5% |\n| ES | strict | 100.0% | 96.7% | 98.3% | 0.0% | 3.3% |\n| ES | **balanced** | **100.0%** | **96.7%** | **98.3%** | **0.0%** | **3.3%** |\n| ES | loose | 100.0% | 96.7% | 98.3% | 0.0% | 3.3% |\n| DE | strict | 100.0% | 100.0% | 100.0% | 0.0% | 0.0% |\n| DE | **balanced** | **100.0%** | **100.0%** | **100.0%** | **0.0%** | **0.0%** |\n| DE | loose | 100.0% | 100.0% | 100.0% | 0.0% | 0.0% |\n\n**Mode characteristics:**\n- **Strict** — highest precision (0% FP), trades recall for safety. Misses some suffixed forms and evasion patterns.\n- **Balanced** — best overall F1. Catches evasion patterns while keeping FPR near zero. **Recommended for production.**\n- **Loose** — adds fuzzy matching. Slightly higher FPR due to similarity matches on borderline words.\n\nReproduce: `pnpm bench:accuracy` — outputs per-category breakdown, failure list, and JSON results.\n\n## Options\n\n```ts\nconst terlik = new Terlik({\n  language: \"tr\",                // built-in: \"tr\" | \"en\" | \"es\" | \"de\" (default: \"tr\")\n  mode: \"balanced\",              // \"strict\" | \"balanced\" | \"loose\"\n  maskStyle: \"stars\",            // \"stars\" | \"partial\" | \"replace\"\n  replaceMask: \"[***]\",          // mask text for \"replace\" style\n  customList: [\"customword\"],    // additional words to detect\n  whitelist: [\"safeword\"],       // additional words to whitelist\n  enableFuzzy: false,            // enable fuzzy matching\n  fuzzyThreshold: 0.8,           // similarity threshold (0-1). 0.8 ≈ 1 typo per 5 chars\n  fuzzyAlgorithm: \"levenshtein\", // \"levenshtein\" | \"dice\"\n  maxLength: 10000,              // truncate input beyond this\n  backgroundWarmup: false,       // compile patterns in background via setTimeout\n  extendDictionary: undefined,   // DictionaryData object to merge with built-in dictionary\n});\n```\n\n## Detection Modes\n\n| Mode | What it does | Best for |\n|---|---|---|\n| `strict` | Normalize + exact match only | Minimum false positives |\n| `balanced` | Normalize + pattern matching with separator/leet tolerance | **General use (default)** |\n| `loose` | Pattern + fuzzy matching (Levenshtein or Dice) | Maximum coverage, typo tolerance |\n\n## API\n\n### `terlik.containsProfanity(text, options?): boolean`\n\nQuick boolean check. Runs full detection internally and returns `true` if any match exists.\n\n### `terlik.getMatches(text, options?): MatchResult[]`\n\nReturns all matches with details:\n\n```ts\ninterface MatchResult {\n  word: string;       // matched text from original input\n  root: string;       // dictionary root word\n  index: number;      // position in original text\n  severity: \"high\" | \"medium\" | \"low\";\n  method: \"exact\" | \"pattern\" | \"fuzzy\";\n}\n```\n\n### `terlik.clean(text, options?): string`\n\nReturns text with profanity masked. Three styles:\n\n```ts\nterlik.clean(\"siktir git\");                                    // \"****** git\"\nterlik.clean(\"siktir git\", { maskStyle: \"partial\" });          // \"s****r git\"\nterlik.clean(\"siktir git\", { maskStyle: \"replace\" });          // \"[***] git\"\n```\n\n### `terlik.addWords(words) / removeWords(words)`\n\nRuntime dictionary modification. Recompiles patterns automatically.\n\n```ts\nterlik.addWords([\"customword\"]);\nterlik.containsProfanity(\"customword\"); // true\n\nterlik.removeWords([\"salak\"]);\nterlik.containsProfanity(\"salak\"); // false\n```\n\n### `Terlik.warmup(languages, options?): Map\u003cstring, Terlik\u003e`\n\nStatic method. Creates and JIT-warms instances for multiple languages at once.\n\n```ts\nconst cache = Terlik.warmup([\"tr\", \"en\", \"es\", \"de\"]);\ncache.get(\"en\")!.containsProfanity(\"fuck\"); // true — no cold start\n```\n\n### `extendDictionary` Option\n\nMerge an external dictionary with the built-in one. Useful for teams managing custom word lists without modifying the core package:\n\n```ts\nconst terlik = new Terlik({\n  extendDictionary: {\n    version: 1,\n    suffixes: [\"ci\", \"cu\"],\n    entries: [\n      { root: \"customword\", variants: [\"cust0mword\"], severity: \"high\", category: \"general\", suffixable: true },\n    ],\n    whitelist: [\"safeterm\"],\n  },\n});\n\nterlik.containsProfanity(\"customword\");    // true\nterlik.containsProfanity(\"customwordci\");  // true (suffix match)\nterlik.containsProfanity(\"safeterm\");      // false (whitelisted)\nterlik.containsProfanity(\"siktir\");        // true (built-in still works)\n```\n\nThe extension dictionary must follow the same schema as built-in dictionaries. Duplicate roots are skipped; suffixes and whitelist entries are merged. Pattern cache is disabled for extended instances.\n\n### `terlik.language: string`\n\nRead-only property. Returns the language code of the instance.\n\n### `getSupportedLanguages(): string[]`\n\nReturns all available language codes.\n\n```ts\nimport { getSupportedLanguages } from \"terlik.js\";\ngetSupportedLanguages(); // [\"tr\", \"en\", \"es\", \"de\"]\n```\n\n### `normalize(text): string`\n\nStandalone export. Uses Turkish locale by default.\n\n```ts\nimport { normalize, createNormalizer } from \"terlik.js\";\n\nnormalize(\"S.İ.K.T.İ.R\"); // \"siktir\" (Turkish default)\n\n// Custom normalizer for any language\nconst deNormalize = createNormalizer({\n  locale: \"de\",\n  charMap: { ä: \"a\", ö: \"o\", ü: \"u\", ß: \"ss\" },\n  leetMap: { \"0\": \"o\", \"3\": \"e\" },\n});\ndeNormalize(\"Scheiße\"); // \"scheisse\"\n```\n\n## Testing\n\n874 tests covering all built-in languages, 25 Turkish root words, suffix detection, lazy compilation, multi-language isolation, normalization, fuzzy matching, cleaning, integration, ReDoS hardening, attack surface coverage, external dictionary merging, and edge cases:\n\n```bash\npnpm test          # run once\npnpm test:watch    # watch mode\n```\n\n### Live Test Server\n\nAn interactive browser-based test environment is included. Chat interface on the left, real-time process log on the right — see exactly what terlik.js does at each step (normalization, pattern matching, match details, timing).\n\n```bash\npnpm dev:live      # http://localhost:2026\n```\n\nSee [`tools/README.md`](./tools/README.md) for details.\n\n### Integration Guide\n\nSee [**Integration Guide**](./docs/integration-guide.md) for Express, Fastify, Next.js, Nuxt, Socket.io, and multi-language server examples.\n\n## Development\n\n```bash\npnpm install          # install dependencies\npnpm test             # run tests\npnpm test:coverage    # run tests with coverage report\npnpm typecheck        # TypeScript type checking\npnpm build            # build ESM + CJS output\npnpm bench            # run performance benchmarks\npnpm dev:live         # start interactive test server\n```\n\nPre-commit hooks (via Husky) automatically run type checking on staged `.ts` files.\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines.\n\n## Changelog\n\nSee [CHANGELOG.md](./CHANGELOG.md) for the full version history.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbadursun%2Fterlik.js","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbadursun%2Fterlik.js","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbadursun%2Fterlik.js/lists"}