safe_text 3.0.2
safe_text: ^3.0.2 copied to clipboard
A fast Dart profanity & swear word filter with phone number detection, powered by Aho-Corasick. Supports 80+ languages and 55,000+ curated bad words.
A high-performance pure Dart package for filtering offensive language (profanity) and detecting phone numbers. Powered by the Aho-Corasick algorithm for O(N) single-pass scanning across 80+ languages and 55,000+ curated words.
π Find SafeText useful? A like on pub.flutter-io.cn or star on GitHub helps others discover it.
Table of Contents #
- Table of Contents
- Features
- Installation
- Quick Start
- API Reference
- Supported Languages
- How it Works
- Migrating from v1.x
- Limitations
- Contributing
- Data Source
- Authors
- Contributors
Features #
- Scans thousands of bad words in a single pass of the input text.
- Catches common character substitutions:
@βa,4βa,3βe,0βo,$βs, and more. - Detects phone numbers in digits, words, mixed formats, and multiplier words (e.g., "triple five").
- Multiple masking strategies β full (
******), partial (f**k), or custom replacement ([censored]). - Customizable β add your own words or exclude specific phrases.
- No setup required β lazily auto-initializes with the full
Language.englishword list (~13k words) on first use;initis optional. - Non-blocking β
PhoneNumberCheckerruns in a separate isolate viaIsolate.run. - Works on Android, iOS, Web, macOS, Linux, and Windows.
Installation #
Add safe_text to your project using the Dart CLI:
dart pub add safe_text
Or manually add it to your pubspec.yaml:
dependencies:
safe_text: ^3.0.0
Then run:
dart pub get
Quick Start #
import 'package:safe_text/safe_text.dart';
void main() async {
// Optional: initialize once at app startup with a specific language.
// If you skip this, the filter lazily auto-initializes with the full
// Language.english word list (lib/data/en.dart, ~13k words) on first use β
// not the small legacy constants/badwords.dart list from v1.x/v2.x.
SafeTextFilter.init(language: Language.english);
// Filter profanity (full masking β default)
final clean = SafeTextFilter.filterText(text: "What the f@ck!");
print(clean); // "What the ****!"
// Partial masking β keeps first & last characters for 4+ letter words
final partial = SafeTextFilter.filterText(
text: "What the f@ck!",
strategy: const MaskStrategy.partial(),
);
print(partial); // "What the f**k!"
// Custom replacement
final custom = SafeTextFilter.filterText(
text: "What the f@ck!",
strategy: const MaskStrategy.custom(replacement: '[censored]'),
);
print(custom); // "What the [censored]!"
// Check for bad words
final hasBad = SafeTextFilter.containsBadWord(text: "Some bad input");
print(hasBad); // true or false
// Detect phone numbers
final hasPhone = await PhoneNumberChecker.containsPhoneNumber(
text: "Call me at nine 7 eight 3 triple four",
);
print(hasPhone); // true
}
API Reference #
SafeTextFilter.init #
Optional. Builds the Aho-Corasick trie from the selected word list(s). If you never call it, the filter lazily auto-initializes with Language.english (lib/data/en.dart, ~13k words) on first use of filterText / containsBadWord. Call it explicitly when you want a specific language or combination.
Upgrading from v2.x? In 2.x, an app that never called
initfell back to the small legacy list inconstants/badwords.dart(~1.7k words). As of 3.0.0, an uninitialized filter now lazily loads the fullLanguage.englishlist instead β a much larger, different word list. If your app relied on the old, smaller list (e.g."adult"was previously not flagged), see below for how to opt back into it.
// Single language
SafeTextFilter.init(language: Language.english);
// Custom combination
SafeTextFilter.init(languages: [Language.english, Language.hindi, Language.spanish]);
// All 75+ languages
SafeTextFilter.init(language: Language.all);
| Parameter | Type | Default | Description |
|---|---|---|---|
language |
Language? |
Language.english |
A single language to load. Use Language.all to load every language. Ignored when languages is provided. |
languages |
List<Language>? |
null |
A custom list of languages. Takes precedence over language. |
Note: If neither parameter is provided, the filter defaults to
Language.english.
SafeTextFilter.isInitialized & SafeTextFilter.reset #
Check initialization status or reset loaded word lists dynamically (e.g., when switching languages). Because init auto-initializes on first use, you generally don't need to guard calls with isInitialized β but it's available if you want to check, and reset() lets you reload with a different language:
// Reset state to reload with a different language
SafeTextFilter.reset();
SafeTextFilter.init(language: Language.spanish);
SafeTextFilter.filterText #
Synchronous. Returns the input text with matched bad words masked according to the chosen MaskStrategy.
// Full masking (default)
String result = SafeTextFilter.filterText(
text: "Hello b4dass world!",
extraWords: ["badterm"], // optional: add custom words
excludedWords: ["bass"], // optional: never filter these
useDefaultWords: true, // use the built-in word list
);
// Result: "Hello ****** world!"
// Partial masking
String partial = SafeTextFilter.filterText(
text: "Hello b4dass world!",
strategy: const MaskStrategy.partial(),
);
// Result: "Hello b****s world!"
// Custom replacement
String custom = SafeTextFilter.filterText(
text: "Hello b4dass world!",
strategy: const MaskStrategy.custom(), // defaults to "[censored]"
);
// Result: "Hello [censored] world!"
| Parameter | Type | Default | Description |
|---|---|---|---|
text |
String |
required | The input string to process. |
extraWords |
List<String>? |
null |
Additional words to filter on top of (or instead of) the built-in list. |
excludedWords |
List<String>? |
null |
Words that must never be filtered, even if they appear in the list. |
useDefaultWords |
bool |
true |
Include the built-in language word list. Set to false to use only extraWords. |
strategy |
MaskStrategy? |
null (defaults to MaskStrategy.full()) |
Masking strategy. See Masking Strategies below. |
fullMode |
bool |
true |
Deprecated. Use strategy instead. true maps to MaskStrategy.full(), false maps to MaskStrategy.partial(). |
obscureSymbol |
String |
* |
Deprecated. Pass obscureSymbol via MaskStrategy.full() or MaskStrategy.partial() instead. |
Precedence: When
strategyis provided, it takes full precedence over the deprecatedfullModeandobscureSymbolparameters. Whenstrategyis omitted,fullMode: truemaps toMaskStrategy.full(obscureSymbol: obscureSymbol)andfullMode: falsemaps toMaskStrategy.partial(obscureSymbol: obscureSymbol).
Masking Strategies
| Strategy | Constructor | Output Example | Description |
|---|---|---|---|
| Full | MaskStrategy.full(obscureSymbol: '*') |
badass β ****** |
Replaces every character with the obscure symbol. |
| Partial | MaskStrategy.partial(obscureSymbol: '*') |
damn β d**n, ass β a** |
Keeps first character visible. For 4+ letter words, also keeps the last character. |
| Custom | MaskStrategy.custom(replacement: '[censored]') |
badass β [censored] |
Replaces the entire word with a fixed string. |
Note:
obscureSymbolmust be exactly one character. This is enforced viaassertin debug mode β a multi-character string will trigger anAssertionErrorduring development.
SafeTextFilter.containsBadWord #
Asynchronous. Returns true if the text contains at least one filtered word.
bool hasBadWord = SafeTextFilter.containsBadWord(
text: "Don't be a pendejo",
extraWords: ["badterm"], // optional
excludedWords: ["pend"], // optional
useDefaultWords: true, // optional
);
| Parameter | Type | Default | Description |
|---|---|---|---|
text |
String |
required | The input string to check. |
extraWords |
List<String>? |
null |
Additional words to check against. |
excludedWords |
List<String>? |
null |
Words to ignore even if matched. |
useDefaultWords |
bool |
true |
Include the built-in word list in the check. |
Keeping the legacy word list
If your app depends on the smaller v1.x/v2.x fallback list (constants/badwords.dart, ~1.7k words) instead of the full Language.english list (~13k words) that 3.0.0 lazily auto-initializes with, opt out of the default list and pass the legacy one in explicitly:
import 'package:safe_text/constants/badwords.dart';
bool hasBadWord = SafeTextFilter.containsBadWord(
text: text,
useDefaultWords: false,
extraWords: badWords, // the legacy list from constants/badwords.dart
);
PhoneNumberChecker.containsPhoneNumber #
Asynchronous. Runs in a separate isolate via Dart's Isolate.run so it never blocks the calling thread.
Detects phone numbers expressed as:
- Pure digits:
9783444 - Word-based:
nine seven eight three four four four - Mixed:
9 seven 8 3444 - Multiplier words:
nine seven eight three triple four
Supported multiplier words: double, triple, quadruple, quintuple, sextuple, septuple, octuple, nonuple, decuple.
bool hasPhone = await PhoneNumberChecker.containsPhoneNumber(
text: "Call me at nine 7 eight 3 triple four",
minLength: 7, // minimum digit count to be considered a phone number
maxLength: 15, // maximum digit count
);
| Parameter | Type | Default | Description |
|---|---|---|---|
text |
String |
required | The input string to check. |
minLength |
int |
7 |
Minimum number of digits for a valid phone number. |
maxLength |
int |
15 |
Maximum number of digits for a valid phone number. |
Supported Languages #
Pass any of these Language enum values to SafeTextFilter.init. Use Language.all to load every language simultaneously.
View all 82 supported languages
| Enum | Language |
|---|---|
Language.afrikaans |
Afrikaans |
Language.amharic |
Amharic |
Language.arabic |
Arabic |
Language.azerbaijani |
Azerbaijani |
Language.belarusian |
Belarusian |
Language.bulgarian |
Bulgarian |
Language.catalan |
Catalan |
Language.cebuano |
Cebuano |
Language.czech |
Czech |
Language.welsh |
Welsh |
Language.danish |
Danish |
Language.german |
German |
Language.dzongkha |
Dzongkha |
Language.greek |
Greek |
Language.english |
English |
Language.esperanto |
Esperanto |
Language.spanish |
Spanish |
Language.estonian |
Estonian |
Language.basque |
Basque |
Language.persian |
Persian |
Language.finnish |
Finnish |
Language.filipino |
Filipino |
Language.french |
French |
Language.scottishGaelic |
Scottish Gaelic |
Language.galician |
Galician |
Language.hindi |
Hindi |
Language.croatian |
Croatian |
Language.hungarian |
Hungarian |
Language.armenian |
Armenian |
Language.indonesian |
Indonesian |
Language.icelandic |
Icelandic |
Language.italian |
Italian |
Language.japanese |
Japanese |
Language.kabyle |
Kabyle |
Language.kannada |
Kannada |
Language.khmer |
Khmer |
Language.korean |
Korean |
Language.latin |
Latin |
Language.lithuanian |
Lithuanian |
Language.latvian |
Latvian |
Language.maori |
Maori |
Language.macedonian |
Macedonian |
Language.malayalam |
Malayalam |
Language.mongolian |
Mongolian |
Language.marathi |
Marathi |
Language.malay |
Malay |
Language.maltese |
Maltese |
Language.burmese |
Burmese |
Language.dutch |
Dutch |
Language.norwegian |
Norwegian |
Language.norfuk |
Norfuk / Pitcairn |
Language.piapoco |
Piapoco |
Language.polish |
Polish |
Language.portuguese |
Portuguese |
Language.romanian |
Romanian |
Language.kriol |
Kriol |
Language.russian |
Russian |
Language.slovak |
Slovak |
Language.slovenian |
Slovenian |
Language.samoan |
Samoan |
Language.albanian |
Albanian |
Language.serbian |
Serbian |
Language.swedish |
Swedish |
Language.tamil |
Tamil |
Language.telugu |
Telugu |
Language.tetum |
Tetum |
Language.thai |
Thai |
Language.klingon |
Klingon |
Language.tongan |
Tongan |
Language.turkish |
Turkish |
Language.ukrainian |
Ukrainian |
Language.uzbek |
Uzbek |
Language.vietnamese |
Vietnamese |
Language.yiddish |
Yiddish |
Language.chinese |
Chinese |
Language.zulu |
Zulu |
Language.bengali |
Bengali |
Language.gujarati |
Gujarati |
Language.punjabi |
Punjabi |
Language.swahili |
Swahili |
Language.urdu |
Urdu |
Language.all |
All of the above |
How it Works #
Legacy approach (v1.x): For each bad word in a list of 10,000+ words, run a separate regex scan over the entire input β O(W Γ N) where W is the word count.
v2.0.0 approach: The Aho-Corasick algorithm builds a Finite State Automaton (Trie) once from the entire word list. The engine then scans the input exactly once, matching all patterns simultaneously in O(N) time where N is the length of the text β regardless of how many words are in the list.
Input text βββΊ [Normalizer] βββΊ [Aho-Corasick FSA] βββΊ Match ranges βββΊ [StringBuffer] βββΊ Filtered text
(leet-speak) (single O(N) pass) (merged) (single-pass)
Migrating from v1.x #
The original SafeText class is still available but marked @Deprecated. It internally delegates to the new classes. Migrate when ready:
| v1.x | v2.0.0 |
|---|---|
SafeTextFilter.init(...) |
Recommended β call once at startup. If skipped, containsBadWord/filterText fall back to the small legacy constants/badwords.dart list (~1.7k words) instead of the full multilingual dataset. |
SafeText.filterText(text: ...) |
SafeTextFilter.filterText(text: ...) |
await SafeText.containsBadWord(text: ...) |
SafeTextFilter.containsBadWord(text: ...) |
await SafeText.containsPhoneNumber(text: ...) |
await PhoneNumberChecker.containsPhoneNumber(text: ...) |
Before:
// v1.x β no init required, but slow
bool bad = await SafeText.containsBadWord(text: "some input");
After:
// v2.0.0 β init once, then use anywhere
SafeTextFilter.init(language: Language.english); // once, e.g. in main()
bool bad = SafeTextFilter.containsBadWord(text: "some input");
3.0.0 changed this again:
initbecame fully optional. If you never call it, the filter now lazily auto-initializes with the fullLanguage.englishlist (~13k words) β not the small legacy list above. See Keeping the legacy word list if you're upgrading from 2.x and relied on the smaller fallback.
Limitations #
- Skipping
initis not behavior-preserving across major versions. An uninitialized filter now lazily loads the fullLanguage.englishlist (~13k words); in 2.x, an uninitialized filter fell back to the small legacyconstants/badwords.dartlist (~1.7k words) instead. If you're upgrading from 2.x, see Keeping the legacy word list. - Phone number detection is English-word based. Words like "nine", "triple", etc. are English only β the detector does not parse written numbers in other languages.
- False positives on technical terms. Short words in the filter list may match substrings of unrelated technical terms. Use
excludedWordsto suppress known false positives.
Contributing #
Contributions are welcome! Please read CONTRIBUTING.md for the full guidelines. The short version:
- Clone the repo and check out the
developbranch. - Create a feature branch:
git checkout -b feature/your-feature - Add tests for any new behaviour.
- Run checks before submitting:
dart analyze dart test - Open a pull request targeting
develop. Ensure CI passes.
For major changes, please open an issue first to discuss the approach.
Data Source #
SafeText uses the List of Dirty, Naughty, Obscene, and Otherwise Bad Words dataset:
- 80+ dialects and languages
- 55,000+ curated words
We are grateful to the contributors of this dataset for providing a robust multilingual foundation.
Authors #
LinkedIn β’ Report an Issue β’ Discussions β’ Buy me a coffee
Contributors #
Thanks to everyone who has contributed to SafeText!
Made with contrib.rocks
