"""Light i18n layer. Two jobs: 1. UI string translations for English / Kannada / Hindi. 2. Resolve a native-script (Kannada/Hindi) place query to the English place names in the data, via transliteration + fuzzy matching. NOTE: the Kannada/Hindi UI strings are standard civic terms but should be sanity-checked by a native speaker before the finale. """ import re import difflib import unicodedata from indic_transliteration import sanscript from indic_transliteration.sanscript import transliterate # label -> code, and code -> Web Speech API locale LANGS = {"English": "en", "ಕನ್ನಡ": "kn", "हिन्दी": "hi"} SPEECH_LANG = {"en": "en-IN", "kn": "kn-IN", "hi": "hi-IN"} STRINGS = { "title": { "en": "ParkSight — Parking-Induced Congestion Intelligence", "kn": "ಪಾರ್ಕ್‌ಸೈಟ್ — ಪಾರ್ಕಿಂಗ್ ದಟ್ಟಣೆ ವಿಶ್ಲೇಷಣೆ", "hi": "पार्कसाइट — पार्किंग जनित भीड़ विश्लेषण"}, "kpi_violations": {"en": "Parking violations", "kn": "ಪಾರ್ಕಿಂಗ್ ಉಲ್ಲಂಘನೆಗಳು", "hi": "पार्किंग उल्लंघन"}, "kpi_zones": {"en": "Impact zones", "kn": "ಪ್ರಭಾವ ವಲಯಗಳು", "hi": "प्रभाव क्षेत्र"}, "kpi_high": {"en": "High-impact (CII ≥ 70)", "kn": "ಹೆಚ್ಚು-ಪ್ರಭಾವ (CII ≥ 70)", "hi": "उच्च-प्रभाव (CII ≥ 70)"}, "kpi_mae": {"en": "Forecast MAE", "kn": "ಮುನ್ಸೂಚನೆ MAE", "hi": "पूर्वानुमान MAE"}, "voice_nav": {"en": "Voice / command navigation", "kn": "ಧ್ವನಿ / ಆದೇಶ ಸಂಚಲನೆ", "hi": "वॉइस / कमांड नेविगेशन"}, "ask": {"en": "Ask in plain language", "kn": "ಕನ್ನಡದಲ್ಲಿ ಹುಡುಕಿ", "hi": "हिंदी में खोजें"}, "placeholder": {"en": "e.g. 'show worst zones in Shivaji Nagar' · 'read top 5'", "kn": "ಉದಾ: 'ಶಿವಾಜಿನಗರದ ಕೆಟ್ಟ ವಲಯಗಳು'", "hi": "उदा: 'शिवाजी नगर के सबसे खराब क्षेत्र'"}, "speak": {"en": "🎤 Speak", "kn": "🎤 ಮಾತನಾಡಿ", "hi": "🎤 बोलें"}, "stop": {"en": "⏹ Stop", "kn": "⏹ ನಿಲ್ಲಿಸಿ", "hi": "⏹ रोकें"}, "understood": {"en": "Understood", "kn": "ಅರ್ಥವಾಯಿತು", "hi": "समझ गया"}, "tab_map": {"en": "🗺️ Impact map", "kn": "🗺️ ಪ್ರಭಾವ ನಕ್ಷೆ", "hi": "🗺️ प्रभाव नक्शा"}, "tab_ops": {"en": "🚨 Live alerts", "kn": "🚨 ಲೈವ್ ಎಚ್ಚರಿಕೆ", "hi": "🚨 लाइव अलर्ट"}, "tab_rank": {"en": "📋 Enforcement priorities", "kn": "📋 ಜಾರಿ ಆದ್ಯತೆಗಳು", "hi": "📋 प्रवर्तन प्राथमिकताएँ"}, "tab_off": {"en": "🚨 Repeat offenders", "kn": "🚨 ಪುನರಾವರ್ತಿತ ಅಪರಾಧಿಗಳು", "hi": "🚨 बार-बार उल्लंघनकर्ता"}, "tab_fc": {"en": "🔮 Tomorrow's forecast", "kn": "🔮 ನಾಳಿನ ಮುನ್ಸೂಚನೆ", "hi": "🔮 कल का पूर्वानुमान"}, "tab_patrol": {"en": "🗓️ Patrol planner", "kn": "🗓️ ಗಸ್ತು ಯೋಜನೆ", "hi": "🗓️ गश्त योजना"}, "tab_whatif": {"en": "🧪 What-if simulator", "kn": "🧪 ವಾಟ್-ಇಫ್ ಸಿಮ್ಯುಲೇಟರ್", "hi": "🧪 व्हाट-इफ सिम्युलेटर"}, "tab_event": {"en": "🎪 Event mode", "kn": "🎪 ಈವೆಂಟ್ ಮೋಡ್", "hi": "🎪 इवेंट मोड"}, "min_cii": {"en": "Minimum CII to display", "kn": "ಪ್ರದರ್ಶಿಸಲು ಕನಿಷ್ಠ CII", "hi": "दिखाने हेतु न्यूनतम CII"}, "top_n_zones": {"en": "Show top N zones", "kn": "ಮೇಲಿನ N ವಲಯಗಳನ್ನು ತೋರಿಸಿ", "hi": "शीर्ष N क्षेत्र दिखाएँ"}, "dl_priorities": {"en": "⬇️ Download enforcement priorities (CSV)", "kn": "⬇️ ಜಾರಿ ಆದ್ಯತೆಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ (CSV)", "hi": "⬇️ प्रवर्तन प्राथमिकताएँ डाउनलोड करें (CSV)"}, "repeat_offenders": {"en": "Repeat offenders", "kn": "ಪುನರಾವರ್ತಿತ ಅಪರಾಧಿಗಳು", "hi": "बार-बार उल्लंघनकर्ता"}, "share_violations": {"en": "Share of all violations", "kn": "ಎಲ್ಲಾ ಉಲ್ಲಂಘನೆಗಳ ಪಾಲು", "hi": "कुल उल्लंघनों में हिस्सा"}, "worst_vehicle": {"en": "Worst single vehicle", "kn": "ಅತಿ ಕೆಟ್ಟ ಏಕೈಕ ವಾಹನ", "hi": "सबसे खराब एकल वाहन"}, "search_vehicle": {"en": "Search a vehicle ID or area", "kn": "ವಾಹನ ID ಅಥವಾ ಪ್ರದೇಶ ಹುಡುಕಿ", "hi": "वाहन ID या क्षेत्र खोजें"}, "top_n_off": {"en": "Show top N offenders", "kn": "ಮೇಲಿನ N ಅಪರಾಧಿಗಳನ್ನು ತೋರಿಸಿ", "hi": "शीर्ष N उल्लंघनकर्ता दिखाएँ"}, "dl_offenders": {"en": "⬇️ Download offender watchlist (CSV)", "kn": "⬇️ ಅಪರಾಧಿಗಳ ಪಟ್ಟಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ (CSV)", "hi": "⬇️ उल्लंघनकर्ता सूची डाउनलोड करें (CSV)"}, "language": {"en": "Language", "kn": "ಭಾಷೆ", "hi": "भाषा"}, "theme": {"en": "Theme", "kn": "ಥೀಮ್", "hi": "थीम"}, "dark": {"en": "🌙 Dark", "kn": "🌙 ಕಪ್ಪು", "hi": "🌙 डार्क"}, "light": {"en": "☀️ Light", "kn": "☀️ ಬೆಳಕು", "hi": "☀️ लाइट"}, "history": {"en": "🕘 Your searches this session", "kn": "🕘 ಈ ಅವಧಿಯ ಹುಡುಕಾಟಗಳು", "hi": "🕘 इस सत्र की खोजें"}, "clear_history": {"en": "Clear history", "kn": "ಇತಿಹಾಸ ಅಳಿಸಿ", "hi": "इतिहास साफ़ करें"}, "no_history": {"en": "No searches yet.", "kn": "ಇನ್ನೂ ಹುಡುಕಾಟಗಳಿಲ್ಲ.", "hi": "अभी तक कोई खोज नहीं।"}, "tab_trends": {"en": "📈 Trends", "kn": "📈 ಪ್ರವೃತ್ತಿಗಳು", "hi": "📈 रुझान"}, "t_daily": {"en": "Daily violations", "kn": "ದೈನಂದಿನ ಉಲ್ಲಂಘನೆಗಳು", "hi": "दैनिक उल्लंघन"}, "t_hourly": {"en": "By hour of day", "kn": "ಗಂಟೆಯ ಪ್ರಕಾರ", "hi": "घंटे के अनुसार"}, "t_vehicle": {"en": "By vehicle type", "kn": "ವಾಹನ ಪ್ರಕಾರ", "hi": "वाहन प्रकार के अनुसार"}, "t_vtype": {"en": "By violation type", "kn": "ಉಲ್ಲಂಘನೆ ಪ್ರಕಾರ", "hi": "उल्लंघन प्रकार के अनुसार"}, "t_dow": {"en": "By day of week", "kn": "ವಾರದ ದಿನದ ಪ್ರಕಾರ", "hi": "सप्ताह के दिन अनुसार"}, "t_gauge": {"en": "Top-100 zones · share of all violations", "kn": "ಮೇಲಿನ 100 ವಲಯಗಳ ಪಾಲು", "hi": "शीर्ष 100 क्षेत्रों का हिस्सा"}, "live_replay": {"en": "▶ Live replay (simulated from historical data)", "kn": "▶ ಲೈವ್ ರಿಪ್ಲೇ (ಐತಿಹಾಸಿಕ ದತ್ತಾಂಶದಿಂದ)", "hi": "▶ लाइव रीप्ले (ऐतिहासिक डेटा से)"}, } def t(key, lang): """Translate a UI string key into the chosen language (falls back to English).""" return STRINGS.get(key, {}).get(lang) or STRINGS.get(key, {}).get("en", key) def _norm(s): s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode() s = re.sub(r"[^a-z]", "", s.lower()) s = re.sub(r"([a-z])\1+", r"\1", s) # collapse doubled letters return s.replace("nagara", "nagar") def romanize(text, lang): scr = {"kn": sanscript.KANNADA, "hi": sanscript.DEVANAGARI}.get(lang) if scr is None: return text try: return transliterate(text, scr, sanscript.IAST) except Exception: return text def build_area_vocab(hot): """Distinct locality names from the hotspot location strings.""" areas = set() for loc in hot["location"].dropna(): for p in [x.strip() for x in str(loc).split(",")][1:4]: pl = p.lower() if p and "bengaluru" not in pl and "karnataka" not in pl and "pin" not in pl: areas.add(p) return sorted(areas) def resolve_area(query, lang, area_vocab): """Map a (possibly Kannada/Hindi) place query to an English locality name.""" has_native = any(ord(c) > 127 for c in query) rom = romanize(query, lang) if (has_native or lang != "en") else query key = _norm(rom) if not key: return None norm_map = {_norm(a): a for a in area_vocab} best = difflib.get_close_matches(key, list(norm_map.keys()), n=1, cutoff=0.55) return norm_map[best[0]] if best else None