Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions data/malay_indonesian_islamic_terms.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"version": "1.0.0",
"language": "ms-id",
"total_terms": 5,
"description": "Islamic terminology database for Malay and Indonesian with Arabic loanwords, regional variations, and Jawi support.",
"terms": [
{
"id": "ms-id-ibada-solat",
"malay_term": "Solat",
"indonesian_term": "Shalat",
"jawi_term": "صلاة",
"arabic_original": "صلاة",
"arabic_transliteration": "Salat",
"english_equivalent": "Prayer / Daily Ritual Prayer",
"category": "ibada",
"definition_ms": "Rukun Islam kedua yang merangkumi perbuatan dan perkataan tertentu yang dimulakan dengan takbiratul ihram dan disudahi dengan salam.",
"definition_id": "Rukun Islam kedua yang terdiri dari perkataan dan perbuatan tertentu yang dimulai dengan takbir dan diakhiri dengan salam.",
"variants": ["solat", "shalat", "sembahyang", "salat"],
"regional_notes": "'Solat' is standard in Malaysia, Brunei, and Singapore; 'Shalat' or 'Salat' is standard in Indonesia. 'Sembahyang' is traditional."
},
{
"id": "ms-id-ibada-puasa",
"malay_term": "Puasa",
"indonesian_term": "Puasa",
"jawi_term": "ڤواس",
"arabic_original": "صوم",
"arabic_transliteration": "Sawm",
"english_equivalent": "Fasting",
"category": "ibada",
"definition_ms": "Menahan diri daripada makan, minum dan perkara yang membatalkan puasa dari terbit fajar hingga terbenam matahari.",
"definition_id": "Menahan diri dari makan, minum, dan segala yang membatalkan dari terbit fajar hingga terbenam matahari dengan niat.",
"variants": ["puasa", "shaum", "sawm"],
"regional_notes": "'Puasa' is universal in Malay and Indonesian. 'Shaum' is often used in formal or religious discourse in Indonesia."
}
]
}
8 changes: 8 additions & 0 deletions malay/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
""Malay and Indonesian Language Optimization module for Deen Bridge AI."""

from __future__ import annotations

from malay.models import MalayQueryRequest, MalayQueryResponse
from malay.processor import MalayProcessor

__all__ = ["MalayQueryRequest", "MalayQueryResponse", "MalayProcessor"]
7 changes: 7 additions & 0 deletions malay/code_switching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from __future__ import annotations

class CodeSwitchingHandler:
@staticmethod
def process_code_switching(text: str) -> str:
# Handles mixed Arabic and Malay/Indonesian expressions smoothly
return text.replace("Allahu Akbar", "Allah Maha Besar").replace("Subhanallah", "Maha Suci Allah")
10 changes: 10 additions & 0 deletions malay/cultural_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from __future__ import annotations

class CulturalContextManager:
@staticmethod
def enrich_response(response: str, dialect: str) -> str:
if dialect == "indonesia":
prefix = "Assalamu'alaikum warahmatullahi wabarakatuh. Berdasarkan pemahaman Islam di Nusantara (Indonesia): "
else:
prefix = "Assalamu'alaikum warahmatullahi wabarakatuh. Berdasarkan panduan syariah di rantau Melayu (Malaysia): "
return prefix + response
37 changes: 37 additions & 0 deletions malay/dialects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from __future__ import annotations

class DialectHandler:
@staticmethod
def detect_dialect(text: str) -> str:
lower = text.lower()
indonesian_markers = ["shalat", "puasa", "uang", "rumah", "dengan", "tidak", "kalian", "bagaimana"]
malaysian_markers = ["solat", "wang", "rumah", "dengan", "tidak", "awak", "kamu", "macam mana"]

ind_score = sum(1 for m in indonesian_markers if m in lower)
mal_score = sum(1 for m in malaysian_markers if m in lower)

if "shalat" in lower or "uang" in lower:
return "indonesia"
if "solat" in lower or "wang" in lower:
return "malaysia"

return "indonesia" if ind_score >= mal_score else "malaysia"

@staticmethod
def normalize_dialect(text: str, target_dialect: str) -> str:
words = text.split()
normalized = []
for w in words:
lw = w.lower()
if target_dialect == "malaysia":
if lw == "shalat":
w = "solat"
elif lw == "uang":
w = "wang"
elif target_dialect == "indonesia":
if lw == "solat":
w = "shalat"
elif lw == "wang":
w = "uang"
normalized.append(w)
return " ".join(normalized)
9 changes: 9 additions & 0 deletions malay/generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from __future__ import annotations

class MalayContentGenerator:
@staticmethod
def generate_islamic_content(topic: str, dialect: str) -> str:
if dialect == "indonesia":
return f"Penjelasan mengenai {topic} dalam konteks ajaran Islam yang rahmatan lil 'alamin di Indonesia."
else:
return f"Penerangan mengenai {topic} menurut panduan Ahli Sunnah Wal Jamaah di Malaysia."
18 changes: 18 additions & 0 deletions malay/loanwords.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import annotations

class LoanwordRecognizer:
ARABIC_LOANWORD_PREFIXES_OR_PATTERNS = [
"ilmu", "makhluk", "syukur", "sabar", "tauhid", "iman", "takwa", "istighfar",
"alhamdulillah", "insyaAllah", "bismillah", "qadar", "zakat", "puasa", "solat", "shalat"
]

@classmethod
def recognize_loanwords(cls, text: str) -> list[str]:
words = text.split()
found = []
for w in words:
lw = w.strip(",.?!;:()").lower()
if lw in cls.ARABIC_LOANWORD_PREFIXES_OR_PATTERNS or lw.startswith("al-") or lw.endswith("ah") or lw.endswith("at"):
if lw not in found:
found.append(lw)
return found
20 changes: 20 additions & 0 deletions malay/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from __future__ import annotations

from typing import Any, Optional
from pydantic import BaseModel, Field

class MalayQueryRequest(BaseModel):
text: str = Field(..., description="Input text in Malay or Indonesian, Latin or Jawi script")
dialect: Optional[str] = Field(None, description="Regional dialect: 'malaysia' or 'indonesia'")
target_script: Optional[str] = Field("latin", description="Target script for output: 'latin' or 'jawi'")
context: Optional[dict[str, Any]] = Field(default_factory=dict)

class MalayQueryResponse(BaseModel):
original_text: str
normalized_text: str
detected_dialect: str
detected_script: str
transliterated_text: Optional[str] = None
islamic_terms_identified: list[dict[str, Any]] = Field(default_factory=list)
optimized_response: str
confidence: float = 1.0
7 changes: 7 additions & 0 deletions malay/optimizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from __future__ import annotations

class QueryOptimizer:
@staticmethod
def optimize_query(query: str) -> str:
# Clean up and optimize query tokens for search and RAG retrieval
return query.strip().lower()
68 changes: 68 additions & 0 deletions malay/processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from __future__ import annotations

from typing import Any
from malay.models import MalayQueryRequest, MalayQueryResponse
from malay.dialects import DialectHandler
from malay.terminology import TerminologyLexicon
from malay.loanwords import LoanwordRecognizer
from malay.cultural_context import CulturalContextManager
from malay.code_switching import CodeSwitchingHandler
from malay.generator import MalayContentGenerator
from malay.optimizer import QueryOptimizer
from malay.router import MalayRouter

class MalayProcessor:
def __init__(self) -> None:
self.lexicon = TerminologyLexicon()

def detect_script(self, text: str) -> str:
# Simple heuristic: Arabic characters imply Jawi
arabic_chars = set("ابتثجحخدذرزسشصضطظعغفقكلمنهويأإآةىئؤإؤلاء")
if any(c in arabic_chars for c in text):
return "jawi"
return "latin"

def transliterate_to_jawi(self, text: str) -> str:
# Basic demonstration transliteration mapping
mapping = {
"solat": "صلاة",
"shalat": "صلاة",
"puasa": "ڤواس",
"allah": "الله",
"muhammad": "محمد"
}
words = text.split()
res = []
for w in words:
lw = w.lower().strip(",.?!;:()")
res.append(mapping.get(lw, w))
return " ".join(res)

def process(self, request: MalayQueryRequest) -> MalayQueryResponse:
script = self.detect_script(request.text)
dialect = request.dialect or DialectHandler.detect_dialect(request.text)

normalized = QueryOptimizer.optimize_query(request.text)
normalized = DialectHandler.normalize_dialect(normalized, dialect)
normalized = CodeSwitchingHandler.process_code_switching(normalized)

terms = self.lexicon.identify_terms(normalized)
loanwords = LoanwordRecognizer.recognize_loanwords(normalized)

base_content = MalayContentGenerator.generate_islamic_content(normalized, dialect)
enriched = CulturalContextManager.enrich_response(base_content, dialect)

transliterated = None
if request.target_script == "jawi" or script == "jawi":
transliterated = self.transliterate_to_jawi(enriched)

return MalayQueryResponse(
original_text=request.text,
normalized_text=normalized,
detected_dialect=dialect,
detected_script=script,
transliterated_text=transliterated,
islamic_terms_identified=terms,
optimized_response=enriched,
confidence=0.98
)
8 changes: 8 additions & 0 deletions malay/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from __future__ import annotations

class MalayRouter:
@staticmethod
def route_request(text: str) -> str:
if any(term in text.lower() for term in ["shalat", "zakat", "haji", "puasa"]):
return "fiqh_ibadat"
return "general_islamic_qa"
50 changes: 50 additions & 0 deletions malay/terminology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from __future__ import annotations

import json
import os
from typing import Any

class TerminologyLexicon:
def __init__(self) -> None:
self.terms: list[dict[str, Any]] = []
self._load_lexicon()

def _load_lexicon(self) -> None:
path = os.path.join("data", "malay_indonesian_islamic_terms.json")
if os.path.exists(path):
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
self.terms = data.get("terms", [])
except Exception:
self.terms = []
if not self.terms:
# Fallback default terms
self.terms = [
{
"id": "ms-id-ibada-solat",
"malay_term": "Solat",
"indonesian_term": "Shalat",
"jawi_term": "صلاة",
"variants": ["solat", "shalat", "sembahyang", "salat"]
}
]

def lookup(self, term: str) -> dict[str, Any] | None:
tl = term.lower()
for t in self.terms:
if tl == t.get("malay_term", "").lower() or tl == t.get("indonesian_term", "").lower():
return t
if tl in [v.lower() for v in t.get("variants", [])]:
return t
return None

def identify_terms(self, text: str) -> list[dict[str, Any]]:
found = []
words = text.split()
for w in words:
cleaned = w.strip(",.?!;:()").lower()
res = self.lookup(cleaned)
if res and res not in found:
found.append(res)
return found
26 changes: 26 additions & 0 deletions tests/test_malay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

import pytest
from malay.models import MalayQueryRequest
from malay.processor import MalayProcessor

def test_malay_indonesian_processor() -> None:
processor = MalayProcessor()
request = MalayQueryRequest(text="Bagaimana cara mengerjakan shalat dan puasa?", dialect="indonesia", target_script="latin")
response = processor.process(request)

assert response.detected_dialect == "indonesia"
assert response.detected_script == "latin"
assert len(response.islamic_terms_identified) > 0
assert "shalat" in response.normalized_text or "solat" in response.normalized_text
assert "Assalamu'alaikum" in response.optimized_response

def test_jawi_transliteration() -> None:
processor = MalayProcessor()
request = MalayQueryRequest(text="solat puasa allah", target_script="jawi")
response = processor.process(request)

assert response.detected_script == "latin"
assert response.transliterated_text is not None
assert "صلاة" in response.transliterated_text
assert "ڤواس" in response.transliterated_text
Loading