Documentation · API · Guides · Changelog · Contributing · Bahasa Indonesia
Keep chat, comments and usernames civil in Bahasa Indonesia, and stay in control of what counts as "bad".
People type g0bl0k or GOBLOK (both caught by default), or spaced-out g-o-b-l-o-k (caught with the opt-in evasion policy), to slip past a filter. Your team also needs to let a name like Baju Bodo through, treat a mild word differently from a severe one, and send doubtful cases to a person. Indonesian Badwords does all of that, and it reports what it found instead of guessing what the writer meant.
npm install indonesian-badwordsimport { Badwords } from 'indonesian-badwords';
const checker = new Badwords();
checker.flag('terima kasih'); // => false
checker.flag('dasar g0bl0k!'); // => true
checker.censor('dasar g0bl0k!'); // => 'dasar ***!'Requires Node.js 22+ (browser bundles need ES2022 and Intl.Segmenter). ESM, CommonJS and TypeScript types are included, with zero runtime dependencies.
| Capability | How to use it |
|---|---|
Whole-word matching with Unicode, case, width and invisible-character normalization, plus leetspeak (g0bl0k) |
On by default (mode: 'word', normalization: 'standard') |
| A conservative default list, an expanded list, and your own words | dictionary, expandedDictionary, new Badwords({ dictionary: [...] }) |
| Reviewed lists, selected by category, severity and region | selectDictionary() from indonesian-badwords/metadata |
How each match was found: exact, normalized, leetspeak, evasion or fuzzy |
via on every item from find() and analyze().locations |
Category, editorial severity, region, and a 0-100 score with a level |
classify() from indonesian-badwords/metadata |
| Inserted separators, stretched letters and lookalike characters (opt-in, bounded) | boundedEvasion() from indonesian-badwords/evasion |
| Typos and swapped letters (opt-in, bounded) | boundedFuzzy() from indonesian-badwords/fuzzy |
| Exceptions at the narrowest scope: a phrase, or a range in one call | allowPhrases option, ignoreRanges call option |
Censoring with ***, mask(), grawlix() or your own callback |
indonesian-badwords/censor |
| Exact UTF-16 offsets into your original text, for highlights | find(), analyze() |
| Precision, recall and false positives on your own labelled examples, and a before/after comparison of results | evaluate(), compare() from indonesian-badwords/evaluate |
| Typed errors and finite work limits on untrusted input | maxMatches and the limits of each policy |
Methods: flag, find, badwords, censor, filter, analyze. Evasion and fuzzy matching are off unless you pass a policy.
- Correctness first. A false positive is a bug. The default list leaves out words with everyday meanings, such as
anjingandbabi; broader matching is something you opt into. - No guessing from context. The library says what matched and how. Your app decides what to do about it. A
scorerates the matched words, never the intent of a message. - Control at the narrowest scope. Change one word, one phrase, one category or one call, not the whole checker.
- Reliable on untrusted input. Results are deterministic, work is bounded, and a limit throws a typed error instead of quietly returning "clean".
- Fast. Chat-sized checks take microseconds, and optional features never grow the default import.
Every occurrence carries via, so one pass can treat sure matches and guesses differently.
import { Badwords } from 'indonesian-badwords';
import { boundedFuzzy } from 'indonesian-badwords/fuzzy';
const checker = new Badwords({ dictionary: ['goblok'], fuzzy: boundedFuzzy() });
const review: string[] = [];
const output = checker.censor('goblok g0bl0k goblox', (context) => {
const kinds = context.occurrences().map(({ via }) => via.kind);
if (kinds.every((kind) => kind === 'fuzzy')) {
review.push(context.match);
return context.match;
}
return '***';
});
output; // => '*** *** goblox'
review; // => ['goblox']classify() adds category, severity and region to each match and summarizes the message. level is none, unrated, mild, moderate, strong or extreme. A level of unrated (with a score of null) means words matched but have no severity, such as custom words. It is never clean.
import { Badwords } from 'indonesian-badwords';
import { classify } from 'indonesian-badwords/metadata';
const checker = new Badwords();
function decide(message: string): 'allow' | 'mask' | 'review' | 'block' {
const result = classify(checker.analyze(message));
if (result.level === 'none') return 'allow';
// Matched but unrated is never clean: a person looks at it.
if (result.level === 'unrated' || result.summary.unrated + result.summary.unclassified > 0) return 'review';
return (result.score ?? 0) >= 60 ? 'block' : 'mask';
}
decide('terima kasih'); // => 'allow'
decide('dasar g0bl0k!'); // => 'mask'
decide('dasar gebleg'); // => 'review'How the score and level are defined
import { Badwords } from 'indonesian-badwords';
import { selectDictionary } from 'indonesian-badwords/metadata';
const checker = new Badwords({
dictionary: selectDictionary({ categories: ['insult'], minSeverity: 2 }),
});
checker.flag('dasar goblok'); // => trueSelectors are profiles, categories, regions and minSeverity (1 to 4). Regional vocabulary includes Javanese, such as jancuk, and Sundanese, such as kehed. Severity describes a dictionary entry, not a whole message. Dictionary guide.
g0bl0k and full-width text already match with no setup. Evasion and fuzzy policies catch more, and also match ordinary words: with maxEdits: 1, bangsa, kampung and kontrol are each one edit from a listed word. Test on text from your app, and send fuzzy matches to review (first recipe) instead of censoring them.
import { Badwords } from 'indonesian-badwords';
import { boundedEvasion, latinLookalikes } from 'indonesian-badwords/evasion';
import { boundedFuzzy } from 'indonesian-badwords/fuzzy';
const tolerant = new Badwords({
fuzzy: boundedFuzzy({ model: 'osa', maxEdits: 1 }),
evasion: boundedEvasion({
separators: { characters: [' ', '.', '_', '-'], maxGap: 1, maxTotal: 8 },
repetitions: { maxExtraPerLetter: 2, maxTotal: 8 },
confusables: { mappings: latinLookalikes, maxTotal: 2 },
}),
});
tolerant.censor('dasar g-o-b-l-o-x!'); // => 'dasar ***!'Evasion options · Fuzzy matching
import { Badwords } from 'indonesian-badwords';
import { mask } from 'indonesian-badwords/censor';
const plain = new Badwords();
plain.censor('dasar g0bl0k!', mask({ keepFirst: 1, keepLast: 1 })); // => 'dasar g****k!'
plain.censor('dasar g0bl0k!', ({ words }) => `[${words[0]}]`); // => 'dasar [goblok]!'
const checker = new Badwords({ allowPhrases: ['Baju Bodo'] });
checker.censor('Baju Bodo goblok bodo'); // => 'Baju Bodo *** ***'
checker.censor('goblok\nBaju Bodo goblok', '***', { ignoreRanges: [{ index: 0, end: 7 }] }); // => 'goblok\nBaju Bodo ***'An allowed phrase exempts only itself, never the rest of the message. Censoring · Exceptions
index and end are UTF-16 offsets into the text you passed in, and match equals text.slice(index, end), even after normalization.
import { Badwords } from 'indonesian-badwords';
const checker = new Badwords();
checker.find('👩🏽💻 g0bl0k!').map(({ word, index, end, match }) => ({ word, index, end, match }));
// [{ word: 'goblok', index: 8, end: 14, match: 'g0bl0k' }]Measured with the default checker on Node.js 26.10.0 (AMD Ryzen 7 9700X, Linux; warmed synthetic medians, setup excluded).
| Clean ASCII chat check 280 characters |
Default browser bundle minified + gzip |
Runtime dependencies |
| Workload | Operation | Median per call |
|---|---|---|
| Clean ASCII paragraph, 4,000 characters | flag |
5.17 µs |
| Clean ASCII paragraph, 4,000 characters | censor |
5.13 µs |
| Accents, emoji, and one literal match, 4,000 UTF-16 units | flag |
29.65 µs |
Optional policies have their own costs. Run npm run benchmark from the repository to measure your runtime. Methodology and full results.
Documentation index · API reference · Dictionaries and metadata · Chat moderation guide · Troubleshooting · llms.txt
Missing a word or getting a false positive? Open an issue with the example, the result you expected, and a similar benign case. For dictionary additions, include sources showing the abusive usage. Use the dictionary report form and the contribution guide.
Semoga bermanfaat 😉