A shared vernacular layer for civic data dashboards — drop-in, zero-backend, client-side. One small script gives any dashboard a language switcher across Indian languages without ever translating the data.
Built to close the single biggest gap in a portfolio of India public-goods dashboards: they were English- and GIS-literate only. bhasha is the reusable fix — add it once per app instead of rebuilding i18n every time.
Live demo: ashwask.github.io/bhasha — switch to हिन्दी / ಕನ್ನಡ (bundled, offline) or தமிழ் / తెలుగు / मराठी / বাংলা (machine fallback). Or open standalone.html locally — it's fully self-contained (no server).
bhasha translates UI chrome and explanatory prose — labels, headings, guidance. It never touches authoritative data: place names, numbers, units, sources. Those are shown exactly as published.
This is deliberate. Translating "Bengaluru → ಬೆಂಗಳೂರು" or a sourced figure would breach faithful presentation — the value the whole civic-data project rests on. bhasha enforces the line in code: nothing is translated unless you opt it in, and protected elements are skipped no matter what.
- Translate this: wrap it in
data-bhasha→<span data-bhasha>Air Quality</span> - Never translate this: wrap it in
bhasha-keep/data-bhasha-keep→<span class="bhasha-keep">182 µg/m³</span>
<script src="bhasha.js"></script>
<script>
Bhasha.init({
dict: { // your bundled, offline dictionary
hi: { "Air Quality": "वायु गुणवत्ता", /* … */ },
kn: { "Air Quality": "ವಾಯು ಗುಣಮಟ್ಟ", /* … */ }
}
});
</script>Then tag your UI strings with data-bhasha and your data cells with class="bhasha-keep". That's the whole integration.
Call Bhasha.init(...) after mount, pointing root at your app node. Re-run _collect() semantics by re-initialising after route changes, or use mode:'opt-out' (below) so you don't have to tag every string.
| Mode | What gets translated | Use when |
|---|---|---|
opt-in (default) |
only [data-bhasha] elements |
safest — data is protected unless you forget to tag it |
opt-out |
all text under root except keepSelector |
fastest to adopt on an existing page; you protect data instead of tagging prose |
In opt-out, protect data with class="bhasha-keep", [data-value], <code>, etc. (configurable via keepSelector).
- Bundled dictionary (
dict) — hand-authored, high quality, works fully offline. Recommended for your core UI strings and your priority languages. translate(text, lang)hook — optional async fallback for the long tail. Results are cached inlocalStorage(each string leaves the browser at most once) and marked with a dotted underline so machine output is honest. Ships with a keyless MyMemory adapter; omit it for a fully-offline build.- English — if neither has it, the string stays in English. No garbling, ever.
Set an ai(question, lang, contextText) hook to add an "Ask in your language" panel — the PDGI "humans-in-the-loop" pattern. The user asks in any language; the model answers in that language, grounded only in the page's canonical data (it explains the values, it doesn't restate or translate them). A Claude adapter is included.
Security: never ship an API key in client-side code. Point the adapter at a thin serverless proxy (Cloudflare Worker / Pages Function) that holds the key. The demo leaves it unset, so the panel is inert until you wire a proxy.
Bhasha.init({
languages, // [{code, native}] — defaults to en/hi/kn/ta/te/mr/bn
dict, // { lang: { "English source": "translated" } }
translate, // async (text, lang) => string | null (optional)
ai, // async (q, lang, contextText) => string (optional)
mode, // 'opt-in' (default) | 'opt-out'
root, // default document.body
selector, // opt-in target, default '[data-bhasha]'
keepSelector, // never-translate, default protects data cells/code
position, // 'bottom-right' (default) | 'bottom-left' | 'top-…'
defaultLang, // 'en'
note // faithful-data disclosure shown in the panel
});
Bhasha.setLang('hi'); // programmatic switch; persists to localStorageMark the region the AI should read as context with data-bhasha-context.
Each single-file dashboard (Bhoomi, rtpv-agropv-explorer, india-energy-story, oaq-health-burden, urban-thesis-tracker …) needs three things:
<script src="bhasha.js">+ a smalldictfor its ~20–40 UI strings,data-bhashaon those strings,bhasha-keepon the data cells.
Framework apps (village-espresso, IndiaTemperatureMap) use mode:'opt-out' and protect the map/data layers.
| Dashboard | Type | Scope | Status |
|---|---|---|---|
| Bhoomi | environmental map + climate-risk | full UI (labels, controls, guidance) | ✅ merged — English / हिन्दी / ಕನ್ನಡ |
| india-energy-story | data narrative | nav + section structure (body prose stays English — data-dense) | ✅ merged — English / हिन्दी / ಕನ್ನಡ |
What the two show: dashboards translate almost fully; data-dense narratives translate their chrome + structure, and the body is better served by the conversational "ask in your language" layer than by translating figure-laden prose. Either way, no number, place name or source is ever altered.
Before shipping to users: bundled dictionaries are only as good as their authoring. The Hindi/Kannada strings in these adoptions are authored + machine-verified but not yet native-speaker reviewed — do that review before a dashboard's vernacular mode is called "done".
bhasha is itself built to the principles it helps others meet: client-side (privacy — no data leaves the browser by default), open format, no lock-in, MIT. It moves the "grassroots and reaching the divide-affected" row from ⛳ toward ✅ — conversational and vernacular access without touching the authoritative data. Framework: pdgi.org.
MIT © 2026 Ashwin S Kulkarni