<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Swayam Jethi</title>
    <description>The latest articles on DEV Community by Swayam Jethi (@swayam42).</description>
    <link>https://dev.to/swayam42</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3390233%2Fb6627064-2c22-4000-8083-75780114795a.png</url>
      <title>DEV Community: Swayam Jethi</title>
      <link>https://dev.to/swayam42</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swayam42"/>
    <language>en</language>
    <item>
      <title>Building Mo Saathi: An Odia-First AI Voice Learning Companion</title>
      <dc:creator>Swayam Jethi</dc:creator>
      <pubDate>Sat, 15 Aug 2026 17:02:17 +0000</pubDate>
      <link>https://dev.to/swayam42/building-mo-saathi-an-odia-first-ai-voice-learning-companion-41nj</link>
      <guid>https://dev.to/swayam42/building-mo-saathi-an-odia-first-ai-voice-learning-companion-41nj</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR: What is Mo Saathi?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The Problem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Students in Odisha lack digital learning tools in their native language (Odia).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The Solution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Mo Saathi (ମୋ ସାଥୀ)&lt;/strong&gt;, an Odia-first AI voice learning companion.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core Stack&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LiveKit (WebRTC/SIP), Sarvam AI (Odia STT), Google Gemini (LLM), Murf Falcon (TTS).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Key Features&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Persistent Memory, RAG (Syllabus), Practice Tools, Outbound Phone Calls, Teacher Escalation, Specialist Handoff.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target Audience&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Odia-medium school students needing accessible, conversational tutoring.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The Problem and the Users&lt;/li&gt;
&lt;li&gt;How the System Works&lt;/li&gt;
&lt;li&gt;The 9-Day Engineering Journey&lt;/li&gt;
&lt;li&gt;Challenges and How I Overcame Them&lt;/li&gt;
&lt;li&gt;How You Can Build and Run It&lt;/li&gt;
&lt;li&gt;What I Would Improve Next&lt;/li&gt;
&lt;li&gt;Links and Resources&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Problem and the Users
&lt;/h2&gt;

&lt;p&gt;A student should not have to translate their question into English before they can learn from an AI tutor. In real conversations, language is rarely perfectly formal. Students naturally mix Odia with English terms like &lt;em&gt;Physics&lt;/em&gt;, &lt;em&gt;Carbon&lt;/em&gt;, or &lt;em&gt;Photosynthesis&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;Most educational AI tools treat regional Indian languages as an afterthought, relying on slow, generic translation APIs. &lt;strong&gt;Mo Saathi&lt;/strong&gt; was designed around an Odia-first interaction model for students in Odisha. &lt;/p&gt;

&lt;p&gt;The goal was simple: A student says, &lt;em&gt;"ଆଜି ଆମେ Physics ବିଷୟରେ ପଢ଼ିବା?"&lt;/em&gt;, and the system understands it natively, responds conversationally, and teaches the concept without forcing the student to change how they speak.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the System Works
&lt;/h2&gt;

&lt;p&gt;Mo Saathi is split into two layers to separate real-time transport from application logic.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Client Layer (Next.js):&lt;/strong&gt; A distraction-free, hand-drawn "pencil box" aesthetic interface. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Agent Layer (Python):&lt;/strong&gt; A LiveKit Agents backend coordinating the STT, LLM, TTS, tools, and local SQLite memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1cfdrdwin2mo0km7umlo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1cfdrdwin2mo0km7umlo.png" alt="Mo Saathi System Architecture" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

The final Mo Saathi architecture, showing the real-time voice layer, AI services, memory, tools and human support.
&lt;a href="https://cdn.jsdelivr.net/gh/Swayam42/voice-agent-for-bharat-2026@main/docs/architecture.svg" rel="noopener noreferrer"&gt;View the architecture in HD&lt;/a&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LiveKit WebRTC&lt;/td&gt;
&lt;td&gt;Captures real-time student audio.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transcription&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sarvam AI&lt;/td&gt;
&lt;td&gt;Natively transcribes code-mixed Odia/English instantly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reasoning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gemini 3.5 Flash Lite&lt;/td&gt;
&lt;td&gt;Processes context, decides to explain, use a tool, or hand off.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Synthesis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Murf Falcon (Anisha)&lt;/td&gt;
&lt;td&gt;Streams ultra-low latency conversational Odia speech back to the student.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The 9-Day Engineering Journey
&lt;/h2&gt;

&lt;p&gt;I built Mo Saathi as part of the &lt;strong&gt;10 Days of Voice Agents — VoiceForBharat Edition&lt;/strong&gt;. Here is how it evolved from a simple script into a stateful educational platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Day 1–3: The Voice Pipeline and Persona
&lt;/h3&gt;

&lt;p&gt;The first hurdle was simply making the agent hear and speak Odia naturally. I integrated &lt;strong&gt;Sarvam AI&lt;/strong&gt; for native Indic speech-to-text, and &lt;strong&gt;Murf Falcon&lt;/strong&gt; for blazing-fast speech generation. I crafted a strict system prompt to ensure the agent acted as a tutor—explaining concepts step-by-step rather than just behaving like a search engine. I also built the Next.js frontend with a hand-drawn, paper-like UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fij8jnyhhp9as2fdqwzvy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fij8jnyhhp9as2fdqwzvy.png" alt="Mo Saathi Interface" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Day 4: Memory and RAG (Grounding the AI)
&lt;/h3&gt;

&lt;p&gt;A tutor is useless if it forgets you or hallucinates facts. &lt;br&gt;
I implemented a &lt;strong&gt;local SQLite database&lt;/strong&gt; to persist student profiles across sessions. When a student joins, the agent recalls their name and past struggles. To prevent hallucinations, I built a local RAG pipeline (&lt;code&gt;sentence-transformers&lt;/code&gt;) that injects actual Class 9 and 10 Science and Maths syllabus content into the LLM context before it answers.&lt;/p&gt;
&lt;h3&gt;
  
  
  Day 5: Interactive Tools (Practice Exercises)
&lt;/h3&gt;

&lt;p&gt;I didn't want the LLM hallucinating math problems. Instead, I gave the agent a &lt;code&gt;get_next_exercise&lt;/code&gt; function tool. Now, when a student asks for practice, the LLM triggers the tool, fetches a structured question from a local question bank, and evaluates the student's spoken answer dynamically.&lt;/p&gt;
&lt;h3&gt;
  
  
  Day 6: Outbound SIP Phone Calls (Study Reminders)
&lt;/h3&gt;

&lt;p&gt;Learning shouldn't depend on opening a web browser. I integrated LiveKit's SIP outbound trunking. A student can ask, &lt;em&gt;"Remind me to study at 5 PM."&lt;/em&gt; A background Python scheduler polls the SQLite database, dials the student's actual phone number using Linphone, and greets them by name.&lt;br&gt;
&lt;em&gt;(See the live phone call demonstration on my &lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7492950604848386049/" rel="noopener noreferrer"&gt;LinkedIn post&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Day 7: Human Escalation (Knowing when to stop)
&lt;/h3&gt;

&lt;p&gt;AI isn't perfect. If a student expresses emotional distress or repeatedly fails an exercise, the agent halts the lesson. It asks for consent and triggers a &lt;code&gt;create_escalation&lt;/code&gt; tool. This fires a colored-priority email to a human teacher via the Resend API, keeping human oversight in the loop.&lt;/p&gt;
&lt;h3&gt;
  
  
  Day 8: Analytics and Call Outcomes
&lt;/h3&gt;

&lt;p&gt;It’s easy to track if an AI &lt;em&gt;talked&lt;/em&gt;, but did it &lt;em&gt;teach&lt;/em&gt;? I built a real-time Next.js &lt;code&gt;/analytics&lt;/code&gt; dashboard. A call is only marked "Successful" in the database if the student actively attempted a practice exercise via the function tool. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftb42pnxy39bjcrlbowy0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftb42pnxy39bjcrlbowy0.png" alt="Analytics Dashboard" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Day 9: Specialist Handoff (Vigyan Saathi)
&lt;/h3&gt;

&lt;p&gt;A general-purpose prompt struggles with deep Physics or Biology nuances. So, I introduced a multi-agent handoff. If a student asks a complex science question, Mo Saathi (Anisha) announces a transfer: &lt;em&gt;"I am connecting you to our science expert, Vigyan Saathi."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The system hot-swaps the active agent, passing the exact conversation context so the student doesn't have to repeat themselves. The new agent switches to a different Murf Falcon voice (&lt;code&gt;Samar&lt;/code&gt;) and operates under a strict, science-focused prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@function_tool&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transfer_to_science_specialist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Pass a copy of the active conversation matrix to the new agent
&lt;/span&gt;    &lt;span class="n"&gt;science_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ScienceSpecialist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;chat_ctx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat_ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exclude_instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;science_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ବିଜ୍ଞାନ ସାଥୀ ସହ ଯୋଡ଼ୁଛି...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Challenges and How I Overcame Them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Context and Turn Ordering Rules&lt;/strong&gt;&lt;br&gt;
Gemini is incredibly strict about turn order. When injecting RAG context, if the LLM then attempted to call a tool (like the handoff or memory tool), Gemini would reject it with a &lt;code&gt;400 Bad Request&lt;/code&gt; if the message history didn't strictly alternate between User and Model. &lt;br&gt;
&lt;em&gt;Solution:&lt;/em&gt; I had to refactor the RAG pipeline to dynamically append the retrieved textbook context directly into the current User message payload, rather than appending hidden system messages to the history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Speech Output vs. Code-Mixed Text&lt;/strong&gt;&lt;br&gt;
The LLM would sometimes generate text with English translations inside brackets (e.g., "ବାଷ୍ପୀଭବନ (Evaporation)"). While correct as text, Murf TTS would read both languages awkwardly, breaking the conversational flow. &lt;br&gt;
&lt;em&gt;Solution:&lt;/em&gt; I strictly prompt-engineered the model to forbid bracketed translations, forcing it to pick one script and stick to it so the audio sounded like a natural human speaking.&lt;/p&gt;


&lt;h2&gt;
  
  
  How You Can Build and Run It
&lt;/h2&gt;

&lt;p&gt;You can run this entire multi-agent system locally. You will need API keys for LiveKit, Google Gemini, Murf AI, Sarvam AI, and Resend.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Start the Backend
&lt;/h3&gt;

&lt;p&gt;The backend runs on Python. Use &lt;code&gt;uv&lt;/code&gt; for lightning-fast dependency resolution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Swayam42/voice-agent-for-bharat-2026.git
&lt;span class="nb"&gt;cd &lt;/span&gt;voice-agent-for-bharat-2026/backend

&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env.local
&lt;span class="c"&gt;# Add your API keys to .env.local (Never commit this file!)&lt;/span&gt;

uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run python src/agent.py dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Start the Frontend
&lt;/h3&gt;

&lt;p&gt;The client is a Next.js app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../frontend

&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env.local
&lt;span class="c"&gt;# Add your LIVEKIT_URL and LIVEKIT_API_KEY&lt;/span&gt;

pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;http://localhost:3000&lt;/code&gt; to talk to Mo Saathi, and &lt;code&gt;http://localhost:3000/analytics&lt;/code&gt; to view real-time call outcomes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Improve Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline / Low-Bandwidth Mode:&lt;/strong&gt; Students in rural Odisha shouldn't be blocked by poor internet. I'd like to implement local caching for practice exercises so the learning loop can survive network drops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More Specialists:&lt;/strong&gt; Expand the multi-agent system to include a dedicated Maths specialist and an English grammar specialist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teacher Insights:&lt;/strong&gt; Enhance the escalation dashboard to highlight &lt;em&gt;why&lt;/em&gt; the student is struggling, using the RAG history to pinpoint knowledge gaps.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links and Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/Swayam42/voice-agent-for-bharat-2026" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/strong&gt; (Full Source Code)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://livekit.io/" rel="noopener noreferrer"&gt;LiveKit&lt;/a&gt;&lt;/strong&gt; (WebRTC and SIP Voice Infrastructure)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://murf.ai/" rel="noopener noreferrer"&gt;Murf Falcon&lt;/a&gt;&lt;/strong&gt; (Low-latency TTS API)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sarvam.ai/" rel="noopener noreferrer"&gt;Sarvam AI&lt;/a&gt;&lt;/strong&gt; (Indic Language Speech-to-Text)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://openrouter.ai/" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt;&lt;/strong&gt; (LLM routing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building a voice agent is easy. Building a &lt;em&gt;useful learning companion&lt;/em&gt; takes memory, safety boundaries, and an understanding of how people actually speak. That is what I want to keep building.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;#VoiceForBharat #MurfAI #10DaysofAIVoiceAgents #LiveKit #AI #EdTech #BuildInPublic&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>voiceai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Real-Time Multilingual Telegram Bot with Lingo.dev</title>
      <dc:creator>Swayam Jethi</dc:creator>
      <pubDate>Mon, 23 Feb 2026 15:19:36 +0000</pubDate>
      <link>https://dev.to/swayam42/i-built-a-real-time-multilingual-telegram-bot-with-lingodev-58e1</link>
      <guid>https://dev.to/swayam42/i-built-a-real-time-multilingual-telegram-bot-with-lingodev-58e1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;LingoComm&lt;/strong&gt; is a Telegram bot that auto-translates every group message into each member's preferred language — text and voice — powered by &lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;Lingo.dev SDK&lt;/a&gt;. Live on Render, built to solve a daily frustration.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Frustration That Started This
&lt;/h2&gt;

&lt;p&gt;I'm a college student in India. My friend circle is... linguistically chaotic.&lt;/p&gt;

&lt;p&gt;Swayam types in Hindi. Sakura replies in Japanese. Erwin writes in English. Diego sends voice notes in Spanish. Everyone &lt;em&gt;sort of&lt;/em&gt; understands each other, but when someone wants to say something with nuance — jokes, sarcasm, heartfelt stuff — they switch to their native language. That's when everyone else gets lost.&lt;/p&gt;

&lt;p&gt;Our group chat had an unspoken ritual:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Someone sends a message in their language&lt;/li&gt;
&lt;li&gt;Three people copy it&lt;/li&gt;
&lt;li&gt;Three people open Google Translate&lt;/li&gt;
&lt;li&gt;Three people paste, read, forget context, scroll back up&lt;/li&gt;
&lt;li&gt;Someone replies — 40 seconds late — and the conversation has already moved on&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I watched this happen every single day. And one evening after losing track of a conversation for the third time in ten minutes, I thought: &lt;strong&gt;what if the group chat itself just... translated?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not a translation app. Not a browser extension. Not "click to translate." Just — you send a message, and everyone reads it in their own language. Automatically. Instantly. In the same chat thread.&lt;/p&gt;

&lt;p&gt;That's LingoComm.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Does LingoComm Actually Do?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Text translation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Someone sends a message in any language&lt;/li&gt;
&lt;li&gt;Bot detects the language, looks up every group member's preferred locale&lt;/li&gt;
&lt;li&gt;Translates the message into all required languages using &lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;Lingo.dev SDK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Posts translations as a threaded reply — clean, contextual, no clutter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Voice translation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reply to any voice/audio message with &lt;code&gt;/analyze&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bot transcribes speech (Deepgram Nova-2), translates the transcript (Lingo.dev), and generates audio playback in your language (Google TTS WaveNet)&lt;/li&gt;
&lt;li&gt;Interactive buttons: 📝 Original transcript, 🌐 Translated text, 🔊 Listen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;User identity:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each user sets their preferred language once (&lt;code&gt;/lang ja&lt;/code&gt;, &lt;code&gt;/lang hi&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Preference follows them across every group the bot is in&lt;/li&gt;
&lt;li&gt;First-time users get auto-detected based on what they type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Production features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Per-user cooldown + burst rate limiting&lt;/li&gt;
&lt;li&gt;Code blocks and URLs preserved through translation (placeholder extraction)&lt;/li&gt;
&lt;li&gt;Retry logic with exponential backoff for Telegram API failures&lt;/li&gt;
&lt;li&gt;HTML-safe escaping for all outgoing messages&lt;/li&gt;
&lt;li&gt;Auto-deleting &lt;code&gt;/lang&lt;/code&gt; messages in groups (keeps chat clean)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Architecture: How a Message Flows
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nh9ka8xegvouxoup2e6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nh9ka8xegvouxoup2e6.png" alt="Architecture" width="800" height="90"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Telegraf&lt;/strong&gt; receives a message event&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input guards&lt;/strong&gt; skip bots, commands, short messages, URL-only/emoji-only content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiter&lt;/strong&gt; applies cooldowns (500ms per-user, 10-message burst window)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code/URL preservation&lt;/strong&gt; — code blocks and links get replaced with placeholders before translation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language detection&lt;/strong&gt; — Unicode script analysis + keyword heuristics (more on this below)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target resolution&lt;/strong&gt; — query MongoDB for all group members, collect distinct locales, exclude source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out translation&lt;/strong&gt; — &lt;code&gt;batchLocalizeText()&lt;/code&gt; sends one API call to &lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;Lingo.dev&lt;/a&gt; for all target locales&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threaded reply&lt;/strong&gt; — translations posted as a reply to the original message&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bot framework&lt;/td&gt;
&lt;td&gt;Telegraf 4.16&lt;/td&gt;
&lt;td&gt;Event-driven, clean middleware API for Telegram&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translation engine&lt;/td&gt;
&lt;td&gt;&lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;Lingo.dev SDK&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;batchLocalizeText()&lt;/code&gt; for one-to-many fan-out in a single call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;MongoDB + Mongoose&lt;/td&gt;
&lt;td&gt;Durable user preferences + group membership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speech-to-text&lt;/td&gt;
&lt;td&gt;Deepgram Nova-2&lt;/td&gt;
&lt;td&gt;Fast, accurate auto-language-detection STT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-speech&lt;/td&gt;
&lt;td&gt;Google Cloud TTS&lt;/td&gt;
&lt;td&gt;WaveNet neural voices for natural playback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;Single web service running bot + Express API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Hard Part: Language Detection Without External APIs
&lt;/h2&gt;

&lt;p&gt;Here's a decision I made early: &lt;strong&gt;don't call an external API just to detect language.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why? In a group chat, messages arrive fast. If every message triggers a detection API call before translation even starts, latency doubles. And if that detection call hangs (which happened during testing — more on that later), the entire pipeline stalls.&lt;/p&gt;

&lt;p&gt;So I built a local detection engine. Two layers:&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Unicode Script Analysis
&lt;/h3&gt;

&lt;p&gt;Most non-Latin languages have distinct Unicode ranges. Japanese has Hiragana/Katakana, Korean has Hangul, Arabic has its own block. This is deterministic, instant, and never wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectByUnicodeScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;3040-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;30FF&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ja&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Japanese&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;AC00-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;D7AF&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ko&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Korean&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0600-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;06FF&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Arabic&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0400-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;04FF&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ru&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Russian&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0B00-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;0B7F&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Odia&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0900-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;097F&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Hindi (Devanagari)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0980-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;09FF&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Bengali&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0B80-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;0BFF&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Tamil&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0C00-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;0C7F&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;te&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Telugu&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;4E00-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;9FFF&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Chinese&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;10 scripts covered. Zero API calls. Sub-millisecond.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Keyword Heuristics (Including Hinglish)
&lt;/h3&gt;

&lt;p&gt;But what about romanized text? A Hindi speaker typing in Latin script — "aaj mujhe bahut neend aa rahi hai" — looks like English to a script detector.&lt;/p&gt;

&lt;p&gt;This is Hinglish, and my friend circle uses it constantly. So I built a keyword scoring engine with 40+ common Hindi words in Latin script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hinglishMarkers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;namaste&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kaise&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kya&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kyu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nahi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mera&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meri&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tum&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aap&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hum&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mujhe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bahut&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bohot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yaar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bhai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;karna&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chalo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jaldi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aaj&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abhi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;phir&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;samjho&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hinglishMarkers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;b`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If two or more Hinglish markers appear, the message is classified as Hindi. Same pattern extends to 8 other languages (Spanish, French, German, Portuguese, Italian, Turkish, Indonesian, Vietnamese) with their own keyword sets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters&lt;/strong&gt;: most translation bots just pass everything through a cloud detection API and hope for the best. This approach is faster, offline-capable for common cases, and I can tune it based on what my actual users type.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lingo.dev: The Core Translation Engine
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;Lingo.dev&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;After detection, the message goes to Lingo.dev for actual translation. Here's the initialization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LingoDotDevEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lingo.dev/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lingo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LingoDotDevEngine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LINGODOTDEV_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the core fan-out function that makes everything work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;translateToMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourceLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetLocales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetLocales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;sourceLocale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="c1"&gt;// Attempt 1: batch (single API call — fastest)&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;race&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;lingo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batchLocalizeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;sourceLocale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;targetLocales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;batch timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... map results to locales&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Attempt 2: parallel individual calls (fallback)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;targetLocale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;race&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
          &lt;span class="nx"&gt;lingo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localizeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sourceLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetLocale&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... collect results, use original text as fallback for failures&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two things I want to highlight:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Batch-first, parallel-fallback&lt;/strong&gt;. &lt;code&gt;batchLocalizeText()&lt;/code&gt; sends one HTTP request for all target languages. If that fails (network hiccup, timeout), the function falls back to &lt;code&gt;Promise.allSettled()&lt;/code&gt; with individual &lt;code&gt;localizeText()&lt;/code&gt; calls. The user always gets &lt;em&gt;something&lt;/em&gt; back.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explicit &lt;code&gt;sourceLocale&lt;/code&gt; always&lt;/strong&gt;. Early in development, I tried passing &lt;code&gt;null&lt;/code&gt; to let the SDK auto-detect. It hung. No error, no timeout, just... waiting. Once I switched to always providing an explicit source locale from my local detection engine, the pipeline became rock-solid. This was the single biggest reliability fix in the project.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why Lingo.dev Specifically?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;batchLocalizeText()&lt;/code&gt;&lt;/strong&gt; is exactly the API shape a group chat bot needs. One message → many languages → one API call. Most translation APIs don't offer this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The JavaScript SDK&lt;/strong&gt; was clean to integrate in an event-driven Node.js architecture. No wrappers, no adapters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast enough for real-time chat&lt;/strong&gt;. Translation comes back before the user scrolls past the original message.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Voice Pipeline: Speech → Text → Translation → Audio
&lt;/h2&gt;

&lt;p&gt;Voice is where this project got ambitious.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flkqeoxecg0wtchtflmbc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flkqeoxecg0wtchtflmbc.png" alt="Voice" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The flow: download the audio file → Deepgram transcribes it with automatic language detection → Lingo.dev translates the transcript to the user's preferred language → Google Cloud TTS generates a WaveNet voice in that language → bot sends an interactive response with three buttons.&lt;/p&gt;

&lt;p&gt;Results are cached in-memory for 1 hour with automatic cleanup. TTS is optional — if Google credentials aren't configured, the bot still works for transcription and translation, just without the audio playback button.&lt;/p&gt;




&lt;h2&gt;
  
  
  Data Model: One User, Many Groups
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;telegramId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;manuallySet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;messageCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;identity is global, membership is list-based.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A user sets their language once. That preference follows them to every group. The &lt;code&gt;groups[]&lt;/code&gt; array tracks which groups they're active in, so the bot knows who to translate for in each group.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;manuallySet&lt;/code&gt; is important — if &lt;code&gt;true&lt;/code&gt;, the bot respects the user's explicit choice and never auto-overrides it. If &lt;code&gt;false&lt;/code&gt;, the bot can update the detected language as the user types more messages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real Demo: 3 Users, 3 Languages, Zero Friction
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Swayam&lt;/strong&gt; → &lt;code&gt;/lang hi&lt;/code&gt; (Hindi)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sakura&lt;/strong&gt; → &lt;code&gt;/lang ja&lt;/code&gt; (Japanese)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Erwin&lt;/strong&gt; → &lt;code&gt;/lang en&lt;/code&gt; (English)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What happens:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Who sends&lt;/th&gt;
&lt;th&gt;What they type&lt;/th&gt;
&lt;th&gt;Sakura sees&lt;/th&gt;
&lt;th&gt;Erwin sees&lt;/th&gt;
&lt;th&gt;Swayam sees&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Swayam&lt;/td&gt;
&lt;td&gt;"aaj ka din bahut acha raha"&lt;/td&gt;
&lt;td&gt;🇯🇵 今日はとても良い一日でした&lt;/td&gt;
&lt;td&gt;🇬🇧 Today was a really good day&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(original)&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sakura&lt;/td&gt;
&lt;td&gt;"明日映画を見に行きましょう"&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(original)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;🇬🇧 Let's go see a movie tomorrow&lt;/td&gt;
&lt;td&gt;🇮🇳 कल चलो फिल्म देखने चलते हैं&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Erwin&lt;/td&gt;
&lt;td&gt;"Sounds great, what time?"&lt;/td&gt;
&lt;td&gt;🇯🇵 いいですね、何時？&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(original)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;🇮🇳 बढ़िया, कितने बजे?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No copy-paste. No app switching. No delay. The conversation just... flows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Things That Broke (and How I Fixed Them)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Hanging Detection Bug
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What happened&lt;/strong&gt;: Early on, I let Lingo.dev SDK auto-detect the source language by passing &lt;code&gt;sourceLocale: null&lt;/code&gt;. It worked... usually. But every few messages, the call would just hang. No error, no timeout, no response. The bot would silently stop translating until I restarted it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned&lt;/strong&gt;: Passing &lt;code&gt;null&lt;/code&gt; as &lt;code&gt;sourceLocale&lt;/code&gt; triggered an internal detection path in the SDK that didn't have a timeout. My user-facing experience was a bot that randomly "went silent."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Built the local Unicode + keyword detection engine described above, and &lt;em&gt;always&lt;/em&gt; pass an explicit &lt;code&gt;sourceLocale&lt;/code&gt; to every Lingo.dev call. The pipeline hasn't hung once since.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Ghost Users in Group Translation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What happened&lt;/strong&gt;: A user would set their language, join a new group, and... their translations wouldn't appear. Other group members saw translations, but this user was invisible to the fan-out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause&lt;/strong&gt;: The user's &lt;code&gt;groups[]&lt;/code&gt; array didn't include the new group ID. Their preference existed globally, but the translation target resolution (&lt;code&gt;User.find({ groups: groupId })&lt;/code&gt;) didn't find them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: I strengthened group registration at every entry point — &lt;code&gt;/start&lt;/code&gt; in a group, &lt;code&gt;/lang&lt;/code&gt; in a group, &lt;code&gt;handleNewMember()&lt;/code&gt; on join, and even on first message (auto-registration). Now &lt;code&gt;$addToSet: { groups: groupId }&lt;/code&gt; runs at every touchpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Google TTS Credentials on Cloud
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What happened&lt;/strong&gt;: TTS worked perfectly on my laptop (using &lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/code&gt; pointing to a JSON file). Deployed to Render — instant crash. The credentials file doesn't exist on cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Added &lt;code&gt;GOOGLE_CREDENTIALS_BASE64&lt;/code&gt; support. The JSON credentials get base64-encoded into an env var, decoded at startup. And I made TTS fully optional — if credentials are missing, the voice pipeline still transcribes and translates, just without the "Listen" button.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CREDENTIALS_BASE64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credentialsJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CREDENTIALS_BASE64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ttsClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;googleTextToSpeech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TextToSpeechClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credentialsJson&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Production Hardening
&lt;/h2&gt;

&lt;p&gt;Things I added that aren't glamorous but keep the bot alive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retry wrapper&lt;/strong&gt; with exponential backoff (300ms → 600ms → 1200ms) for Telegram API calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15-second timeout&lt;/strong&gt; on batch translation, &lt;strong&gt;10-second timeout&lt;/strong&gt; on individual calls — if Lingo.dev is slow, the bot doesn't freeze&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burst rate limiting&lt;/strong&gt;: max 10 messages per 5-second window per user — prevents translation spam&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code block preservation&lt;/strong&gt;: regex extracts &lt;code&gt;`code`&lt;/code&gt; and &lt;code&gt;&lt;/code&gt;blocks&lt;code&gt;&lt;/code&gt; before translation, restores after. Code should never be "translated."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL preservation&lt;/strong&gt;: same pattern — URLs get placeholder-swapped so they survive translation intact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML escaping&lt;/strong&gt; on all outgoing text to prevent parse_mode injection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful degradation&lt;/strong&gt;: if MongoDB is down → users get defaults. If TTS is down → voice still transcribes. If batch API fails → parallel fallback kicks in.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with the data model, not the bot commands.&lt;/strong&gt; The &lt;code&gt;telegramId + locale + groups[]&lt;/code&gt; schema was the best early decision I made. Everything else plugged in cleanly because the data model was right.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't trust external detection for real-time use cases.&lt;/strong&gt; Building local detection felt like over-engineering at first. It turned out to be the single biggest performance and reliability win.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Translation UX &amp;gt; translation quality.&lt;/strong&gt; Even a slightly less accurate translation, delivered instantly in-thread, is more useful than a perfect translation that requires copy-paste-wait.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Solve your own problem.&lt;/strong&gt; I built this because I was annoyed every day. That frustration gave me better instincts about what mattered (speed, threading, preserving context) than any feature spec could.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Usage-based cost optimization (cache frequent translations)&lt;/li&gt;
&lt;li&gt;Admin controls for large communities (moderation, analytics dashboards)&lt;/li&gt;
&lt;li&gt;Smarter detection for code-mixed languages beyond Hinglish&lt;/li&gt;
&lt;li&gt;Richer voice interaction (auto-translate voice in real-time, not just on-demand)&lt;/li&gt;
&lt;li&gt;Optional Discord bridge&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Swayam42/lingocomm" rel="noopener noreferrer"&gt;LingoComm repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LingoComm Live Link&lt;/strong&gt;: &lt;a href="https://lingocomm-bot.onrender.com/" rel="noopener noreferrer"&gt;Click Here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lingo.dev&lt;/strong&gt;: &lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;https://lingo.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a href="https://www.linkedin.com/in/swayam-jethi/" rel="noopener noreferrer"&gt;Swayam Jethi&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I built LingoComm because translation shouldn't be a task — it should be invisible. Lingo.dev made that possible.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegraf</category>
      <category>javascript</category>
      <category>node</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
