Skip to content

Censoring ​

censor returns the text with every match masked. Everything else, including spacing, punctuation and emoji, is left as it was.

dart
censor('you muji');             // 'you ****'
censor('F.U.C.K this Sh1t!');   // '******* this ****!'
censor('मुजीको क्लास');          // '*** क्लास'
censor('Great teacher!');       // 'Great teacher!'

Check and censor in one pass ​

To find out whether text has profanity and get a censored copy, use check. It scans the text once and returns a result you can read and then censor, so you don't pay for a second scan:

dart
final result = check('you muji, F.U.C.K');

result.hasProfanity;   // true
result.words;          // ['muji', 'fuck']
result.censor();       // 'you ****, *******'

Because censor() is a method on the result, you can chain it:

dart
check('you muji').censor();              // 'you ****'
check('you muji').censor(mask: '#');     // 'you ####'

A common pattern is to save the censored text and flag the original for a moderator:

dart
final result = check(commentText);

await comments.add({
  'body': result.censor(),
  'flagged': result.hasProfanity,
  'flaggedWords': result.words,
});

Calling containsProfanity(text) and then censor(text) gives the same result, but scans the text twice.

Change the mask ​

mask sets the string used in place of each character of a match. The default is *.

dart
censor('you muji', mask: '#');   // 'you ####'
censor('you muji', mask: '•');   // 'you ••••'

Custom replacements ​

replace receives each match and returns the text to put in its place. It takes precedence over mask.

dart
// A fixed label
censor('you muji', replace: (m) => '[censored]');
// 'you [censored]'

// Keep the first letter
censor('you muji', replace: (m) => m.text[0] + '*' * (m.text.length - 1));
// 'you m***'

// Grawlix
censor('you muji', replace: (m) => ('@#\$%&!' * m.text.length).substring(0, m.text.length));
// 'you @#$%'

// Wrap it for your UI instead of hiding it
censor('you muji', replace: (m) => '<mark>${m.text}</mark>');
// 'you <mark>muji</mark>'

The match is the same as the ones returned by findProfanityMatches:

FieldExampleMeaning
text'F.U.C.K'Exactly what the user typed.
normalized'fuck'The normalized form that matched.
start0Start index in the input.
end7End index in the input, exclusive.

In Flutter, to style matches rather than replace them, build a TextSpan from the positions instead; see Highlight matches.

With filter options ​

censor and check accept the same languages and strictness options as every other function:

dart
censor('fuck muji', languages: [Language.romanized]);            // 'fuck ****'
censor('you idiot', strictness: Strictness.lenient);             // 'you idiot'
check('you idiot', strictness: Strictness.lenient).censor();     // 'you idiot'

A ProfanityFilter has check and censor too:

dart
final filter = ProfanityFilter(languages: [Language.romanized, Language.devanagari]);

filter.censor('fuck muji');         // 'fuck ****'
filter.check('मुजी').censor();       // '**'

What gets masked ​

  • Exactly the characters the user typed. Positions are traced back through normalization, so leetspeak (Sh1t), ! for i (sh!!t), full-width letters (FUCK) and zero-width characters are masked in full.
  • Spelled-out words, dots included. F.U.C.K becomes *******.
  • One mask character per visible character. Devanagari is counted by what you see, not by code unit, so मुजी becomes **, not ****.
  • Whitespace inside a phrase is kept. sasto manche becomes ***** ******.
  • Overlapping matches are merged. chaak ko pwal is a phrase that contains the word chaak, and it's masked once, as ***** ** ****.
  • Postpositions are masked with the word. mujiko becomes ******, because the filter reads it as one word.