Skip to content

Repository files navigation

SafeText Banner

pub versionpub likespub pointsCIMIT license

platforms

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.dev or star on GitHub helps others discover it.


Table of Contents


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 English on first use; init is optional.
  • Non-blocking — PhoneNumberChecker runs in a separate isolate via Isolate.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';
voidmain() async {
// Optional: initialize once at app startup with a specific language.// If you skip this, the filter lazily auto-initializes with English on// first use.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 wordsfinal partial =SafeTextFilter.filterText(
text:"What the f@ck!",
strategy:constMaskStrategy.partial(),
);
print(partial); // "What the f**k!"// Custom replacementfinal custom =SafeTextFilter.filterText(
text:"What the f@ck!",
strategy:constMaskStrategy.custom(replacement:'[censored]'),
);
print(custom); // "What the [censored]!"// Check for bad wordsfinal hasBad =SafeTextFilter.containsBadWord(text:"Some bad input");
print(hasBad); // true or false// Detect phone numbersfinal hasPhone =awaitPhoneNumberChecker.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 on first use of filterText / containsBadWord. Call it explicitly when you want a specific language or combination.

// Single languageSafeTextFilter.init(language:Language.english);
// Custom combinationSafeTextFilter.init(languages: [Language.english, Language.hindi, Language.spanish]);
// All 75+ languagesSafeTextFilter.init(language:Language.all);
ParameterTypeDefaultDescription
languageLanguage?Language.englishA single language to load. Use Language.all to load every language. Ignored when languages is provided.
languagesList<Language>?nullA 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 languageSafeTextFilter.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 maskingString partial =SafeTextFilter.filterText(
text:"Hello b4dass world!",
strategy:constMaskStrategy.partial(),
);
// Result: "Hello b****s world!"// Custom replacementString custom =SafeTextFilter.filterText(
text:"Hello b4dass world!",
strategy:constMaskStrategy.custom(), // defaults to "[censored]"
);
// Result: "Hello [censored] world!"
ParameterTypeDefaultDescription
textStringrequiredThe input string to process.
extraWordsList<String>?nullAdditional words to filter on top of (or instead of) the built-in list.
excludedWordsList<String>?nullWords that must never be filtered, even if they appear in the list.
useDefaultWordsbooltrueInclude the built-in language word list. Set to false to use only extraWords.
strategyMaskStrategy?null (defaults to MaskStrategy.full())Masking strategy. See Masking Strategies below.
fullModebooltrueDeprecated. Use strategy instead. true maps to MaskStrategy.full(), false maps to MaskStrategy.partial().
obscureSymbolString*Deprecated. Pass obscureSymbol via MaskStrategy.full() or MaskStrategy.partial() instead.

Precedence: When strategy is provided, it takes full precedence over the deprecated fullMode and obscureSymbol parameters. When strategy is omitted, fullMode: true maps to MaskStrategy.full(obscureSymbol: obscureSymbol) and fullMode: false maps to MaskStrategy.partial(obscureSymbol: obscureSymbol).

Masking Strategies

StrategyConstructorOutput ExampleDescription
FullMaskStrategy.full(obscureSymbol: '*')badass******Replaces every character with the obscure symbol.
PartialMaskStrategy.partial(obscureSymbol: '*')damnd**n, assa**Keeps first character visible. For 4+ letter words, also keeps the last character.
CustomMaskStrategy.custom(replacement: '[censored]')badass[censored]Replaces the entire word with a fixed string.

Note:obscureSymbol must be exactly one character. This is enforced via assert in debug mode — a multi-character string will trigger an AssertionError during 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
);
ParameterTypeDefaultDescription
textStringrequiredThe input string to check.
extraWordsList<String>?nullAdditional words to check against.
excludedWordsList<String>?nullWords to ignore even if matched.
useDefaultWordsbooltrueInclude the built-in word list in the check.

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 =awaitPhoneNumberChecker.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
);
ParameterTypeDefaultDescription
textStringrequiredThe input string to check.
minLengthint7Minimum number of digits for a valid phone number.
maxLengthint15Maximum 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
EnumLanguage
Language.afrikaansAfrikaans
Language.amharicAmharic
Language.arabicArabic
Language.azerbaijaniAzerbaijani
Language.belarusianBelarusian
Language.bulgarianBulgarian
Language.catalanCatalan
Language.cebuanoCebuano
Language.czechCzech
Language.welshWelsh
Language.danishDanish
Language.germanGerman
Language.dzongkhaDzongkha
Language.greekGreek
Language.englishEnglish
Language.esperantoEsperanto
Language.spanishSpanish
Language.estonianEstonian
Language.basqueBasque
Language.persianPersian
Language.finnishFinnish
Language.filipinoFilipino
Language.frenchFrench
Language.scottishGaelicScottish Gaelic
Language.galicianGalician
Language.hindiHindi
Language.croatianCroatian
Language.hungarianHungarian
Language.armenianArmenian
Language.indonesianIndonesian
Language.icelandicIcelandic
Language.italianItalian
Language.japaneseJapanese
Language.kabyleKabyle
Language.kannadaKannada
Language.khmerKhmer
Language.koreanKorean
Language.latinLatin
Language.lithuanianLithuanian
Language.latvianLatvian
Language.maoriMaori
Language.macedonianMacedonian
Language.malayalamMalayalam
Language.mongolianMongolian
Language.marathiMarathi
Language.malayMalay
Language.malteseMaltese
Language.burmeseBurmese
Language.dutchDutch
Language.norwegianNorwegian
Language.norfukNorfuk / Pitcairn
Language.piapocoPiapoco
Language.polishPolish
Language.portuguesePortuguese
Language.romanianRomanian
Language.kriolKriol
Language.russianRussian
Language.slovakSlovak
Language.slovenianSlovenian
Language.samoanSamoan
Language.albanianAlbanian
Language.serbianSerbian
Language.swedishSwedish
Language.tamilTamil
Language.teluguTelugu
Language.tetumTetum
Language.thaiThai
Language.klingonKlingon
Language.tonganTongan
Language.turkishTurkish
Language.ukrainianUkrainian
Language.uzbekUzbek
Language.vietnameseVietnamese
Language.yiddishYiddish
Language.chineseChinese
Language.zuluZulu
Language.bengaliBengali
Language.gujaratiGujarati
Language.punjabiPunjabi
Language.swahiliSwahili
Language.urduUrdu
Language.allAll 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.xv2.0.0
SafeTextFilter.init(...)Optional — auto-initializes with English on first use
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 slowbool bad =awaitSafeText.containsBadWord(text:"some input");

After:

// v2.0.0 — init is optional; auto-initializes with English on first useSafeTextFilter.init(language:Language.english); // optional, e.g. for a specific languagebool bad =SafeTextFilter.containsBadWord(text:"some input");

Limitations

  • 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 excludedWords to suppress known false positives.

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for the full guidelines. The short version:

  1. Clone the repo and check out the develop branch.
  2. Create a feature branch: git checkout -b feature/your-feature
  3. Add tests for any new behaviour.
  4. Run checks before submitting:
    dart analyze
    dart test
  5. 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

Ronit Rameja
Ronit Rameja

LinkedInReport an IssueDiscussionsBuy me a coffee


Contributors

Thanks to everyone who has contributed to SafeText!

Contributors

Made with contrib.rocks

About

A high-performance Flutter package for filtering offensive language (profanity) and detecting phone numbers.

Topics

Resources

Contributing

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages