basa 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
basa/core/slang.py ADDED
@@ -0,0 +1,2071 @@
1
+ """
2
+ BASA - Slang Normalization Module
3
+ ===================================
4
+ Converts informal/colloquial Indonesian text (including Javanese/Sundanese
5
+ particles) into standardized Indonesian.
6
+
7
+ Supported categories:
8
+ - Pronouns (aku/kamu shortcuts)
9
+ - Kinship & address terms (kk, bokap, nyokap, dll)
10
+ - Negation (ga/gak/nggak → tidak)
11
+ - Compound negation (gamau, gabisa, gatau → tidak mau, dll)
12
+ - Conjunctions & prepositions (abbreviations like yg, dgn, krn)
13
+ - Common verbs (udah, blm, mo, tau, liat, dll)
14
+ - Adjectives & adverbs (bgt, bener, lumayan, dll)
15
+ - Question words (knp, gmn, kmn, dll)
16
+ - Greetings & responses (makasih, ok, sip, tq, dll)
17
+ - Temporal & location (skrg, ntr, kmrn, sini, dll)
18
+ - Internet slang → Indonesian equivalent (btw, lol, omg, dll)
19
+ - E-commerce & finance (ongkir, rekber, tf, dll)
20
+ - Youth & Gen-Z slang (mager, baper, gabut, dll)
21
+ - Discourse markers (mksd, intinya, menurutku, dll)
22
+ - Javanese extended (mangan, turu, apik, dll)
23
+ - Sundanese extended (abdi, anjeun, geus, dll)
24
+ - Nouns (temen, hp, laptop, dll)
25
+ - Health terms (dmm, btk, opname, dll)
26
+ - Emotions & expressions (galau, baper, kesal, ghosting, dll)
27
+ - Food & drink (nasgor, kopsu, laper, kenyang, dll)
28
+ - Clothing & fashion (ootd, thrifting, hoodie, dll)
29
+ - Transportation (ojol, krl, macet, nebeng, dll)
30
+ - Religion & culture (alhamdulillah, bismillah, ultah, dll)
31
+ - Education extended (bimbel, ospek, maba, snbt, dll)
32
+ - Work & office (wfh, deadline, lembur, resign, dll)
33
+ - Numbers & quantity (1rb, 5jt, rata2, tiba2, dll)
34
+ - Compound expressions (ngapain, otw ke, kapan2, dll)
35
+ - Particles (lah, deh, nih, dong, kok, sih, lho, pun, dll)
36
+
37
+ Usage:
38
+ from basa.core.slang import slang
39
+
40
+ # Basic usage (convenience singleton)
41
+ result = slang.normalize("gw gamau pergi krn lg baper bgt")
42
+ # → "saya tidak mau pergi karena sedang bawa perasaan banget"
43
+
44
+ # Inspect all supported slang words
45
+ words = slang.supported_words()
46
+ # → {"adek": "adik", "ak": "aku", "ane": "saya", ...}
47
+
48
+ # Count total entries
49
+ len(slang)
50
+ # → 120+
51
+
52
+ # With custom entries
53
+ from basa.core.slang import SlangNormalizer
54
+ normalizer = SlangNormalizer(custom_mapping={"jancok": "ekspresi"})
55
+ result = normalizer.normalize("gw jancok kaget")
56
+
57
+ # Batch processing
58
+ texts = ["gw udah makan", "km blm tidur?"]
59
+ results = slang.normalize_batch(texts)
60
+
61
+ Notes:
62
+ - All replacements are lowercased (standard for NLP preprocessing).
63
+ - Word boundaries (\\b) prevent partial-word substitution.
64
+ - Slang keys sorted longest-first to prevent premature partial matches.
65
+ - Repeated character normalization distinguishes vowels vs consonants
66
+ to avoid breaking abbreviations like "kk", "dll", "mm".
67
+ """
68
+
69
+ import re
70
+
71
+ # ─────────────────────────────────────────────────────────────────────────────
72
+ # SLANG DICTIONARY (organized by category for easier maintenance)
73
+ # ─────────────────────────────────────────────────────────────────────────────
74
+
75
+ # First & second person pronouns
76
+ _PRONOUNS: dict[str, str] = {
77
+ # First person singular
78
+ "aq": "aku",
79
+ "ak": "aku",
80
+ "akuh": "aku",
81
+ "q": "aku",
82
+ "sy": "saya",
83
+ "w": "saya",
84
+ "gw": "saya",
85
+ "gue": "saya",
86
+ "gua": "saya",
87
+ "ane": "saya", # Betawi/Jakartan slang
88
+ "ana": "saya", # Arabic-influenced
89
+ "wkwk": "tertawa", # common filler — override happens in internet
90
+ # First person possessive
91
+ "gw nya": "saya punya",
92
+ "gue nya": "saya punya",
93
+ # Second person
94
+ "u": "kamu",
95
+ "lu": "kamu",
96
+ "lo": "kamu",
97
+ "loe": "kamu",
98
+ "elo": "kamu",
99
+ "elu": "kamu",
100
+ "km": "kamu",
101
+ "ente": "kamu", # Betawi/Jakartan slang
102
+ "antum": "kamu", # Arabic-influenced (plural polite)
103
+ "sampeyan": "kamu", # Javanese polite
104
+ "sampean": "kamu", # Javanese casual
105
+ "sampyan": "kamu", # Javanese casual variant
106
+ # Third person
107
+ "dy": "dia",
108
+ "dya": "dia",
109
+ "doi": "dia", # Jakartan slang for girlfriend/boyfriend
110
+ # Plural
111
+ "mrk": "mereka",
112
+ "qt": "kita",
113
+ "kln": "kalian",
114
+ # Reflexive
115
+ "sndiri": "sendiri",
116
+ "sndr": "sendiri",
117
+ }
118
+
119
+ # Kinship & address terms
120
+ _KINSHIP: dict[str, str] = {
121
+ "kk": "kakak",
122
+ "kak": "kakak",
123
+ "adk": "adik",
124
+ "adek": "adik",
125
+ "bpk": "bapak",
126
+ "bpak": "bapak",
127
+ "bapk": "bapak",
128
+ "ayh": "ayah",
129
+ "klg": "keluarga",
130
+ "klrg": "keluarga",
131
+ "kel": "keluarga",
132
+ "ortu": "orang tua",
133
+ "ortuku": "orang tuaku",
134
+ "bokap": "ayah",
135
+ "nyokap": "ibu",
136
+ "pakde": "paman",
137
+ "bude": "bibi",
138
+ "om": "paman",
139
+ "tante": "bibi",
140
+ }
141
+
142
+ # Standalone negation words
143
+ _NEGATION: dict[str, str] = {
144
+ "g": "tidak",
145
+ "ga": "tidak",
146
+ "gak": "tidak",
147
+ "gk": "tidak",
148
+ "ngga": "tidak",
149
+ "nggak": "tidak",
150
+ "engga": "tidak",
151
+ "enggak": "tidak",
152
+ "kagak": "tidak",
153
+ "nope": "tidak",
154
+ "nop": "tidak",
155
+ "gakk": "tidak",
156
+ "gaknya": "tidaknya",
157
+ "bkn": "bukan",
158
+ "bknnya": "bukannya",
159
+ "jgn": "jangan",
160
+ "jgnan": "jangan",
161
+ "belom": "belum",
162
+ "belom2": "belum-belum",
163
+ "blom": "belum",
164
+ "blm2": "belum-belum",
165
+ "masih blm": "masih belum",
166
+ }
167
+
168
+ # Compound negation — written as one word without spaces (very common in chat)
169
+ # e.g. "gamau" = "ga" + "mau" = "tidak mau"
170
+ # NOTE: These MUST be registered before single-char negation in the dict so
171
+ # that the longest-first sort in _compile_pattern() handles them correctly.
172
+ _COMPOUND_NEGATION: dict[str, str] = {
173
+ "gamau": "tidak mau",
174
+ "gmau": "tidak mau",
175
+ "gabisa": "tidak bisa",
176
+ "gbisa": "tidak bisa",
177
+ "gakbisa": "tidak bisa",
178
+ "gapunya": "tidak punya",
179
+ "gpunya": "tidak punya",
180
+ "gasuka": "tidak suka",
181
+ "gsuka": "tidak suka",
182
+ "gaada": "tidak ada",
183
+ "gada": "tidak ada", # gaada → gada after vowel reduction
184
+ "gatau": "tidak tahu",
185
+ "gtau": "tidak tahu",
186
+ "gatahu": "tidak tahu",
187
+ "gaperlu": "tidak perlu",
188
+ "gperlu": "tidak perlu",
189
+ "gaboleh": "tidak boleh",
190
+ "gboleh": "tidak boleh",
191
+ "gamakan": "tidak makan",
192
+ "gaminum": "tidak minum",
193
+ "gadatang": "tidak datang",
194
+ "gamasuk": "tidak masuk",
195
+ "gasempet": "tidak sempat",
196
+ "gabawa": "tidak bawa",
197
+ "gabayar": "tidak bayar",
198
+ "gaberani": "tidak berani",
199
+ "gabalik": "tidak balik",
200
+ "gabener": "tidak benar",
201
+ "gakuat": "tidak kuat",
202
+ "gamungkin": "tidak mungkin",
203
+ }
204
+
205
+ # Conjunctions, prepositions, and formal abbreviations
206
+ _CONJUNCTIONS: dict[str, str] = {
207
+ "tp": "tapi",
208
+ "tpi": "tapi",
209
+ "trs": "terus",
210
+ "trus": "terus",
211
+ "dr": "dari",
212
+ "sm": "sama",
213
+ "ama": "sama",
214
+ "dpt": "dapat",
215
+ "tdk": "tidak",
216
+ "krn": "karena",
217
+ "krna": "karena",
218
+ "karna": "karena",
219
+ "utk": "untuk",
220
+ "bwt": "buat",
221
+ "spy": "supaya",
222
+ "klo": "kalau",
223
+ "kalo": "kalau",
224
+ "yg": "yang",
225
+ "dgn": "dengan",
226
+ "dg": "dengan",
227
+ "jg": "juga",
228
+ "bkn": "bukan",
229
+ "pd": "pada",
230
+ "dlm": "dalam",
231
+ # Formal abbreviations
232
+ "dll": "dan lain-lain",
233
+ "dsb": "dan sebagainya",
234
+ "dst": "dan seterusnya",
235
+ "tsb": "tersebut",
236
+ "yth": "yang terhormat",
237
+ "nb": "catatan",
238
+ # Time-relation conjunctions
239
+ "stlh": "setelah",
240
+ "sblm": "sebelum",
241
+ "slma": "selama",
242
+ "tntg": "tentang",
243
+ "ttng": "tentang",
244
+ "thd": "terhadap",
245
+ "antr": "antara",
246
+ "srt": "serta",
247
+ "kmdian": "kemudian",
248
+ "kmdn": "kemudian",
249
+ "lalu": "kemudian",
250
+ # Formal/business abbreviations
251
+ "ttd": "tanda tangan",
252
+ "an": "atas nama",
253
+ "up": "untuk perhatian",
254
+ "re": "balasan",
255
+ "fwd": "teruskan",
256
+ "fw": "teruskan",
257
+ "lmk": "beri tahu saya",
258
+ "etc": "dan lain-lain",
259
+ "ps": "catatan tambahan",
260
+ "no": "nomor",
261
+ }
262
+
263
+ # Verbs and auxiliary verbs
264
+ _VERBS: dict[str, str] = {
265
+ "udh": "sudah",
266
+ "sdh": "sudah",
267
+ "dah": "sudah",
268
+ "udah": "sudah",
269
+ "udeh": "sudah",
270
+ "blm": "belum",
271
+ "blum": "belum",
272
+ "lg": "sedang",
273
+ "lgi": "sedang",
274
+ "bs": "bisa",
275
+ "bsa": "bisa",
276
+ "jd": "jadi",
277
+ "mo": "mau",
278
+ "mw": "mau",
279
+ "tau": "tahu",
280
+ "taw": "tahu",
281
+ "pake": "pakai",
282
+ "make": "pakai",
283
+ "mkn": "makan",
284
+ "mkan": "makan",
285
+ "liat": "lihat",
286
+ "liad": "lihat",
287
+ "ngerti": "mengerti",
288
+ "ngarti": "mengerti",
289
+ "nanya": "bertanya",
290
+ "nany": "bertanya",
291
+ "nyari": "mencari",
292
+ "nyoba": "mencoba",
293
+ "ngomong": "berbicara",
294
+ "nunggu": "menunggu",
295
+ "lakuin": "lakukan",
296
+ "byr": "bayar",
297
+ "hrs": "harus",
298
+ "blh": "boleh",
299
+ "prlu": "perlu",
300
+ "blg": "bilang",
301
+ "bilng": "bilang",
302
+ "sk": "suka",
303
+ "dtg": "datang",
304
+ "dateng": "datang",
305
+ # Extended verbs
306
+ "bikin": "membuat",
307
+ "bkin": "membuat",
308
+ "ambl": "ambil",
309
+ "simpen": "simpan",
310
+ "smpen": "simpan",
311
+ "masukin": "masukkan",
312
+ "keluarin": "keluarkan",
313
+ "tambahin": "tambahkan",
314
+ "kurangin": "kurangi",
315
+ "hapusin": "hapuskan",
316
+ "benerin": "betulkan",
317
+ "gnti": "ganti",
318
+ "pndh": "pindah",
319
+ "plng": "pulang",
320
+ "brngkt": "berangkat",
321
+ "balik": "kembali",
322
+ "blk": "kembali",
323
+ "nyampe": "sampai",
324
+ "nyampek": "sampai",
325
+ "smpe": "sampai",
326
+ "sampe": "sampai",
327
+ "ketemu": "bertemu",
328
+ "ktmu": "bertemu",
329
+ "ngikut": "ikut",
330
+ "ikutin": "ikuti",
331
+ "dengerin": "dengarkan",
332
+ "denger": "dengar",
333
+ "dnger": "dengar",
334
+ "lpd": "lupa",
335
+ "inget": "ingat",
336
+ "ngelupain": "melupakan",
337
+ "ngebantu": "membantu",
338
+ "bntu": "bantu",
339
+ "cek": "periksa",
340
+ "ngecek": "memeriksa",
341
+ "cobaain": "coba",
342
+ "ngelakuin": "melakukan",
343
+ "mnta": "minta",
344
+ "mintain": "mintakan",
345
+ "krm": "kirim",
346
+ "kirimin": "kirimkan",
347
+ "bli": "beli",
348
+ "beliin": "belikan",
349
+ "jln": "jalan",
350
+ "maen": "main",
351
+ "nemenin": "menemani",
352
+ "temenin": "menemani",
353
+ "temenan": "berteman",
354
+ "kenalan": "berkenalan",
355
+ "ngobrol": "mengobrol",
356
+ "ngopi": "minum kopi",
357
+ "nitip": "titip",
358
+ "titipin": "titipkan",
359
+ "bayarin": "bayarkan",
360
+ "bljr": "belajar",
361
+ "bljar": "belajar",
362
+ "ngajar": "mengajar",
363
+ "ngjar": "mengajar",
364
+ "pegi": "pergi",
365
+ }
366
+
367
+ # Adjectives, adverbs, and discourse particles
368
+ _ADJECTIVES_ADVERBS: dict[str, str] = {
369
+ "bgt": "banget",
370
+ "bngt": "banget",
371
+ "bnget": "banget",
372
+ "bget": "banget",
373
+ "bet": "banget",
374
+ "amet": "amat",
375
+ "bener": "benar",
376
+ "bnr": "benar",
377
+ "bner": "benar",
378
+ "cepet": "cepat",
379
+ "cpet": "cepat",
380
+ "cpat": "cepat",
381
+ "ttp": "tetap",
382
+ "lbh": "lebih",
383
+ "krg": "kurang",
384
+ "sdkt": "sedikit",
385
+ "dikit": "sedikit",
386
+ "dkit": "sedikit",
387
+ "hny": "hanya",
388
+ "hnya": "hanya",
389
+ "bbrp": "beberapa",
390
+ "bbrpa": "beberapa",
391
+ "emg": "memang",
392
+ "emang": "memang",
393
+ "aja": "saja",
394
+ "doang": "saja",
395
+ "kek": "seperti",
396
+ "kayak": "seperti",
397
+ "kyk": "seperti",
398
+ "syg": "sayang",
399
+ "syng": "sayang",
400
+ "syang": "sayang",
401
+ "sayng": "sayang",
402
+ # Common informal particles mapped to nearest standard equivalent
403
+ "nih": "ini",
404
+ "tuh": "itu",
405
+ "ntu": "itu",
406
+ "yok": "ayo",
407
+ "yuk": "ayo",
408
+ "ae": "saja", # Sundanese/Javanese particle
409
+ "wae": "saja", # Sundanese: just/only
410
+ "tok": "saja", # Javanese: only
411
+ # Extended adjectives/adverbs
412
+ "abis": "habis",
413
+ "abish": "habis",
414
+ "hbs": "habis",
415
+ "pol": "sekali",
416
+ "beneran": "sungguhan",
417
+ "lm": "lama",
418
+ "lame": "lama",
419
+ "trllu": "terlalu",
420
+ "ckp": "cukup",
421
+ "smpt": "sempat",
422
+ "prnh": "pernah",
423
+ "pernh": "pernah",
424
+ "prnah": "pernah",
425
+ "srg": "sering",
426
+ "jrng": "jarang",
427
+ "jrang": "jarang",
428
+ "sllu": "selalu",
429
+ "slalu": "selalu",
430
+ "kdng": "kadang",
431
+ "mngkin": "mungkin",
432
+ "mngkn": "mungkin",
433
+ "mgkn": "mungkin",
434
+ "pst": "pasti",
435
+ "trnyta": "ternyata",
436
+ "trnyata": "ternyata",
437
+ "lgsg": "langsung",
438
+ "lngsng": "langsung",
439
+ "mlh": "malah",
440
+ "pdhl": "padahal",
441
+ "pdhal": "padahal",
442
+ "pdahl": "padahal",
443
+ "slnya": "soalnya",
444
+ "gitu": "begitu",
445
+ "gt": "begitu",
446
+ "gini": "begini",
447
+ "gn": "begini",
448
+ "segini": "sebanyak ini",
449
+ "sgini": "sebanyak ini",
450
+ "segitu": "sebanyak itu",
451
+ "sgitu": "sebanyak itu",
452
+ "bnyk": "banyak",
453
+ "byk": "banyak",
454
+ "bnyak": "banyak",
455
+ "sdkit": "sedikit",
456
+ "bgs": "bagus",
457
+ "bgus": "bagus",
458
+ "bagoes": "bagus",
459
+ "jlk": "jelek",
460
+ "jlek": "jelek",
461
+ "kcewa": "kecewa",
462
+ "mhl": "mahal",
463
+ "mhal": "mahal",
464
+ "mahl": "mahal",
465
+ "mrh": "murah",
466
+ "pinter": "pintar",
467
+ "pintr": "pintar",
468
+ "pntar": "pintar",
469
+ "pntr": "pintar",
470
+ "hbat": "hebat",
471
+ "hebt": "hebat",
472
+ "hbt": "hebat",
473
+ "lmyn": "lumayan",
474
+ "lumyn": "lumayan",
475
+ "lmyan": "lumayan",
476
+ "mayan": "lumayan",
477
+ "enk": "enak",
478
+ "mntap": "mantap",
479
+ "asik": "asyik",
480
+ "cpk": "capai",
481
+ "cape": "capai",
482
+ "lela": "lelah",
483
+ "ngantu": "mengantuk",
484
+ "laper": "lapar",
485
+ "lper": "lapar",
486
+ "skt": "sakit",
487
+ "skit": "sakit",
488
+ "smbh": "sembuh",
489
+ "smbuh": "sembuh",
490
+ "seneng": "senang",
491
+ "sneng": "senang",
492
+ "bhgia": "bahagia",
493
+ "tkt": "takut",
494
+ "tkut": "takut",
495
+ "bngun": "bingung",
496
+ "psng": "pusing",
497
+ "pusng": "pusing",
498
+ "pening": "pusing",
499
+ "pning": "pusing",
500
+ "mls": "malas",
501
+ "mlas": "malas",
502
+ "mles": "malas",
503
+ "males": "malas",
504
+ "rjn": "rajin",
505
+ "rjin": "rajin",
506
+ "sbnrnya": "sebenarnya",
507
+ "sbnr": "sebenarnya",
508
+ "mending": "lebih baik",
509
+ "mnding": "lebih baik",
510
+ "kyknya": "sepertinya",
511
+ "sptnya": "sepertinya",
512
+ "sprtnya": "sepertinya",
513
+ "gitu2": "begitu-begitu",
514
+ "pelan2": "pelan-pelan",
515
+ "lama2": "lama-lama",
516
+ "buru2": "terburu-buru",
517
+ "kwalitas": "kualitas",
518
+ }
519
+
520
+ # Question words
521
+ _QUESTION_WORDS: dict[str, str] = {
522
+ "knp": "kenapa",
523
+ "knpa": "kenapa",
524
+ "knapa": "kenapa",
525
+ "gmn": "bagaimana",
526
+ "gmna": "bagaimana",
527
+ "gimna": "bagaimana",
528
+ "gimana": "bagaimana",
529
+ "kmn": "kemana",
530
+ "kmna": "kemana",
531
+ "kmana": "kemana",
532
+ "kemna": "kemana",
533
+ "dmn": "di mana",
534
+ "dmna": "di mana",
535
+ "dmana": "di mana",
536
+ "dimna": "di mana",
537
+ "dimana": "di mana",
538
+ "drmn": "dari mana",
539
+ "drmna": "dari mana",
540
+ "drmana": "dari mana",
541
+ "kpn": "kapan",
542
+ "kpan": "kapan",
543
+ "manaa": "mana",
544
+ # Sundanese question words
545
+ "kumaha": "bagaimana",
546
+ "naon": "apa",
547
+ "saha": "siapa",
548
+ "iraha": "kapan",
549
+ "kamana": "kemana",
550
+ }
551
+
552
+ # Greetings, affirmations, and common social responses
553
+ _GREETINGS_RESPONSES: dict[str, str] = {
554
+ # Thanks
555
+ "makasih": "terima kasih",
556
+ "makasi": "terima kasih",
557
+ "mksh": "terima kasih",
558
+ "mksih": "terima kasih",
559
+ "trims": "terima kasih",
560
+ "thx": "terima kasih",
561
+ "tq": "terima kasih",
562
+ "ty": "terima kasih",
563
+ "trimakasih": "terima kasih",
564
+ "terimakasih": "terima kasih",
565
+ "mkch": "terima kasih",
566
+ "tengkyu": "terima kasih",
567
+ "tengkyuu": "terima kasih",
568
+ # Requests
569
+ "pls": "tolong",
570
+ "pliss": "tolong",
571
+ "plz": "tolong",
572
+ "plzz": "tolong",
573
+ "tlg": "tolong",
574
+ # Apology
575
+ "mf": "maaf",
576
+ "afwan": "maaf", # Arabic-Indonesian
577
+ "maap": "maaf",
578
+ "sori": "maaf",
579
+ "sory": "maaf",
580
+ "sorry": "maaf",
581
+ "sorri": "maaf",
582
+ "mnt maaf": "minta maaf",
583
+ # Agreement
584
+ "ok": "oke",
585
+ "okk": "oke",
586
+ "okee": "oke",
587
+ "okelah": "oke",
588
+ "sip": "baik",
589
+ "siapp": "siap",
590
+ "iy": "iya",
591
+ "iyap": "iya",
592
+ "yap": "ya",
593
+ "yep": "ya",
594
+ "yup": "ya",
595
+ "yupp": "ya",
596
+ "yeah": "ya",
597
+ "yoi": "iya", # Jakartan slang
598
+ "mantul": "mantap betul",
599
+ # Greetings
600
+ "hi": "hai",
601
+ "hey": "hei",
602
+ "met pagi": "selamat pagi",
603
+ "met siang": "selamat siang",
604
+ "met sore": "selamat sore",
605
+ "met malam": "selamat malam",
606
+ "slmt dtng": "selamat datang",
607
+ "smpai jumpa": "sampai jumpa",
608
+ "dadah": "sampai jumpa",
609
+ "dah": "sampai jumpa",
610
+ "bye": "selamat tinggal",
611
+ "byee": "selamat tinggal",
612
+ "byebye": "selamat tinggal",
613
+ "ciao": "selamat tinggal",
614
+ "papay": "selamat tinggal",
615
+ # Javanese / Sundanese particles (polite registers)
616
+ "nggih": "iya", # Javanese: yes (polite)
617
+ "inggih": "iya", # Javanese: yes (formal)
618
+ "mboten": "tidak", # Javanese: no/not
619
+ "monggo": "silakan", # Javanese: please go ahead
620
+ "suwun": "terima kasih", # Javanese: thank you
621
+ "matur suwun": "terima kasih", # Javanese: thank you (full form)
622
+ "nuwun": "terima kasih", # Javanese: thank you
623
+ # Sundanese
624
+ "muhun": "iya",
625
+ "hapunten": "maaf",
626
+ "punten": "permisi",
627
+ "hatur nuhun": "terima kasih",
628
+ # Arabic-Indonesian
629
+ "syukron": "terima kasih",
630
+ # Polite expressions
631
+ "prmisi": "permisi",
632
+ "silahkan": "silakan",
633
+ "samasama": "sama-sama",
634
+ "samsama": "sama-sama",
635
+ "sama sama": "sama-sama",
636
+ }
637
+
638
+ # Temporal expressions and locations
639
+ _TEMPORAL_LOCATION: dict[str, str] = {
640
+ "skrg": "sekarang",
641
+ "skrng": "sekarang",
642
+ "ntr": "nanti",
643
+ "ntar": "nanti",
644
+ "kmrn": "kemarin",
645
+ "kmren": "kemarin",
646
+ "bsk": "besok",
647
+ "td": "tadi",
648
+ "dlu": "dulu",
649
+ "dlo": "dulu",
650
+ "hr": "hari",
651
+ "hri": "hari",
652
+ "mlm": "malam",
653
+ "malem": "malam",
654
+ "pg": "pagi",
655
+ "sni": "sini",
656
+ "dsini": "di sini",
657
+ "dsni": "di sini",
658
+ "sono": "sana",
659
+ "mgg": "minggu",
660
+ "bln": "bulan",
661
+ "thn": "tahun",
662
+ "mnt": "menit",
663
+ "dtk": "detik",
664
+ "jdwl": "jadwal",
665
+ "wktu": "waktu",
666
+ }
667
+
668
+ # Internet slang — mapped to Indonesian equivalents (not English)
669
+ _INTERNET_SLANG: dict[str, str] = {
670
+ "otw": "dalam perjalanan",
671
+ "btw": "omong-omong",
672
+ "fyi": "sebagai informasi",
673
+ "imo": "menurut saya",
674
+ "imho": "menurut saya",
675
+ "cmiiw": "koreksi jika saya salah",
676
+ "lol": "tertawa",
677
+ "wkwk": "tertawa",
678
+ "wkwwk": "tertawa",
679
+ "wkwkwk": "tertawa",
680
+ "awokwok": "tertawa",
681
+ "xixi": "tertawa", # Chinese laughter, common in Indonesian social media
682
+ "lmao": "tertawa keras",
683
+ "omg": "astaga",
684
+ "idk": "saya tidak tahu",
685
+ "tbh": "sejujurnya",
686
+ "ngl": "sejujurnya",
687
+ "tbf": "sejujurnya",
688
+ "nvm": "lupakan saja",
689
+ "jk": "bercanda",
690
+ "np": "tidak masalah",
691
+ "afk": "pergi sebentar",
692
+ "brb": "segera kembali",
693
+ "dm": "pesan langsung",
694
+ "tldr": "ringkasnya",
695
+ "wdyt": "menurutmu",
696
+ "wdym": "apa maksudmu",
697
+ "ftr": "akhirnya",
698
+ # Platforms
699
+ "wa": "whatsapp",
700
+ "ig": "instagram",
701
+ "fb": "facebook",
702
+ "tt": "tiktok",
703
+ "yt": "youtube",
704
+ "twit": "twitter",
705
+ "linkin": "linkedin",
706
+ "gform": "google form",
707
+ "gdrive": "google drive",
708
+ "gmeet": "google meet",
709
+ # Social media terms
710
+ "wag": "grup whatsapp",
711
+ "gc": "grup chat",
712
+ "bc": "pesan siaran",
713
+ "nohp": "nomor handphone",
714
+ "ootd": "pakaian hari ini",
715
+ "vibe": "suasana",
716
+ "vibes": "suasana",
717
+ "mood": "suasana hati",
718
+ "hype": "heboh",
719
+ "trending": "sedang tren",
720
+ "lowkey": "diam-diam",
721
+ "highkey": "terang-terangan",
722
+ }
723
+
724
+ # E-commerce and finance terms
725
+ _ECOMMERCE_FINANCE: dict[str, str] = {
726
+ "gan": "saudara",
727
+ "sis": "saudari",
728
+ "rekber": "rekening bersama",
729
+ "cod": "bayar di tempat",
730
+ "ongkir": "ongkos kirim",
731
+ "tf": "transfer",
732
+ "dp": "uang muka",
733
+ "hrg": "harga",
734
+ "hrga": "harga",
735
+ "jastip": "jasa titip",
736
+ "pesen": "pesan",
737
+ "minat": "berminat",
738
+ "duit": "uang",
739
+ "duwit": "uang",
740
+ "fulus": "uang",
741
+ "rb": "ribu",
742
+ "jt": "juta",
743
+ "mlyr": "miliar",
744
+ "pcs": "buah",
745
+ "lbr": "lembar",
746
+ "btl": "botol",
747
+ "asap": "sesegera mungkin",
748
+ "acc": "disetujui",
749
+ "bkl": "bakal",
750
+ "bakal": "akan",
751
+ "mslh": "masalah",
752
+ "sls": "selesai",
753
+ "slsai": "selesai",
754
+ "mlai": "mulai",
755
+ "mulain": "mulai",
756
+ }
757
+
758
+ # Youth / Gen-Z Indonesian slang
759
+ _YOUTH_SLANG: dict[str, str] = {
760
+ "mager": "malas bergerak",
761
+ "baper": "bawa perasaan",
762
+ "gabut": "tidak ada kegiatan",
763
+ "kepo": "ingin tahu",
764
+ "santuy": "santai",
765
+ "woles": "santai",
766
+ "gercep": "gerak cepat",
767
+ "gass": "ayo",
768
+ "gaspol": "ayo semangat",
769
+ "gaskeun": "ayo lakukan", # Sundanese-influenced
770
+ "bucin": "budak cinta",
771
+ "julid": "iri hati",
772
+ "halu": "halusinasi",
773
+ "flexing": "pamer",
774
+ "pansos": "panjat sosial",
775
+ "curhat": "berbagi cerita",
776
+ "kekinian": "tren terkini",
777
+ "kekinan": "tren terkini",
778
+ "hits": "populer",
779
+ "relate": "relatable",
780
+ "selfie": "foto sendiri",
781
+ "caption": "keterangan foto",
782
+ "story": "cerita",
783
+ "feeds": "beranda",
784
+ }
785
+
786
+ # Discourse markers and meta-commentary
787
+ _DISCOURSE_MARKERS: dict[str, str] = {
788
+ "mksd": "maksud",
789
+ "mksdnya": "maksudnya",
790
+ "pkoknya": "pokoknya",
791
+ "pokonya": "pokoknya",
792
+ "jdinya": "jadinya",
793
+ "hslnya": "hasilnya",
794
+ "sngktnya": "singkatnya",
795
+ "menurutku": "menurut saya",
796
+ "mnrtku": "menurut saya",
797
+ "menurutgw": "menurut saya",
798
+ "akhirny": "akhirnya",
799
+ }
800
+
801
+ # Javanese extended vocabulary
802
+ _JAVANESE_EXTENDED: dict[str, str] = {
803
+ # Pronouns
804
+ "awakmu": "kamu",
805
+ "njenengan": "anda",
806
+ "kulo": "saya",
807
+ "kula": "saya",
808
+ "inyong": "saya",
809
+ "dewek": "sendiri",
810
+ # Adjectives
811
+ "apik": "bagus",
812
+ "elek": "jelek",
813
+ "okeh": "banyak",
814
+ "sithik": "sedikit",
815
+ "gedhe": "besar",
816
+ "cilik": "kecil",
817
+ "adoh": "jauh",
818
+ "cedhak": "dekat",
819
+ "alon": "pelan",
820
+ "anyar": "baru",
821
+ "lawas": "lama",
822
+ "sae": "baik",
823
+ # Verbs
824
+ "mangkat": "berangkat",
825
+ "kondur": "pulang",
826
+ "mangan": "makan",
827
+ "ngombe": "minum",
828
+ "turu": "tidur",
829
+ "tangi": "bangun",
830
+ "mlaku": "berjalan",
831
+ "mlayu": "berlari",
832
+ "lungguh": "duduk",
833
+ "ngadeg": "berdiri",
834
+ "weruh": "tahu",
835
+ "krungu": "dengar",
836
+ "ndelok": "lihat",
837
+ "takon": "bertanya",
838
+ "kandha": "berkata",
839
+ "tuku": "beli",
840
+ "adol": "jual",
841
+ }
842
+
843
+ # Sundanese extended vocabulary
844
+ _SUNDANESE_EXTENDED: dict[str, str] = {
845
+ # Pronouns
846
+ "abdi": "saya",
847
+ "anjeun": "kamu",
848
+ "maneh": "kamu",
849
+ # Particles
850
+ "atuh": "dong",
851
+ "euy": "ya",
852
+ "mah": "sih",
853
+ "teh": "itu",
854
+ "nya": "ya",
855
+ # Adjectives
856
+ "alus": "bagus",
857
+ "awon": "jelek",
858
+ "caket": "dekat",
859
+ "leueur": "lambat",
860
+ "sae pisan": "sangat bagus",
861
+ # Verbs/expressions
862
+ "geus": "sudah",
863
+ "keur": "sedang",
864
+ "bade": "akan",
865
+ "hoyong": "mau",
866
+ "daek": "mau",
867
+ "tiasa": "bisa",
868
+ "aya": "ada",
869
+ "teu": "tidak",
870
+ "acan": "belum",
871
+ "can": "belum",
872
+ # Quantity
873
+ "loba": "banyak",
874
+ "saeutik": "sedikit",
875
+ "ged\u00e9": "besar",
876
+ "leutik": "kecil",
877
+ }
878
+
879
+ # Common nouns (abbreviations and informal forms)
880
+ _NOUNS: dict[str, str] = {
881
+ # People & relationships
882
+ "tmn": "teman",
883
+ "temen": "teman",
884
+ "tmn2": "teman-teman",
885
+ "temen2": "teman-teman",
886
+ "shbt": "sahabat",
887
+ "pcr": "pacar",
888
+ "mntm": "mantan",
889
+ # Work & study
890
+ "krjn": "pekerjaan",
891
+ "kntr": "kantor",
892
+ "sklh": "sekolah",
893
+ "kmps": "kampus",
894
+ "kliah": "kuliah",
895
+ "tgs": "tugas",
896
+ "pr": "pekerjaan rumah",
897
+ "ujin": "ujian",
898
+ "ulngn": "ulangan",
899
+ "mhsw": "mahasiswa",
900
+ "mhs": "mahasiswa",
901
+ "mrd": "murid",
902
+ "kls": "kelas",
903
+ "mapel": "mata pelajaran",
904
+ "matkul": "mata kuliah",
905
+ "mtkl": "mata kuliah",
906
+ "skrpsi": "skripsi",
907
+ "wsd": "wisuda",
908
+ "nlai": "nilai",
909
+ "ipk": "indeks prestasi kumulatif",
910
+ "ip": "indeks prestasi",
911
+ "dl": "batas waktu",
912
+ "prsnts": "presentasi",
913
+ "lprn": "laporan",
914
+ "rvisi": "revisi",
915
+ # Things & gadgets
916
+ "hp": "handphone",
917
+ "hape": "handphone",
918
+ "ltop": "laptop",
919
+ "mtr": "motor",
920
+ "mbl": "mobil",
921
+ "rmh": "rumah",
922
+ "mkn": "makan",
923
+ "mnum": "minum",
924
+ # Places
925
+ "wrg": "warung",
926
+ "warteg": "warung tegal",
927
+ "psr": "pasar",
928
+ "gdg": "gedung",
929
+ "rs": "rumah sakit",
930
+ "pskms": "puskesmas",
931
+ }
932
+
933
+ # Health & wellness vocabulary
934
+ _HEALTH: dict[str, str] = {
935
+ # Symptoms
936
+ "dmm": "demam",
937
+ "dmam": "demam",
938
+ "btk": "batuk",
939
+ "btuk": "batuk",
940
+ "plk": "pilek",
941
+ "ml": "mual",
942
+ "mntah": "muntah",
943
+ "alrgi": "alergi",
944
+ "sesak": "sesak napas",
945
+ "sktkpl": "sakit kepala",
946
+ "mencret": "diare",
947
+ "mules": "mulas",
948
+ "lebam": "memar",
949
+ "kslng": "keseleo",
950
+ # Medical personnel & places
951
+ "obt": "obat",
952
+ "dkter": "dokter",
953
+ "dr": "dokter",
954
+ "prwt": "perawat",
955
+ "rwt": "rawat",
956
+ "opname": "rawat inap",
957
+ "ugd": "unit gawat darurat",
958
+ "igd": "instalasi gawat darurat",
959
+ "poli": "poliklinik",
960
+ "apotk": "apotek",
961
+ "resep": "resep dokter",
962
+ "rsep": "resep dokter",
963
+ "rapid test": "tes cepat",
964
+ # Wellness & fitness
965
+ "jalan2": "jalan-jalan",
966
+ "olrga": "olahraga",
967
+ "gym": "olahraga",
968
+ "lari": "berlari",
969
+ "jogging": "joging",
970
+ "tdr": "tidur",
971
+ "tdrsiang": "tidur siang",
972
+ "bgdng": "begadang",
973
+ "istrhat": "istirahat",
974
+ "istrht": "istirahat",
975
+ "shat": "sehat",
976
+ "smbh": "sembuh",
977
+ "lekas sembuh": "lekas sembuh",
978
+ "cpt smbh": "lekas sembuh",
979
+ "get well soon": "lekas sembuh",
980
+ "gws": "lekas sembuh",
981
+ "isoman": "isolasi mandiri",
982
+ }
983
+
984
+ # Emotions, feelings & expressions
985
+ _EMOTIONS_EXPRESSIONS: dict[str, str] = {
986
+ # Happiness / excitement
987
+ "happy": "bahagia",
988
+ "excited": "bersemangat",
989
+ "semngt": "semangat",
990
+ "smngat": "semangat",
991
+ "smgt": "semangat",
992
+ "bersemgt": "bersemangat",
993
+ "yey": "hore",
994
+ "yeay": "hore",
995
+ "yay": "hore",
996
+ "asoy": "asyik",
997
+ "krn2": "keren-keren",
998
+ "mantab": "mantap",
999
+ "yoi": "iya",
1000
+ "gokil": "luar biasa",
1001
+ "gila": "luar biasa",
1002
+ "gile": "luar biasa",
1003
+ "gilak": "luar biasa",
1004
+ "parah": "luar biasa",
1005
+ "lebay": "berlebihan",
1006
+ "lbay": "berlebihan",
1007
+ # Sadness / disappointment
1008
+ "sdh2": "sedih",
1009
+ "kcewa": "kecewa",
1010
+ "kzl": "kesal",
1011
+ "bete": "kesal",
1012
+ "bt": "kesal",
1013
+ "gondok": "kesal",
1014
+ "sewot": "kesal",
1015
+ "jutek": "judes",
1016
+ "nangis": "menangis",
1017
+ "nangiss": "menangis",
1018
+ "mewek": "menangis",
1019
+ "ngedumel": "mengeluh",
1020
+ "ngeluh": "mengeluh",
1021
+ "ngrundel": "mengeluh",
1022
+ "geregetan": "kesal",
1023
+ "gereget": "kesal",
1024
+ "dongkol": "kesal",
1025
+ "badmood": "suasana hati buruk",
1026
+ "bad mood": "suasana hati buruk",
1027
+ "overthink": "terlalu banyak pikiran",
1028
+ "otk": "terlalu banyak pikiran",
1029
+ "down": "sedang terpuruk",
1030
+ "ngedown": "sedang terpuruk",
1031
+ "insecure": "tidak percaya diri",
1032
+ "minder": "rendah diri",
1033
+ # Surprise / shock
1034
+ "shock": "syok",
1035
+ "bnggong": "bengong",
1036
+ "melongo": "terpana",
1037
+ "bnggung": "bingung",
1038
+ "mumet": "pusing",
1039
+ "mupeng": "sangat menginginkan",
1040
+ # Love / affection
1041
+ "syangku": "sayangku",
1042
+ "cintrong": "cinta",
1043
+ "gebetan2": "gebetan-gebetan",
1044
+ "pdkt": "pendekatan",
1045
+ "chat": "pesan",
1046
+ "dm2": "kirim pesan langsung",
1047
+ "ghosting": "menghilang tiba-tiba",
1048
+ "ghosted": "ditinggal tiba-tiba",
1049
+ "friendzone": "zona teman",
1050
+ "fz": "zona teman",
1051
+ "crush": "orang yang disukai",
1052
+ "clbk": "cinta lama bersemi kembali",
1053
+ "php": "pemberi harapan palsu",
1054
+ "playing": "mempermainkan",
1055
+ "dimainin": "dipermainkan",
1056
+ "selingkuh": "berselingkuh",
1057
+ "selingkuhi": "berselingkuhi",
1058
+ "selingkuhan": "perselingkuhan",
1059
+ "putusin": "memutuskan",
1060
+ "balikan": "bersama kembali",
1061
+ # Tired / lazy
1062
+ "capek": "lelah",
1063
+ "capai": "lelah",
1064
+ "kecapean": "kelelahan",
1065
+ "nganggur": "tidak bekerja",
1066
+ "males": "malas",
1067
+ "mager": "malas bergerak",
1068
+ "rebahan": "berbaring santai",
1069
+ "santey": "santai",
1070
+ "ndak": "tidak",
1071
+ "enggak": "tidak",
1072
+ "nyesel": "menyesal",
1073
+ "ngsyel": "menyesal",
1074
+ }
1075
+
1076
+ # Food & drink vocabulary
1077
+ _FOOD_DRINK: dict[str, str] = {
1078
+ # General food terms
1079
+ "mkan": "makan",
1080
+ "mkanan": "makanan",
1081
+ "lauk": "lauk-pauk",
1082
+ "ns": "nasi",
1083
+ "sayur": "sayuran",
1084
+ "syr": "sayuran",
1085
+ "mie": "mi",
1086
+ "mie2": "mie-mie",
1087
+ "indomie": "mi instan",
1088
+ "indmie": "mi instan",
1089
+ "indomie goreng": "mi goreng instan",
1090
+ "migo": "mi goreng",
1091
+ "migor": "mi goreng",
1092
+ "nasgor": "nasi goreng",
1093
+ "nsgr": "nasi goreng",
1094
+ "molen": "kue molen",
1095
+ "grngn": "gorengan",
1096
+ "batagor": "bakso tahu goreng",
1097
+ "cireng": "aci goreng",
1098
+ "cilok": "aci dicolok",
1099
+ "baso": "bakso",
1100
+ "bks": "bakso",
1101
+ "gado2": "gado-gado",
1102
+ "blr": "bubur",
1103
+ "nsuduk": "nasi uduk",
1104
+ "warteg": "warung tegal",
1105
+ "wrtg": "warung tegal",
1106
+ # Drinks
1107
+ "mnum": "minum",
1108
+ "mnuman": "minuman",
1109
+ "kpi": "kopi",
1110
+ "tehmanis": "teh manis",
1111
+ "juice": "jus",
1112
+ "bubble tea": "boba teh",
1113
+ "kopsu": "kopi susu",
1114
+ "americano": "kopi americano",
1115
+ "minol": "minuman beralkohol",
1116
+ # Taste / quality
1117
+ "enakk": "enak",
1118
+ "mantul": "mantap betul",
1119
+ "kriuk": "renyah",
1120
+ "pedes": "pedas",
1121
+ "asem": "asam",
1122
+ "bukan enak": "tidak enak",
1123
+ "gaenak": "tidak enak",
1124
+ "ga enak": "tidak enak",
1125
+ "keabisan": "kehabisan",
1126
+ "khabisan": "kehabisan",
1127
+ # Meal times / hunger
1128
+ "makanmalam": "makan malam",
1129
+ "srpn": "sarapan",
1130
+ "malem": "malam",
1131
+ "laper": "lapar",
1132
+ "laperan": "sedang lapar",
1133
+ "kelaperan": "sangat lapar",
1134
+ "ngidam": "mengidam",
1135
+ "nagih": "ketagihan",
1136
+ "kenyangn": "kekenyangan",
1137
+ }
1138
+
1139
+ # Clothing & fashion vocabulary
1140
+ _CLOTHING_FASHION: dict[str, str] = {
1141
+ # Garments
1142
+ "bj": "baju",
1143
+ "kaos": "kaus",
1144
+ "kos": "kaus",
1145
+ "kmja": "kemeja",
1146
+ "jket": "jaket",
1147
+ "cln": "celana",
1148
+ "dress": "gaun",
1149
+ "jlbb": "jilbab",
1150
+ "hijaber": "pengguna hijab",
1151
+ "krdng": "kerudung",
1152
+ "koko": "baju koko",
1153
+ "pjm": "piyama",
1154
+ "sweter": "sweater",
1155
+ "hudi": "hoodie",
1156
+ "cardigan": "kardigan",
1157
+ "outer": "jaket luar",
1158
+ "outher": "jaket luar",
1159
+ "vest": "rompi",
1160
+ # Footwear
1161
+ "spt2": "sepatu-sepatu",
1162
+ "sdl": "sandal",
1163
+ "sneakers": "sepatu kets",
1164
+ "snakers": "sepatu kets",
1165
+ "boots": "sepatu bot",
1166
+ "heels": "sepatu hak tinggi",
1167
+ "higheels": "sepatu hak tinggi",
1168
+ "flatshoes": "sepatu flat",
1169
+ "slippers": "sandal",
1170
+ # Accessories
1171
+ "tote bag": "tas tote",
1172
+ "totebag": "tas tote",
1173
+ "dmpt": "dompet",
1174
+ "jamtgn": "jam tangan",
1175
+ "kacmt": "kacamata",
1176
+ # Fashion terms
1177
+ "outfit": "pakaian",
1178
+ "ootd": "pakaian hari ini",
1179
+ "mix match": "padupadan",
1180
+ "mixmatch": "padupadan",
1181
+ "casual": "kasual",
1182
+ "stylish": "bergaya",
1183
+ "trendy": "trendi",
1184
+ "vintage": "jadul bergaya",
1185
+ "thrift": "beli baju bekas",
1186
+ "thrifting": "belanja baju bekas",
1187
+ "preloved": "bekas pakai",
1188
+ "ukrn": "ukuran",
1189
+ "size": "ukuran",
1190
+ "kebesaran": "terlalu besar",
1191
+ "kekecilan": "terlalu kecil",
1192
+ }
1193
+
1194
+ # Transportation vocabulary
1195
+ _TRANSPORTATION: dict[str, str] = {
1196
+ # Vehicles
1197
+ "mtr": "motor",
1198
+ "mbl": "mobil",
1199
+ "spd": "sepeda",
1200
+ "angkot": "angkutan kota",
1201
+ "busway": "bus transjakarta",
1202
+ "tj": "transjakarta",
1203
+ "krl": "kereta rel listrik",
1204
+ "mrt": "mass rapid transit",
1205
+ "lrt": "light rail transit",
1206
+ "commuter": "kereta komuter",
1207
+ "komuter": "kereta komuter",
1208
+ "pswt": "pesawat",
1209
+ "bdr": "bandara",
1210
+ "kpl": "kapal",
1211
+ "ojol": "ojek online",
1212
+ "taxi": "taksi",
1213
+ "bentor": "becak motor",
1214
+ "bnsn": "bensin",
1215
+ "bbm": "bahan bakar minyak",
1216
+ # Navigation / travel
1217
+ "jln": "jalan",
1218
+ "mct": "macet",
1219
+ "plkmpng": "pulang kampung",
1220
+ "prjlnan": "perjalanan",
1221
+ "jrk": "jarak",
1222
+ "deket": "dekat",
1223
+ "prk": "parkir",
1224
+ "numpang": "menumpang",
1225
+ "nunut": "menumpang",
1226
+ "nebeng": "menumpang",
1227
+ "nyetir": "mengemudi",
1228
+ "nyupir": "mengemudikan",
1229
+ "setir": "kemudi",
1230
+ "spion": "kaca spion",
1231
+ "gps": "navigasi",
1232
+ "maps": "peta navigasi",
1233
+ "gmaps": "google maps",
1234
+ }
1235
+
1236
+ # Religion, culture & social life
1237
+ _RELIGION_CULTURE: dict[str, str] = {
1238
+ # Islamic expressions (very common in Indonesian daily speech)
1239
+ "insyaallah": "insya allah",
1240
+ "inshallah": "insya allah",
1241
+ "ia": "insya allah",
1242
+ "alhdlh": "alhamdulillah",
1243
+ "alhdlll": "alhamdulillah",
1244
+ "amdlh": "alhamdulillah",
1245
+ "alhamdu": "alhamdulillah",
1246
+ "masyaallah": "masya allah",
1247
+ "subhankl": "subhanallah",
1248
+ "astagfirullah": "astaghfirullah",
1249
+ "astghfr": "astaghfirullah",
1250
+ "bsmllh": "bismillah",
1251
+ "bismillahirrahmanirrahim": "bismillah",
1252
+ "allahuakbar": "allahu akbar",
1253
+ "aamiin": "amin",
1254
+ "aminn": "amin",
1255
+ "jazakallah": "semoga Allah membalas kebaikanmu",
1256
+ "jazakallahu khair": "semoga Allah membalas kebaikanmu",
1257
+ "barakallah": "semoga Allah memberkahimu",
1258
+ "sholat": "salat",
1259
+ "solat": "salat",
1260
+ "shalat": "salat",
1261
+ "bukpus": "buka puasa",
1262
+ "bukber": "buka bersama",
1263
+ "ramadhan": "ramadan",
1264
+ "idulfitri": "idul fitri",
1265
+ "eid": "lebaran",
1266
+ "fitrah": "zakat fitrah",
1267
+ "msdjd": "masjid",
1268
+ "mushola": "musala",
1269
+ "musholla": "musala",
1270
+ "psntrn": "pesantren",
1271
+ "ustadz": "ustaz",
1272
+ "ustad": "ustaz",
1273
+ "kyai": "kiai",
1274
+ # National / cultural
1275
+ "bhineka": "bhinneka",
1276
+ "17an": "tujuh belasan",
1277
+ "hut ri": "hari ulang tahun republik indonesia",
1278
+ "hutri": "hari ulang tahun republik indonesia",
1279
+ # Events / celebrations
1280
+ "ultah": "ulang tahun",
1281
+ "ulangtahun": "ulang tahun",
1282
+ "ultahku": "ulang tahunku",
1283
+ "happy birthday": "selamat ulang tahun",
1284
+ "hbd": "selamat ulang tahun",
1285
+ "hbday": "selamat ulang tahun",
1286
+ "met ultah": "selamat ulang tahun",
1287
+ "met ulth": "selamat ulang tahun",
1288
+ "wedding": "pernikahan",
1289
+ "nikah": "menikah",
1290
+ "nikahin": "menikahkan",
1291
+ "tunang": "tunangan",
1292
+ "akad": "akad nikah",
1293
+ "undngn": "undangan",
1294
+ "kondangan": "menghadiri pernikahan",
1295
+ "kondngan": "kondangan",
1296
+ "ngaji": "mengaji",
1297
+ }
1298
+
1299
+ # Education extended vocabulary
1300
+ _EDUCATION_EXTENDED: dict[str, str] = {
1301
+ # School levels
1302
+ "sd": "sekolah dasar",
1303
+ "smp": "sekolah menengah pertama",
1304
+ "sma": "sekolah menengah atas",
1305
+ "smk": "sekolah menengah kejuruan",
1306
+ "univ": "universitas",
1307
+ "univrsitas": "universitas",
1308
+ "kampus": "kampus",
1309
+ "kuliah": "kuliah",
1310
+ "mhsw": "mahasiswa",
1311
+ "dsn": "dosen",
1312
+ "gr": "guru",
1313
+ "kepsek": "kepala sekolah",
1314
+ "kpsek": "kepala sekolah",
1315
+ # Academic activities
1316
+ "bljar": "belajar",
1317
+ "bljr": "belajar",
1318
+ "ngajar": "mengajar",
1319
+ "ngjar": "mengajar",
1320
+ "ulngn": "ulangan",
1321
+ "tgs": "tugas",
1322
+ "pr": "pekerjaan rumah",
1323
+ "homework": "pekerjaan rumah",
1324
+ "hw": "pekerjaan rumah",
1325
+ "presntsi": "presentasi",
1326
+ "prsnts": "presentasi",
1327
+ "laporan": "laporan",
1328
+ "lprn": "laporan",
1329
+ "mklh": "makalah",
1330
+ "skrpsi": "skripsi",
1331
+ "thesis": "tesis",
1332
+ "wsuda": "wisuda",
1333
+ "wsda": "wisuda",
1334
+ "wsd": "wisuda",
1335
+ "yudisium": "yudisium",
1336
+ "ospek": "orientasi studi",
1337
+ "maba": "mahasiswa baru",
1338
+ "mhs baru": "mahasiswa baru",
1339
+ # Grades & results
1340
+ "nilai": "nilai",
1341
+ "nlai": "nilai",
1342
+ "rapor": "rapor",
1343
+ "raport": "rapor",
1344
+ "ipk": "indeks prestasi kumulatif",
1345
+ "ip": "indeks prestasi",
1346
+ "llus": "lulus",
1347
+ "lls": "lulus",
1348
+ "ggal": "gagal",
1349
+ "ggl": "gagal",
1350
+ "remedial": "remedial",
1351
+ "remidi": "remedial",
1352
+ "ngulang": "mengulang",
1353
+ "nunggak": "tidak naik kelas",
1354
+ "naikin": "menaikkan",
1355
+ "beasiwa": "beasiswa",
1356
+ "bsw": "beasiswa",
1357
+ "beasiswaku": "beasiswaku",
1358
+ "daftar": "daftar",
1359
+ "dft": "daftar",
1360
+ "pendaftran": "pendaftaran",
1361
+ "pendftran": "pendaftaran",
1362
+ "pndftran": "pendaftaran",
1363
+ "sleksi": "seleksi",
1364
+ "slksi": "seleksi",
1365
+ "snbt": "seleksi nasional berdasarkan tes",
1366
+ "utbk": "ujian tulis berbasis komputer",
1367
+ "sbmptn": "seleksi bersama masuk perguruan tinggi negeri",
1368
+ "ppdb": "penerimaan peserta didik baru",
1369
+ "bimbel": "bimbingan belajar",
1370
+ "bmbl": "bimbingan belajar",
1371
+ "tryout": "uji coba",
1372
+ "to": "uji coba",
1373
+ }
1374
+
1375
+ # Work & office vocabulary
1376
+ _WORK_OFFICE: dict[str, str] = {
1377
+ # Job types & positions
1378
+ "krja": "kerja",
1379
+ "kerjaan": "pekerjaan",
1380
+ "krjaan": "pekerjaan",
1381
+ "kntr": "kantor",
1382
+ "prusahaan": "perusahaan",
1383
+ "prshn": "perusahaan",
1384
+ "bos": "atasan",
1385
+ "boss": "atasan",
1386
+ "rekan": "rekan kerja",
1387
+ "krywn": "karyawan",
1388
+ "pgwai": "pegawai",
1389
+ "pns": "pegawai negeri sipil",
1390
+ "asn": "aparatur sipil negara",
1391
+ "honorer": "tenaga honorer",
1392
+ "freelance": "pekerja lepas",
1393
+ "frilens": "pekerja lepas",
1394
+ "wfh": "kerja dari rumah",
1395
+ "wfo": "kerja dari kantor",
1396
+ "wfa": "kerja dari mana saja",
1397
+ "remote": "kerja jarak jauh",
1398
+ "hybrid": "kerja hibrida",
1399
+ "intern": "magang",
1400
+ "mgang": "magang",
1401
+ "kntrak": "kontrak",
1402
+ "resign": "mengundurkan diri",
1403
+ "resignasi": "pengunduran diri",
1404
+ "phk": "pemutusan hubungan kerja",
1405
+ # Work activities
1406
+ "rpt": "rapat",
1407
+ "meeting": "rapat",
1408
+ "mtg": "rapat",
1409
+ "zoom": "rapat video",
1410
+ "gmeet": "google meet",
1411
+ "deadline": "batas waktu",
1412
+ "dl": "batas waktu",
1413
+ "dl nya": "batas waktunya",
1414
+ "submit": "kirimkan",
1415
+ "sbmt": "kirimkan",
1416
+ "rvisi": "revisi",
1417
+ "approval": "persetujuan",
1418
+ "approved": "disetujui",
1419
+ "acc": "disetujui",
1420
+ "reject": "ditolak",
1421
+ "feedback": "masukan",
1422
+ "ftdbk": "masukan",
1423
+ "report": "laporan",
1424
+ "update": "perbarui",
1425
+ "followup": "tindak lanjut",
1426
+ "fu": "tindak lanjut",
1427
+ "asap": "sesegera mungkin",
1428
+ "urgent": "mendesak",
1429
+ "priority": "prioritas",
1430
+ "prio": "prioritas",
1431
+ "kpi": "indikator kinerja utama",
1432
+ "okr": "tujuan dan hasil kunci",
1433
+ "brainstorm": "curah gagasan",
1434
+ "brnstrm": "curah gagasan",
1435
+ # Finance / salary
1436
+ "gji": "gaji",
1437
+ "gajinya": "gajinya",
1438
+ "thr": "tunjangan hari raya",
1439
+ "lmbr": "lembur",
1440
+ "overtime": "lembur",
1441
+ "ot": "lembur",
1442
+ "ct": "cuti",
1443
+ "check in": "masuk kerja",
1444
+ "checkin": "masuk kerja",
1445
+ "check out": "keluar kerja",
1446
+ "checkout": "keluar kerja",
1447
+ # Tools / digital work
1448
+ "email": "surel",
1449
+ "mailing": "mengirim surel",
1450
+ "cc": "tembusan",
1451
+ "bcc": "tembusan tersembunyi",
1452
+ "forward": "teruskan",
1453
+ "teams": "microsoft teams",
1454
+ "ms teams": "microsoft teams",
1455
+ "excel": "microsoft excel",
1456
+ "word": "microsoft word",
1457
+ "ppt": "powerpoint",
1458
+ "powerpoint": "presentasi powerpoint",
1459
+ "drive": "google drive",
1460
+ "cloud": "komputasi awan",
1461
+ "database": "basis data",
1462
+ "app": "aplikasi",
1463
+ "apk": "aplikasi",
1464
+ "software": "perangkat lunak",
1465
+ "hardware": "perangkat keras",
1466
+ }
1467
+
1468
+ # Numbers, measurements & quantity expressions
1469
+ _NUMBERS_QUANTITY: dict[str, str] = {
1470
+ # Informal prefix shortcuts (sejuta / semiliar are genuine contractions)
1471
+ "sejuta": "satu juta",
1472
+ "semiliar": "satu miliar",
1473
+ # Informal number abbreviations (rb = ribu, jt = juta)
1474
+ "1rb": "seribu",
1475
+ "2rb": "dua ribu",
1476
+ "5rb": "lima ribu",
1477
+ "10rb": "sepuluh ribu",
1478
+ "20rb": "dua puluh ribu",
1479
+ "50rb": "lima puluh ribu",
1480
+ "100rb": "seratus ribu",
1481
+ "200rb": "dua ratus ribu",
1482
+ "500rb": "lima ratus ribu",
1483
+ "1jt": "satu juta",
1484
+ "2jt": "dua juta",
1485
+ "5jt": "lima juta",
1486
+ "10jt": "sepuluh juta",
1487
+ # Quantity abbreviations (slang → standard)
1488
+ "slrh": "seluruh",
1489
+ "sktr": "sekitar",
1490
+ "hmpr": "hampir",
1491
+ "hmpir": "hampir",
1492
+ "plng2": "paling-paling",
1493
+ # Reduplications (written as X2 or concatenated)
1494
+ "rata2": "rata-rata",
1495
+ "ratarata": "rata-rata",
1496
+ "kira2": "kira-kira",
1497
+ "kirakira": "kira-kira",
1498
+ # Time quantities
1499
+ "sebentar": "sebentar",
1500
+ "sbntr": "sebentar",
1501
+ "sbntr lg": "sebentar lagi",
1502
+ "slmanya": "selamanya",
1503
+ "smntara": "sementara",
1504
+ "mndak": "mendadak",
1505
+ "dadakan": "mendadak",
1506
+ "ssgera": "segera",
1507
+ "sgera": "segera",
1508
+ "sgra": "segera",
1509
+ "cpat": "cepat",
1510
+ }
1511
+
1512
+ # Common compound slang expressions & phrases
1513
+ # Only include entries where the COMBINED phrase produces a different result
1514
+ # than individual word lookups would give (e.g. slang fusion, negation compounds
1515
+ # with particles, or abbreviations that span multiple tokens).
1516
+ _COMPOUND_EXPRESSIONS: dict[str, str] = {
1517
+ # Slang word + particle/qualifier — transformation differs from individual lookups
1518
+ "eh iya": "oh iya", # discourse opener
1519
+ "gitu kan": "begitu kan",
1520
+ "gitu dong": "begitu dong",
1521
+ "beneran ga": "sungguh tidak",
1522
+ "beneran nih": "sungguh ini",
1523
+ "srius": "serius", # typo of standard word
1524
+ "serius nih": "serius ini",
1525
+ # Question compounds with negation particle
1526
+ "bisa ga": "bisa tidak",
1527
+ "bisa gak": "bisa tidak",
1528
+ "boleh ga": "boleh tidak",
1529
+ "boleh gak": "boleh tidak",
1530
+ "mau ga": "mau tidak",
1531
+ "mau gak": "mau tidak",
1532
+ "tau ga": "tahu tidak",
1533
+ "tau gak": "tahu tidak",
1534
+ # Multi-token slang phrases
1535
+ "gimana caranya": "bagaimana caranya",
1536
+ "gmn caranya": "bagaimana caranya",
1537
+ "gue juga": "saya juga",
1538
+ "gw juga": "saya juga",
1539
+ "kayaknya sih": "sepertinya",
1540
+ "kayaknya iya": "sepertinya iya",
1541
+ "buat apa": "untuk apa",
1542
+ "bwt apa": "untuk apa",
1543
+ "ngapain": "sedang apa",
1544
+ "ngapain aja": "sedang apa saja",
1545
+ "lagi ngapain": "sedang apa",
1546
+ "lagi dimana": "sedang di mana",
1547
+ "lg dmn": "sedang di mana",
1548
+ "otw ke": "dalam perjalanan ke",
1549
+ "nyampe blm": "sudah sampai belum",
1550
+ "sampai blm": "sudah sampai belum",
1551
+ # Compound slang where slang token fuses with standard word
1552
+ "udah mkan": "sudah makan", # mkan not split correctly if fused
1553
+ "blm makan": "belum makan",
1554
+ "udh lma": "sudah lama",
1555
+ "br aja": "baru saja",
1556
+ "bsk aja": "besok saja",
1557
+ "skrg juga": "sekarang juga",
1558
+ "dr tadi": "dari tadi",
1559
+ "dr dulu": "dari dulu",
1560
+ "tlg bantu": "tolong bantu",
1561
+ # Agreement / disagreement — particle changes meaning of phrase
1562
+ "emang bener": "memang benar",
1563
+ "bener juga": "benar juga",
1564
+ "iya bener": "iya benar",
1565
+ "nggak dong": "tidak dong",
1566
+ "nggak sih": "tidak sih",
1567
+ "ya ga": "ya tidak",
1568
+ "ga juga": "tidak juga",
1569
+ "gak juga": "tidak juga",
1570
+ "bukan gitu": "bukan begitu",
1571
+ "bukan disini": "bukan di sini",
1572
+ "maksudnya gimana": "maksudnya bagaimana",
1573
+ "mksdnya gmn": "maksudnya bagaimana",
1574
+ # Temporal compound abbreviations
1575
+ "kapan2": "kapan-kapan",
1576
+ "kapan2 ya": "kapan-kapan ya",
1577
+ "ntar aja": "nanti saja",
1578
+ "tar aja": "nanti saja",
1579
+ "makasih banyak": "terima kasih banyak",
1580
+ }
1581
+
1582
+ # ─────────────────────────────────────────────────────────────────────────────
1583
+ # PARTICLES
1584
+ # Indonesian sentence-final / clause-final pragmatic particles.
1585
+ # The 6 canonical forms (lah, deh, nih, dong, kok, sih) are pass-through mapped
1586
+ # to prevent them from being stripped. Other particles rely on auto-reduction.
1587
+ #
1588
+ # Design: _reduce_repeated_chars automatically collapses:
1589
+ # - vowels: 2+ consecutive → 1 (laah → lah, yaa → ya)
1590
+ # - consonants: 3+ consecutive → 1 (lahhhh → lah, yukkk → yuk)
1591
+ # Note: 2-consonant repeats (lahh, dehh) are explicitly mapped here as they
1592
+ # are not auto-reduced.
1593
+ # ─────────────────────────────────────────────────────────────────────────────
1594
+ _PARTICLES: dict[str, str] = {
1595
+ # ── lah ──
1596
+ "lah": "lah",
1597
+ "lh": "lah",
1598
+ "lahh": "lah",
1599
+ "la": "lah",
1600
+ # ── deh ──
1601
+ "deh": "deh",
1602
+ "dh": "deh",
1603
+ "dehh": "deh",
1604
+ "de": "deh",
1605
+ # ── nih ──
1606
+ "nih": "nih",
1607
+ "ni": "nih",
1608
+ "nihh": "nih",
1609
+ "neh": "nih", # Betawi variant
1610
+ "nehh": "nih",
1611
+ # ── dong ──
1612
+ "dong": "dong",
1613
+ "dng": "dong",
1614
+ "dongg": "dong",
1615
+ "don": "dong",
1616
+ # ── kok ──
1617
+ "kok": "kok",
1618
+ "kokk": "kok",
1619
+ # ── sih ──
1620
+ "sih": "sih",
1621
+ "si": "sih",
1622
+ "sihh": "sih",
1623
+ "sich": "sih",
1624
+ # ── Other Particle Variants ──
1625
+ "loh": "lho",
1626
+ "lo": "lho",
1627
+ "punn": "pun",
1628
+ "tohh": "toh",
1629
+ "kahh": "kah",
1630
+ "tahh": "tah",
1631
+ "woy": "woi",
1632
+ "woyy": "woi",
1633
+ "hey": "hei",
1634
+ "yukk": "yuk",
1635
+ "yuk ah": "yuk",
1636
+ "ahh": "ah",
1637
+ "ihh": "ih",
1638
+ "idihh": "idih",
1639
+ }
1640
+
1641
+ # ─────────────────────────────────────────────────────────────────────────────
1642
+ # MERGE ALL CATEGORIES INTO ONE MAPPING
1643
+ # Order matters: later dicts override earlier ones on key conflicts.
1644
+ # Compound negation must appear so that it sorts BEFORE single-char negation
1645
+ # (handled automatically by longest-first sort in _compile_pattern).
1646
+ # ─────────────────────────────────────────────────────────────────────────────
1647
+ _DEFAULT_SLANG: dict[str, str] = {}
1648
+ for _cat in (
1649
+ _PRONOUNS,
1650
+ _KINSHIP,
1651
+ _NEGATION,
1652
+ _COMPOUND_NEGATION,
1653
+ _CONJUNCTIONS,
1654
+ _VERBS,
1655
+ _ADJECTIVES_ADVERBS,
1656
+ _QUESTION_WORDS,
1657
+ _GREETINGS_RESPONSES,
1658
+ _TEMPORAL_LOCATION,
1659
+ _INTERNET_SLANG,
1660
+ _ECOMMERCE_FINANCE,
1661
+ _YOUTH_SLANG,
1662
+ _DISCOURSE_MARKERS,
1663
+ _JAVANESE_EXTENDED,
1664
+ _SUNDANESE_EXTENDED,
1665
+ _NOUNS,
1666
+ _HEALTH,
1667
+ _EMOTIONS_EXPRESSIONS,
1668
+ _FOOD_DRINK,
1669
+ _CLOTHING_FASHION,
1670
+ _TRANSPORTATION,
1671
+ _RELIGION_CULTURE,
1672
+ _EDUCATION_EXTENDED,
1673
+ _WORK_OFFICE,
1674
+ _NUMBERS_QUANTITY,
1675
+ _COMPOUND_EXPRESSIONS,
1676
+ _PARTICLES,
1677
+ ):
1678
+ _DEFAULT_SLANG.update(_cat)
1679
+
1680
+
1681
+ # ─────────────────────────────────────────────────────────────────────────────
1682
+ # REPEATED CHARACTER NORMALIZATION PATTERNS
1683
+ # ─────────────────────────────────────────────────────────────────────────────
1684
+
1685
+ # Vowels: reduce 2+ consecutive identical vowels → 1
1686
+ # e.g. "manaa" → "mana", "bangeeeet" → "banget"
1687
+ # Rationale: vowel elongation is always expressive, never meaningful.
1688
+ _VOWEL_REPEAT = re.compile(r"([aeiouAEIOU])\1+")
1689
+
1690
+ # Strict vowel reduction: only collapse 3+ consecutive identical vowels.
1691
+ # Used in the global fallback pass so that valid standard Indonesian spellings
1692
+ # containing double vowels (e.g. "pekerjaan", "maaf", "laal") are NOT touched,
1693
+ # while obvious elongation ("manaaaa", "beneeer") is still reduced.
1694
+ _VOWEL_REPEAT_STRICT = re.compile(r"([aeiouAEIOU])\1{2,}")
1695
+
1696
+ # Consonants: reduce 3+ consecutive identical consonants → 1
1697
+ # e.g. "bgttt" → "bgt", "wkwwwk" → "wkwk"
1698
+ # Rationale: keep "kk" (kakak), "ll" (dalam "dll"), "mm" etc. intact;
1699
+ # only collapse truly elongated forms (3+).
1700
+ # Note: [^\W\s\d] matches letter-only consonants, excluding punctuation
1701
+ # so that "!!!" is NOT reduced here (that is handled by normalize_punctuation).
1702
+ _CONSONANT_REPEAT = re.compile(r"([^\W\s\daeiouAEIOU])\1{2,}")
1703
+
1704
+ # Whitespace normalization
1705
+ _WHITESPACE = re.compile(r"\s+")
1706
+
1707
+
1708
+ # ─────────────────────────────────────────────────────────────────────────────
1709
+ # MAIN CLASS
1710
+ # ─────────────────────────────────────────────────────────────────────────────
1711
+
1712
+
1713
+ class SlangNormalizer:
1714
+ """
1715
+ Normalizes informal/colloquial Indonesian text to standard Indonesian.
1716
+
1717
+ Args:
1718
+ custom_mapping: Optional additional slang entries that override or
1719
+ extend the built-in dictionary.
1720
+
1721
+ Example:
1722
+ >>> normalizer = SlangNormalizer()
1723
+ >>> normalizer.normalize("gw gamau pergi krn lg baper bgt")
1724
+ 'saya tidak mau pergi karena sedang bawa perasaan banget'
1725
+
1726
+ >>> normalizer.add("npwp", "nomor pokok wajib pajak")
1727
+ >>> normalizer.normalize("masukin npwp dulu")
1728
+ 'masukkan nomor pokok wajib pajak dulu'
1729
+
1730
+ >>> normalizer.normalize_batch(["gw mau makan", "km udah makan?"])
1731
+ ['saya mau makan', 'kamu sudah makan?']
1732
+ """
1733
+
1734
+ def __init__(self, custom_mapping: dict[str, str] | None = None):
1735
+ self.mapping: dict[str, str] = {k.lower(): v for k, v in _DEFAULT_SLANG.items()}
1736
+ if custom_mapping:
1737
+ self.mapping.update({k.lower(): v for k, v in custom_mapping.items()})
1738
+ self._regex: re.Pattern | None = None
1739
+ self._compile_pattern()
1740
+
1741
+ # ─── Internal ────────────────────────────────────────────────────────────
1742
+
1743
+ def _compile_pattern(self) -> None:
1744
+ """
1745
+ Build a compiled regex from the current mapping.
1746
+
1747
+ Keys are sorted longest-first so compound slang like "gamau"
1748
+ matches before its shorter substring "ga".
1749
+ """
1750
+ if not self.mapping:
1751
+ self._regex = None
1752
+ return
1753
+ sorted_keys = sorted(self.mapping.keys(), key=len, reverse=True)
1754
+ escaped = [re.escape(k) for k in sorted_keys]
1755
+ pattern = r"\b(" + "|".join(escaped) + r")\b"
1756
+ self._regex = re.compile(pattern, re.IGNORECASE)
1757
+
1758
+ @staticmethod
1759
+ def _reduce_repeated_chars(text: str) -> str:
1760
+ """
1761
+ Collapse elongated characters to their canonical form.
1762
+
1763
+ Rules:
1764
+ - Vowels (a, e, i, o, u): 2+ consecutive → 1
1765
+ "manaaaa" → "mana", "bangeeeet" → "banget"
1766
+ - Consonants: 3+ consecutive → 1
1767
+ "bgttttt" → "bgt", "wkwwwwk" → "wkwk"
1768
+ "kk" → "kk" (preserved — abbreviation for "kakak")
1769
+ "dll" → "dll" (preserved — "dan lain-lain")
1770
+
1771
+ Vowels are processed first so that a string like "aaakk" is handled
1772
+ correctly in two distinct passes without cross-contamination.
1773
+ """
1774
+ text = _VOWEL_REPEAT.sub(r"\1", text)
1775
+ text = _CONSONANT_REPEAT.sub(r"\1", text)
1776
+ return text
1777
+
1778
+ @staticmethod
1779
+ def _reduce_repeated_chars_strict(text: str) -> str:
1780
+ """
1781
+ Like _reduce_repeated_chars but uses a stricter threshold for vowels:
1782
+ only collapses 3+ consecutive identical vowels (not 2+).
1783
+
1784
+ This is used as the global fallback pass inside normalize() so that
1785
+ standard Indonesian words with legitimate double vowels (e.g.
1786
+ "pekerjaan", "maaf") are not corrupted, while obvious elongation
1787
+ ("manaaaa", "beneeer") is still collapsed.
1788
+ """
1789
+ text = _VOWEL_REPEAT_STRICT.sub(r"\1", text)
1790
+ text = _CONSONANT_REPEAT.sub(r"\1", text)
1791
+ return text
1792
+
1793
+ # ─── Public API ──────────────────────────────────────────────────────────
1794
+
1795
+ @staticmethod
1796
+ def _mirror_case(original: str, replacement: str) -> str:
1797
+ """
1798
+ Mirror the casing pattern of ``original`` onto ``replacement``.
1799
+
1800
+ Rules (applied in priority order):
1801
+ - All-uppercase original → replacement.upper()
1802
+ - Title-case original → replacement.capitalize()
1803
+ - Mixed / lowercase → replacement unchanged (already lowercase)
1804
+
1805
+ This lets ``normalize(lowercase=False)`` preserve casing intent
1806
+ even after slang substitution.
1807
+
1808
+ Examples:
1809
+ >>> SlangNormalizer._mirror_case("GW", "saya")
1810
+ 'SAYA'
1811
+ >>> SlangNormalizer._mirror_case("Gw", "saya")
1812
+ 'Saya'
1813
+ >>> SlangNormalizer._mirror_case("gw", "saya")
1814
+ 'saya'
1815
+ """
1816
+ if original.isupper():
1817
+ return replacement.upper()
1818
+ if original[0].isupper():
1819
+ return replacement.capitalize()
1820
+ return replacement
1821
+
1822
+ def normalize(
1823
+ self,
1824
+ text: str,
1825
+ normalize_whitespace: bool = True,
1826
+ lowercase: bool = True,
1827
+ ) -> str:
1828
+ """
1829
+ Normalize a single text string.
1830
+
1831
+ Pipeline:
1832
+ 1. Slang replacement with two-pass per-token lookup
1833
+ 2. Global repeated-char reduction for out-of-vocab elongated words
1834
+ 3. Second regex pass for words newly recognisable after reduction
1835
+ (e.g. "gwwww" → step2 → "gw" → step3 → "saya")
1836
+ 4. (Optionally) collapse multiple whitespace into one
1837
+
1838
+ Args:
1839
+ text: Input text string.
1840
+ normalize_whitespace: If True, collapse multi-space into single
1841
+ space and strip leading/trailing whitespace.
1842
+ lowercase: If True (default), replacement values are returned as-is
1843
+ (all lowercase). If False, the casing pattern of the
1844
+ original matched token is mirrored onto the replacement
1845
+ (ALL-CAPS → upper, Title → capitalize, mixed → as-is).
1846
+
1847
+ Returns:
1848
+ Normalized text string.
1849
+ """
1850
+ if not text or not text.strip():
1851
+ return text
1852
+
1853
+ # Step 1: Slang replacement with two-pass per-token lookup.
1854
+ # The regex matches candidates from the slang dict; for each match:
1855
+ # Pass A – lookup the original token (handles "krjaan" which has a
1856
+ # meaningful double-a that char-reduction would destroy).
1857
+ # Pass B – reduce repeated chars first, then lookup (handles elongated
1858
+ # forms like "gkkkk" → "gk" → "tidak").
1859
+ # Fallback – return the char-reduced form so elongation is still
1860
+ # collapsed even when no dict entry exists.
1861
+ if self._regex:
1862
+
1863
+ def _replace(match: re.Match) -> str:
1864
+ original = match.group(1)
1865
+ word = original.lower()
1866
+
1867
+ # Pass A: direct lookup (preserve meaningful repeated vowels)
1868
+ if word in self.mapping:
1869
+ result = self.mapping[word]
1870
+ if not lowercase:
1871
+ return self._mirror_case(original, result)
1872
+ return result
1873
+
1874
+ # Pass B: reduce chars first, then lookup
1875
+ reduced = self._reduce_repeated_chars(word)
1876
+ if reduced in self.mapping:
1877
+ result = self.mapping[reduced]
1878
+ if not lowercase:
1879
+ return self._mirror_case(original, result)
1880
+ return result
1881
+
1882
+ # Fallback: return char-reduced form (preserves original case)
1883
+ return self._reduce_repeated_chars(original)
1884
+
1885
+ text = self._regex.sub(_replace, text)
1886
+
1887
+ # Step 2: Global char reduction (strict mode) for words not captured
1888
+ # by the dict regex, e.g. out-of-vocab elongated words ("manaaaa" ->
1889
+ # "mana") or words whose elongated form isn't in the regex ("gwwww" ->
1890
+ # "gw").
1891
+ # STRICT: only collapses 3+ consecutive identical vowels so that valid
1892
+ # standard Indonesian double vowels in replacement outputs (e.g.
1893
+ # "pekerjaan") are NOT corrupted.
1894
+ text = self._reduce_repeated_chars_strict(text)
1895
+
1896
+ # Step 3: Second regex pass — catches words that only became recognisable
1897
+ # after Step 2 (e.g. "gwwww" -> "gw" -> "saya", "gkkkk" -> "gk" -> "tidak").
1898
+ if self._regex:
1899
+ text = self._regex.sub(_replace, text)
1900
+
1901
+ # Step 4: Normalize whitespace
1902
+ if normalize_whitespace:
1903
+ text = _WHITESPACE.sub(" ", text).strip()
1904
+
1905
+ return text
1906
+
1907
+ def normalize_batch(
1908
+ self,
1909
+ texts: list[str],
1910
+ normalize_whitespace: bool = True,
1911
+ lowercase: bool = True,
1912
+ ) -> list[str]:
1913
+ """
1914
+ Normalize a list of text strings efficiently.
1915
+
1916
+ Args:
1917
+ texts: List of input strings.
1918
+ normalize_whitespace: Passed through to normalize().
1919
+ lowercase: Passed through to normalize().
1920
+
1921
+ Returns:
1922
+ List of normalized strings (same order as input).
1923
+
1924
+ Example:
1925
+ >>> slang.normalize_batch(["gw udah makan", "km blm tidur?"])
1926
+ ['saya sudah makan', 'kamu belum tidur?']
1927
+ """
1928
+ return [self.normalize(t, normalize_whitespace, lowercase) for t in texts]
1929
+
1930
+ def add(self, word: str, replacement: str) -> None:
1931
+ """
1932
+ Add a single entry to the slang dictionary.
1933
+
1934
+ Triggers regex recompilation. For adding many entries at once,
1935
+ prefer bulk_add() to avoid repeated recompilation.
1936
+
1937
+ Args:
1938
+ word: Slang word (stored as lowercase).
1939
+ replacement: Standard replacement string.
1940
+
1941
+ Example:
1942
+ >>> normalizer.add("npwp", "nomor pokok wajib pajak")
1943
+ """
1944
+ self.mapping[word.lower()] = replacement
1945
+ self._compile_pattern()
1946
+
1947
+ def bulk_add(self, mapping: dict[str, str]) -> None:
1948
+ """
1949
+ Add multiple entries at once with a single regex recompilation.
1950
+
1951
+ More efficient than calling add() repeatedly when inserting
1952
+ many domain-specific terms.
1953
+
1954
+ Args:
1955
+ mapping: Dict of {slang: replacement} pairs.
1956
+
1957
+ Example:
1958
+ >>> normalizer.bulk_add({
1959
+ ... "kk": "kakak",
1960
+ ... "adek": "adik",
1961
+ ... "bokap": "ayah",
1962
+ ... "nyokap": "ibu",
1963
+ ... })
1964
+ """
1965
+ self.mapping.update({k.lower(): v for k, v in mapping.items()})
1966
+ self._compile_pattern()
1967
+
1968
+ def remove(self, word: str) -> bool:
1969
+ """
1970
+ Remove a word from the dictionary.
1971
+
1972
+ Args:
1973
+ word: Slang word to remove.
1974
+
1975
+ Returns:
1976
+ True if the word was found and removed, False otherwise.
1977
+ """
1978
+ word_lower = word.lower()
1979
+ if word_lower in self.mapping:
1980
+ del self.mapping[word_lower]
1981
+ self._compile_pattern()
1982
+ return True
1983
+ return False
1984
+
1985
+ def reset(self) -> None:
1986
+ """
1987
+ Reset dictionary to built-in defaults, discarding all custom changes.
1988
+
1989
+ Example:
1990
+ >>> normalizer.add("x", "ekstra")
1991
+ >>> "x" in normalizer
1992
+ True
1993
+ >>> normalizer.reset()
1994
+ >>> "x" in normalizer
1995
+ False
1996
+ """
1997
+ self.mapping = {k.lower(): v for k, v in _DEFAULT_SLANG.items()}
1998
+ self._compile_pattern()
1999
+
2000
+ def export(self) -> dict[str, str]:
2001
+ """
2002
+ Export the current mapping as a plain dictionary.
2003
+
2004
+ Returns:
2005
+ A copy of the current {slang: replacement} mapping.
2006
+ """
2007
+ return self.mapping.copy()
2008
+
2009
+ def supported_words(self) -> dict[str, str]:
2010
+ """
2011
+ Return all supported slang words and their standard replacements.
2012
+
2013
+ Useful for debugging, documentation, or building custom UIs.
2014
+
2015
+ Returns:
2016
+ A copy of the current {slang: standard} mapping, sorted
2017
+ alphabetically by slang key.
2018
+
2019
+ Example:
2020
+ >>> from basa import slang
2021
+ >>> words = slang.supported_words()
2022
+ >>> words["gw"]
2023
+ 'saya'
2024
+ >>> len(slang)
2025
+ 120
2026
+ """
2027
+ return dict(sorted(self.mapping.items()))
2028
+
2029
+ def lookup(self, word: str) -> str | None:
2030
+ """
2031
+ Look up a single slang word without normalizing a full sentence.
2032
+
2033
+ Args:
2034
+ word: Word to look up (case-insensitive).
2035
+
2036
+ Returns:
2037
+ The replacement string, or None if not found.
2038
+
2039
+ Example:
2040
+ >>> slang.lookup("bgt")
2041
+ 'banget'
2042
+ >>> slang.lookup("makan")
2043
+ None
2044
+ """
2045
+ return self.mapping.get(word.lower())
2046
+
2047
+ # ─── Dunder helpers ──────────────────────────────────────────────────────
2048
+
2049
+ def __len__(self) -> int:
2050
+ """Return number of entries in the slang dictionary."""
2051
+ return len(self.mapping)
2052
+
2053
+ def __contains__(self, word: str) -> bool:
2054
+ """Check if a word is in the slang dictionary (case-insensitive)."""
2055
+ return word.lower() in self.mapping
2056
+
2057
+ def __repr__(self) -> str:
2058
+ return f"SlangNormalizer(entries={len(self.mapping)})"
2059
+
2060
+
2061
+ # Backward-compatible alias
2062
+ SlangDictionary = SlangNormalizer
2063
+
2064
+ # ─────────────────────────────────────────────────────────────────────────────
2065
+ # MODULE-LEVEL CONVENIENCE SINGLETON
2066
+ #
2067
+ # Import and use directly without instantiation:
2068
+ # from basa.core.slang import slang
2069
+ # slang.normalize("gw gamau pergi")
2070
+ # ─────────────────────────────────────────────────────────────────────────────
2071
+ slang = SlangNormalizer()