safe_text
一個用於從文字輸入中過濾不良詞彙並檢測各種格式(包括倍數詞)電話號碼的 Flutter 套件。
一個高性能的 Flutter 套件,用於過濾不當語言(粗話)和檢測電話號碼。
以下為英文專案原文快照,最新內容請造訪 GitHub。
SafeText Banner
pub version pub likes pub points CI MIT license
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.
@→a, 4→a, 3→e, 0→o, $→s, and more.******), partial (f**k), or custom replacement ([censored]).Language.english word list (~13k words) on first use; init is optional.PhoneNumberChecker runs in a separate isolate via Isolate.run.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
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
}
SafeTextFilter.initOptional. 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.resetCheck 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.filterTextSynchronous. 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).
| 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.containsBadWordAsynchronous. 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. |
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.containsPhoneNumberAsynchronous. Runs in a separate isolate via Dart's Isolate.run so it never blocks the calling thread.
Detects phone numbers expressed as:
9783444nine seven eight three four four four9 seven 8 3444nine seven eight three triple fourSupported 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. |
Pass any of these Language enum values to SafeTextFilter.init. Use Language.all to load every language simultaneously.
| 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 |
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)
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.
init is not behavior-preserving across major versions. An uninitialized filter now lazily loads the full Language.english list (~13k words); in 2.x, an uninitialized filter fell back to the small legacy constants/badwords.dart list (~1.7k words) instead. If you're upgrading from 2.x, see Keeping the legacy word list.excludedWords to suppress known false positives.Contributions are welcome! Please read CONTRIBUTING.md for the full guidelines. The short version:
develop branch.git checkout -b feature/your-featuredart analyze
dart test
develop. Ensure CI passes.For major changes, please open an issue first to discuss the approach.
SafeText uses the List of Dirty, Naughty, Obscene, and Otherwise Bad Words dataset:
We are grateful to the contributors of this dataset for providing a robust multilingual foundation.
LinkedIn • Report an Issue • Discussions • Buy me a coffee
Thanks to everyone who has contributed to SafeText!
Made with contrib.rocks