<?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>Forem: Manjunath Patil</title>
    <description>The latest articles on Forem by Manjunath Patil (@manjunathpatil).</description>
    <link>https://forem.com/manjunathpatil</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3787205%2F74b39a73-12d6-46a1-8203-2c34d853aa1c.jpg</url>
      <title>Forem: Manjunath Patil</title>
      <link>https://forem.com/manjunathpatil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/manjunathpatil"/>
    <language>en</language>
    <item>
      <title>Google Cloud Next '26 Made One Thing Clear: Agents Need Infrastructure, Not Hype</title>
      <dc:creator>Manjunath Patil</dc:creator>
      <pubDate>Thu, 30 Apr 2026 03:23:48 +0000</pubDate>
      <link>https://forem.com/manjunathpatil/google-cloud-next-26-made-one-thing-clear-agents-need-infrastructure-not-hype-3i7f</link>
      <guid>https://forem.com/manjunathpatil/google-cloud-next-26-made-one-thing-clear-agents-need-infrastructure-not-hype-3i7f</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-cloud-next-2026-04-22"&gt;Google Cloud NEXT Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Google Cloud Next '26 had a very loud headline: the agentic enterprise is here.&lt;/p&gt;

&lt;p&gt;But the more interesting story, at least for developers, was quieter.&lt;/p&gt;

&lt;p&gt;It was not just "agents are smarter now." It was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI agents are finally being treated like production software.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That may sound less exciting than a keynote demo, but I think it is the real shift. The serious part of the event was not that an agent can answer a question, generate a document, or call an API. We have seen enough demos like that.&lt;/p&gt;

&lt;p&gt;The serious part was Google saying, in many different ways, that agents now need infrastructure: runtime, identity, memory, observability, evaluation, access control, data grounding, secure sandboxes, and governance.&lt;/p&gt;

&lt;p&gt;In other words, the future of agents is not just about better models. It is about everything around the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The announcement that stood out
&lt;/h2&gt;

&lt;p&gt;The center of Google Cloud Next '26 was the new &lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/introducing-gemini-enterprise-agent-platform" rel="noopener noreferrer"&gt;Gemini Enterprise Agent Platform&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Google described it as the evolution of Vertex AI: a platform to build, scale, govern, and optimize agents. That framing matters because it moves the conversation away from "Can I build a cool AI demo?" and toward "Can I run thousands of agents safely inside a real organization?"&lt;/p&gt;

&lt;p&gt;Sundar Pichai's Next '26 post captured the shift well. He wrote that the question has moved from "Can we build an agent?" to "How do we manage thousands of them?" That one sentence explains most of the event.&lt;/p&gt;

&lt;p&gt;Google's answer is a stack with pieces like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent Development Kit, or ADK&lt;/li&gt;
&lt;li&gt;Agent Studio&lt;/li&gt;
&lt;li&gt;Agent Runtime&lt;/li&gt;
&lt;li&gt;Agent Registry&lt;/li&gt;
&lt;li&gt;Agent Identity&lt;/li&gt;
&lt;li&gt;Agent Gateway&lt;/li&gt;
&lt;li&gt;Agent Observability&lt;/li&gt;
&lt;li&gt;Agent Simulation&lt;/li&gt;
&lt;li&gt;Agent Evaluation&lt;/li&gt;
&lt;li&gt;Memory Bank&lt;/li&gt;
&lt;li&gt;Agent Sessions&lt;/li&gt;
&lt;li&gt;Model Armor&lt;/li&gt;
&lt;li&gt;Google Cloud MCP servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a lot of product names. But under the naming, there is a real architecture forming.&lt;/p&gt;

&lt;h2&gt;
  
  
  The developer keynote made it real
&lt;/h2&gt;

&lt;p&gt;The opening keynote gave the vision. The developer keynote made it practical.&lt;/p&gt;

&lt;p&gt;The demo was built around planning a marathon in Las Vegas. That sounds like a toy problem until you think about what it actually requires: route planning, constraints, simulation, safety, logistics, evaluation, and constant iteration.&lt;/p&gt;

&lt;p&gt;The system used multiple specialized agents instead of one giant "do everything" agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A planner agent proposed marathon routes.&lt;/li&gt;
&lt;li&gt;An evaluator agent checked those routes against requirements.&lt;/li&gt;
&lt;li&gt;A simulator agent modeled the impact on the city.&lt;/li&gt;
&lt;li&gt;A supply chain agent handled logistics like water stations, medical tents, and portable toilets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the architecture pattern I found most useful: not one magical assistant, but a network of smaller agents with clear jobs.&lt;/p&gt;

&lt;p&gt;The developer keynote showed agents using ADK, MCP, Agent Runtime, Agent Registry, A2A-style agent communication, A2UI-style user interfaces, Memory Bank, runtime traces, Cloud Assist, Cloud Run, GKE, Agent Identity, and Agent Gateway.&lt;/p&gt;

&lt;p&gt;That is much closer to real software engineering than the usual "I typed a prompt and magic happened" demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden message: agents need boring systems
&lt;/h2&gt;

&lt;p&gt;The most underrated part of Next '26 was how much of the agent story was boring in the best possible way.&lt;/p&gt;

&lt;p&gt;Boring like logs.&lt;/p&gt;

&lt;p&gt;Boring like identity.&lt;/p&gt;

&lt;p&gt;Boring like access control.&lt;/p&gt;

&lt;p&gt;Boring like knowing what changed, who changed it, what tool got called, what data was accessed, and why the agent made a bad decision.&lt;/p&gt;

&lt;p&gt;That is what makes software production-grade.&lt;/p&gt;

&lt;p&gt;A chatbot can be loose. A production agent cannot.&lt;/p&gt;

&lt;p&gt;If an agent can read documents, write code, trigger workflows, browse internal systems, call another agent, or act on customer data, then it needs the same discipline we expect from any other production system.&lt;/p&gt;

&lt;p&gt;That is why features like Agent Identity and Agent Gateway matter. Agent Identity gives agents a trackable identity. Agent Gateway becomes a control point for agent-to-agent and agent-to-tool traffic. Agent Registry gives organizations a way to know what agents and tools exist. Agent Observability gives developers traces and logs for debugging.&lt;/p&gt;

&lt;p&gt;This is not glamorous, but it is the difference between "cool demo" and "I would trust this in production."&lt;/p&gt;

&lt;h2&gt;
  
  
  What people seem excited about
&lt;/h2&gt;

&lt;p&gt;After reading the official announcements and community reactions, the excitement is mostly around practical developer workflows.&lt;/p&gt;

&lt;p&gt;ADK is a big one. Developers want a way to build multi-agent systems without inventing the architecture from scratch. Google's graph-based ADK direction is interesting because it acknowledges that agent workflows need structure. Some tasks can be generative, but some need deterministic paths, especially in compliance, security, finance, healthcare, and operations.&lt;/p&gt;

&lt;p&gt;MCP is another major theme. Google is exposing cloud services through Model Context Protocol and also announced an official &lt;a href="https://cloud.google.com/blog/topics/developers-practitioners/level-up-your-agents-announcing-googles-official-skills-repository" rel="noopener noreferrer"&gt;Agent Skills repository&lt;/a&gt;. I like this because it tackles a problem developers already feel: context bloat. Giving an agent the entire internet, all docs, and every internal page is not a strategy. Smaller, task-specific skills are a cleaner way to give agents expertise only when needed.&lt;/p&gt;

&lt;p&gt;Cloud Run updates also caught attention. Developers care about things like managed MCP servers, long-running background agents, sandboxing, SSH support, service bindings, serverless GPUs, and billing caps. Billing caps may not sound like a keynote-worthy feature, but for developers worried about surprise cloud bills, that can be more exciting than another model benchmark.&lt;/p&gt;

&lt;p&gt;There was also interest in the codelabs. Google said there were 55+ new codelabs across Cloud at Next, with labs covering ADK + A2UI, multi-agent systems, secure agents, Google Maps grounding, Agent Engine deployment, Cloud Run, and agent skills. That matters because developers do not just need a keynote. They need something they can run after the keynote ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  What people are worried about
&lt;/h2&gt;

&lt;p&gt;The concern I saw again and again is AI fatigue.&lt;/p&gt;

&lt;p&gt;Some developers are asking whether Google Cloud Next is becoming "Gemini Next." On Reddit, one person joked about trying to find a session without AI and failing. Another pre-event thread asked what people expected besides "Agentic AI spam."&lt;/p&gt;

&lt;p&gt;That frustration is fair.&lt;/p&gt;

&lt;p&gt;A lot of production teams still care about IAM, networking, Kubernetes, databases, cost controls, observability, migrations, and reliability. If every topic gets wrapped in "agentic AI" language, it can start to feel like the practical infrastructure concerns are being painted over with marketing.&lt;/p&gt;

&lt;p&gt;But I think the best version of Google's Next '26 story actually answers that criticism.&lt;/p&gt;

&lt;p&gt;The useful announcements were not "AI will fix everything." The useful announcements were about the infrastructure agents need when they stop being demos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GKE Agent Sandbox for isolated execution&lt;/li&gt;
&lt;li&gt;Agent Runtime for deployment&lt;/li&gt;
&lt;li&gt;Agent Gateway for governed traffic&lt;/li&gt;
&lt;li&gt;Agent Identity for traceable permissions&lt;/li&gt;
&lt;li&gt;Agent Observability for debugging&lt;/li&gt;
&lt;li&gt;Agent Evaluation for quality checks&lt;/li&gt;
&lt;li&gt;Model Armor for prompt injection and data leakage protection&lt;/li&gt;
&lt;li&gt;Knowledge Catalog for trusted business context&lt;/li&gt;
&lt;li&gt;Cloud Run billing caps for cost safety&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not just AI features. They are operational features.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data story may be more important than the model story
&lt;/h2&gt;

&lt;p&gt;Google also announced the &lt;a href="https://cloud.google.com/blog/products/data-analytics/whats-new-in-the-agentic-data-cloud" rel="noopener noreferrer"&gt;Agentic Data Cloud&lt;/a&gt;, and I think this might be one of the most important parts of the event.&lt;/p&gt;

&lt;p&gt;Agents are only useful if they understand the business context around the task.&lt;/p&gt;

&lt;p&gt;A generic model may understand the word "margin," but inside a company, "margin" might depend on team-specific definitions, regional rules, product lines, internal dashboards, and messy historical decisions. If an agent does not understand that context, it will confidently do the wrong thing.&lt;/p&gt;

&lt;p&gt;That is why Knowledge Catalog, Cross-Cloud Lakehouse, BigQuery measures, LookML Agent, Data Agent Kit, and Conversational Analytics matter. They are not just data products. They are attempts to make enterprise context usable by agents.&lt;/p&gt;

&lt;p&gt;This is where the agentic enterprise either becomes real or falls apart.&lt;/p&gt;

&lt;p&gt;Without trusted context, agents hallucinate.&lt;/p&gt;

&lt;p&gt;Without permissions, agents leak data.&lt;/p&gt;

&lt;p&gt;Without observability, agents become impossible to debug.&lt;/p&gt;

&lt;p&gt;Without evaluation, agents drift.&lt;/p&gt;

&lt;p&gt;Without cost controls, agents become expensive experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security is not optional anymore
&lt;/h2&gt;

&lt;p&gt;The security announcements also stood out. Google is combining Google Threat Intelligence, Security Operations, and Wiz into what it calls Agentic Defense.&lt;/p&gt;

&lt;p&gt;The important idea here is that agents create new attack surfaces.&lt;/p&gt;

&lt;p&gt;If agents can use tools, attackers will try to poison tools.&lt;/p&gt;

&lt;p&gt;If agents can read data, attackers will try to extract data.&lt;/p&gt;

&lt;p&gt;If agents can call other agents, attackers will try to exploit the chain.&lt;/p&gt;

&lt;p&gt;If agents can execute code, attackers will try to turn them into execution paths.&lt;/p&gt;

&lt;p&gt;This is why Agent Identity, Agent Gateway, Model Armor, Wiz, threat detection agents, detection engineering agents, and runtime security all belong in the same conversation as ADK.&lt;/p&gt;

&lt;p&gt;Security cannot be a final checklist after the agent is built. It has to be part of the agent architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  My honest take
&lt;/h2&gt;

&lt;p&gt;I am excited, but cautiously.&lt;/p&gt;

&lt;p&gt;I like that Google is treating agents as systems, not toys. The developer keynote was strongest when it showed the full lifecycle: build, test, remember, debug, deploy, scale, and secure.&lt;/p&gt;

&lt;p&gt;That is the right framing.&lt;/p&gt;

&lt;p&gt;But I also think the industry needs to be careful. "Agentic" is becoming a word that gets attached to everything. Not every workflow needs an autonomous agent. Some need a form. Some need a queue. Some need a cron job. Some need a dashboard. Some need better documentation.&lt;/p&gt;

&lt;p&gt;The best agent systems will not be the ones that automate the most. They will be the ones that know when to act, when to ask, when to stop, and when to hand control back to a human.&lt;/p&gt;

&lt;p&gt;That is why the boring parts matter so much.&lt;/p&gt;

&lt;p&gt;The future is not one giant AI agent running a company. The future is probably many small, specialized agents operating inside strict boundaries, with humans still setting intent, reviewing important decisions, and owning the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Google Cloud Next '26 made one thing clear to me:&lt;/p&gt;

&lt;p&gt;The agent era will not be won by the flashiest chatbot.&lt;/p&gt;

&lt;p&gt;It will be won by the platform that makes agents observable, governable, secure, grounded in real data, and boring enough to trust.&lt;/p&gt;

&lt;p&gt;That is a less dramatic story than "AI will do everything."&lt;/p&gt;

&lt;p&gt;But for developers, it is a much better one.&lt;/p&gt;

&lt;p&gt;Because if agents are going to become part of real software, they need to behave like real software.&lt;/p&gt;

&lt;p&gt;And real software needs infrastructure.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>cloudnextchallenge</category>
      <category>googlecloud</category>
      <category>ai</category>
    </item>
    <item>
      <title>Earth Day Study Companion: For Teach-Ins and Climate Learning</title>
      <dc:creator>Manjunath Patil</dc:creator>
      <pubDate>Mon, 20 Apr 2026 06:54:44 +0000</pubDate>
      <link>https://forem.com/manjunathpatil/earth-day-study-companion-turning-climate-research-into-teach-ins-learning-pathways-and-29jh</link>
      <guid>https://forem.com/manjunathpatil/earth-day-study-companion-turning-climate-research-into-teach-ins-learning-pathways-and-29jh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-04-16"&gt;Weekend Challenge: Earth Day Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;Earth Day Study Companion&lt;/strong&gt;, a climate learning workspace designed to help people move from reading, to understanding, to teaching, to action.&lt;/p&gt;

&lt;p&gt;The idea came from a simple problem. A lot of Earth Day and climate learning happens in fragments. Someone reads a long PDF report, watches a few videos, collects some slides, saves a few articles, and then tries to turn all of that into a class, a club session, a workshop, or a community discussion. The information exists, but the workflow is messy. Research is disconnected from teaching. Planning is disconnected from delivery. Good material is often trapped inside dense documents that are hard to search and even harder to teach from.&lt;/p&gt;

&lt;p&gt;I wanted to build a product that makes that process feel connected from start to finish.&lt;/p&gt;

&lt;p&gt;Earth Day Study Companion has three core experiences: &lt;strong&gt;Climate Library&lt;/strong&gt;, &lt;strong&gt;Teach-In Builder&lt;/strong&gt;, and &lt;strong&gt;Teach-In Facilitator&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Climate Library&lt;/strong&gt; is the research layer. Users can upload climate reports, sustainability guides, Earth Day toolkits, policy PDFs, and other learning material. The system stores the documents, processes them, chunks them into smaller pieces, creates embeddings, and uses retrieval before answering questions. That means the experience is not just a generic chatbot on top of a file upload. It is a grounded document assistant built on top of retrieval augmented generation. Users can ask questions about what is inside the document, open the relevant page, and jump back to the exact source material that informed the answer. This is especially useful for long climate documents where the user needs clarity, context, and source confidence.&lt;/p&gt;

&lt;p&gt;The library also becomes more useful because it is connected to live multimodal interaction. A user can turn on the microphone and ask questions naturally. They can also share the screen and ask if a chart, image, article, PDF section, or slide is worth using. This matters in real Earth Day preparation because people do not only work with text. They work with visual material, reports, and presentation content. The screen share helps Gemini reason over what is visible, so the user can ask practical questions like whether a page is relevant, whether a graph is clear enough, or whether a specific visual should be included in a session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teach-In Builder&lt;/strong&gt; is the structure layer. Once the user knows the topic they want to teach, the Builder turns that topic into a learning pathway. Instead of producing only a plain outline, the system creates both a pathway view and a mind map. The pathway helps with order and progression. The mind map helps with relationships and scope. This makes it easier to take a broad subject like renewable energy, climate justice, biodiversity, food systems, or circular economy and turn it into something teachable.&lt;/p&gt;

&lt;p&gt;Each generated module can then be expanded into &lt;strong&gt;Guide&lt;/strong&gt;, &lt;strong&gt;Practice Lab&lt;/strong&gt;, and &lt;strong&gt;Field Media&lt;/strong&gt;. The Guide is for explanation and learning flow. Practice Lab helps turn passive reading into active thinking. Field Media connects the topic to supporting examples and related material. I wanted the Builder to feel like a real educational workspace, not a one shot prompt output. The result is something that can help a student explore a topic, but it can also help a facilitator or organizer design a real Earth Day learning session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teach-In Facilitator&lt;/strong&gt; is the planning layer. This is where the project moves from learning support into real event preparation. The user starts by filling in a session brief with the audience, venue or context, title, duration, goals, focus areas, and available materials. From there, Gemini acts like a live planning partner. The user can talk through the idea naturally, refine the session structure, and decide how the event should feel for that specific audience.&lt;/p&gt;

&lt;p&gt;This is also where the live controls become very important. The microphone supports a natural planning conversation. Screen share allows the facilitator to show slides, PDFs, webpages, images, and other planning material while asking Gemini for feedback. The webcam adds live visual context during the planning session. In practice, this makes the product feel much closer to a real coaching partner than a standard prompt box. A user can ask if a resource looks relevant, if a visual seems clear, or if a piece of material fits the tone of the session they are planning.&lt;/p&gt;

&lt;p&gt;As the session develops, the app turns that planning process into a structured output with a summary, learning objectives, agenda, materials, and community actions. At the end, the system generates a facilitator report as a downloadable PDF. That final step is important because it turns a live planning session into something reusable. The user leaves with a practical artifact they can actually use for a school club, a local workshop, a library event, or a community Earth Day gathering.&lt;/p&gt;

&lt;p&gt;My intended goal with this project was to build something that feels genuinely useful for Earth Day. Not just a climate themed UI, not just an AI wrapper, and not just a static educational demo. I wanted to build a tool that helps people understand climate material, turn it into a teaching structure, and prepare a real session around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Live demo: &lt;a href="https://earthdaycompanion.vercel.app/" rel="noopener noreferrer"&gt;https://earthdaycompanion.vercel.app/&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Video demo: &lt;a href="https://youtu.be/V-v_MHj3HJw" rel="noopener noreferrer"&gt;https://youtu.be/V-v_MHj3HJw&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the demo, I walk through the product as a full learning and planning flow.&lt;/p&gt;

&lt;p&gt;I start inside Climate Library by uploading a climate related PDF and asking questions about it. This shows how the system indexes the document, retrieves relevant chunks, and answers in a way that stays tied to the uploaded material. I also show page level navigation and jumping back to the relevant part of the PDF, because that is one of the most important parts of the library experience. I wanted viewers to see that the system is not guessing. It is actually working with the document.&lt;/p&gt;

&lt;p&gt;Then I move into Teach-In Builder and generate a structured pathway on an Earth Day topic. I show the pathway view and the mind map view, then open a module to show how the Guide, Practice Lab, and Field Media sections work. This part demonstrates how the product turns a broad environmental topic into a teachable sequence instead of only summarizing it.&lt;/p&gt;

&lt;p&gt;Finally, I open Teach-In Facilitator and show how the app can be used as a live planning partner for a real Earth Day session. I walk through the brief, start the live flow, and show how Gemini helps shape the structure of the teach-in. I also show how screen sharing can be used to review materials visually during the planning process. At the end, I generate the facilitator report PDF to show how the live planning flow becomes something concrete and reusable.&lt;/p&gt;
&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;p&gt;This is the landing experience, where the product introduces the three connected flows: Climate Library, Teach-In Builder, and Teach-In Facilitator.&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%2F4eexbq4t3qexh4xwmltb.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%2F4eexbq4t3qexh4xwmltb.png" alt="HOMEPAGE" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the Climate Library, where uploaded PDFs become grounded, searchable learning material.&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%2Flfddswuyturinmo12m2b.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%2Flfddswuyturinmo12m2b.png" alt="CLIMATE LIBRARY" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the Teach-In Builder, where a broad Earth Day topic becomes a pathway and a mind map.&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%2Fyuujqm81n2mdc6yjlgqo.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%2Fyuujqm81n2mdc6yjlgqo.png" alt="TEACH IN BUILDER" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the Teach-In Facilitator, where Gemini helps shape a real session and generate a usable report.&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%2Fi8o5fyiwty89kewrw7l4.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%2Fi8o5fyiwty89kewrw7l4.png" alt="TEACH IN FACILITATOR" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;GitHub repository: &lt;a href="https://github.com/ladiesmans217/Earth-Day-Challenge" rel="noopener noreferrer"&gt;https://github.com/ladiesmans217/Earth-Day-Challenge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is built with a React and TypeScript frontend and a Python backend. The frontend handles the user experience across the three main flows, while the backend handles document processing, retrieval, generation, and report output.&lt;/p&gt;

&lt;p&gt;On the document side, the backend stores uploaded PDFs, chunks the content, creates embeddings, and uses ChromaDB for retrieval. On the live interaction side, Gemini powers the voice based multimodal experience. On the planning side, Gemini function calling is used to create a structured teach-in plan. On the output side, the backend generates a facilitator report PDF so the planning session ends with a practical result.&lt;/p&gt;
&lt;h3&gt;
  
  
  Code Snippets
&lt;/h3&gt;

&lt;p&gt;This snippet shows how PDF pages are chunked and indexed into ChromaDB for grounded retrieval.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ingest_pdf&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;pdf_path&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PDF not found: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;pages&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="nf"&gt;extract_text_from_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;pages&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to extract text from PDF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;added_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;chunks&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="nf"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;page_num&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chunk_idx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&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;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;metadatas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page_num&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;page_num&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_idx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chunk_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingested_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&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="n"&gt;added_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks_added&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;added_count&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This snippet shows part of the Climate Library viewer logic, including the local PDF worker and the in-memory PDF cache used to keep the viewer responsive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pdfjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GlobalWorkerOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workerSrc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&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;PUBLIC_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/pdf.worker.min.js`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CachedPdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;objectUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pageTextByPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pdfDocument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;pdfCacheRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CachedPdf&lt;/span&gt;&lt;span class="o"&gt;&amp;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;Map&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;getPdfCacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&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;return&lt;/span&gt; &lt;span class="nf"&gt;normalizePdfName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;p&gt;This snippet shows the default Teach-In Facilitator brief and how the experience starts with a strong session setup instead of a blank form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_TEACH_IN_BRIEF&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TeachInBrief&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sessionTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Neighborhood Clean Energy Teach-In&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;High-school students, parents, and community volunteers curious about practical local climate action&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;venueContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Public library community room with projector and open discussion seating&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;45 minutes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Help attendees understand clean energy in everyday life, connect it to climate justice, and leave with two realistic actions they can take this month.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;focusAreas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Renewable energy basics, energy bills, climate justice, neighborhood resilience, community action&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;materials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Projector, whiteboard, local utility bill example, sticky notes, Earth Day handout, signup sheet&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I built the product around a three step model: study, structure, and facilitate.&lt;/p&gt;

&lt;p&gt;For the study layer, the main focus was grounding. Climate material is often long, technical, and dense, so I did not want a system that simply accepted a PDF and then answered in a vague way. When a user uploads a document into Climate Library, the backend stores the file, extracts the text, splits it into chunks, creates embeddings, and stores them in ChromaDB. When the user asks a question, the backend retrieves the most relevant chunks and passes them into the model as context. That creates a proper retrieval augmented generation flow instead of a general chat flow. It also allows the app to support citations, page navigation, and highlighted source jumps back into the document.&lt;/p&gt;

&lt;p&gt;That source connection was important to me because climate literacy is not only about getting an answer. It is about trusting where the answer came from. If someone is preparing an Earth Day session, a school lesson, or a community discussion, they need to be able to go back to the original material and verify what they are using.&lt;/p&gt;

&lt;p&gt;For the structure layer, I wanted to go beyond a single generated course outline. That is why Teach-In Builder creates both a pathway and a mind map. Those two views do slightly different jobs. The pathway helps the user think in sequence, while the mind map helps the user think in connections. Once a module is opened, the system expands it into Guide, Practice Lab, and Field Media so the topic becomes something a person can actually work through and teach from. This part of the build was about making generated content feel usable, not just impressive.&lt;/p&gt;

&lt;p&gt;For the facilitate layer, I adapted the live assistant flow into a planning companion for real Earth Day events. The user starts with a structured brief, then moves into a live Gemini session where the focus is on audience fit, session flow, materials, and next steps. Function calling is used to turn that planning flow into a structured output with learning objectives, agenda, materials, and community actions. I wanted this to feel like an event planning tool, not a generic live chat demo.&lt;/p&gt;

&lt;p&gt;Gemini is the key technology across the whole project. I used it for live voice interaction, multimodal context, structured teach-in planning, and document assistance when paired with retrieval. In Climate Library, Gemini helps turn indexed PDFs into an interactive learning experience. In Teach-In Builder, it supports turning broad topics into structured educational pathways. In Teach-In Facilitator, it helps shape a real Earth Day session that can actually be delivered to an audience.&lt;/p&gt;

&lt;p&gt;The shared live control tray also became a meaningful part of the product. The microphone supports a more natural planning and exploration flow. Screen share makes the assistant useful for real materials, not just typed prompts. Webcam adds live visual context to the session. Together, those controls make the app feel more like a working multimodal study and facilitation environment.&lt;/p&gt;

&lt;p&gt;I also spent time on the interface direction because I did not want the project to feel like a generic AI dashboard. I moved away from a loud or overly synthetic look and shaped it into something more like an editorial field guide for climate learning. The goal was to make the experience feel grounded, readable, and specific to Earth Day rather than looking like a general purpose AI tool with green branding.&lt;/p&gt;

&lt;p&gt;The biggest thing I learned while building this was that climate education is a strong and practical Earth Day direction. Many projects in this space focus only on tracking or visualization. Those are useful, but I wanted to build around the human part of environmental action: understanding the material, organizing it, and helping other people learn from it. That is where I think this project is strongest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;I am submitting this project for Best Use of Google Gemini.&lt;/p&gt;

&lt;p&gt;Gemini is central to the project, not an extra layer added on top. It powers the live voice interaction, the multimodal reasoning over shared visual context, the structured teach-in planning flow, and the grounded assistance in the document workflow when combined with retrieval.&lt;/p&gt;

&lt;p&gt;This is a solo submission.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>earthday</category>
      <category>gemini</category>
    </item>
    <item>
      <title>TerraRun — A Territory Capture Running App Where Every Loop You Run Becomes Your Turf</title>
      <dc:creator>Manjunath Patil</dc:creator>
      <pubDate>Mon, 02 Mar 2026 07:58:17 +0000</pubDate>
      <link>https://forem.com/manjunathpatil/terrarun-a-territory-capture-running-app-where-every-loop-you-run-becomes-your-turf-ie</link>
      <guid>https://forem.com/manjunathpatil/terrarun-a-territory-capture-running-app-where-every-loop-you-run-becomes-your-turf-ie</guid>
      <description>&lt;p&gt;I'm part of the running community — people who lace up every morning, track every mile, and obsess over their Strava stats. Running apps today are incredible at what they do. Strava tracks your routes. Nike Run Club coaches your training. MapMyRun logs your history.&lt;/p&gt;

&lt;p&gt;But here's the gap I kept noticing: &lt;strong&gt;once you finish a run, what do you actually own?&lt;/strong&gt; A line on a map and some numbers. There's no lasting mark on the world. No reason to go back to the same streets, no incentive to explore new ones, and no connection between your physical effort and the neighborhoods you run through every day.&lt;/p&gt;

&lt;p&gt;Runners already feel ownership over their routes — "that's MY morning loop" — but nothing on the map reflects that. I wanted to change that.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TerraRun&lt;/strong&gt; — a territory capture running app where every closed loop you run becomes your turf on the map.&lt;/p&gt;

&lt;p&gt;The concept: go for a run, close a loop (your route connects back to where you started), and the enclosed area fills in with your color on a shared public map. That's your territory now. Other runners can reclaim it by running the same loop. The more territory you hold, the higher you rank.&lt;/p&gt;

&lt;p&gt;Think of it as &lt;strong&gt;Strava meets king-of-the-hill&lt;/strong&gt; — your runs aren't just logged, they're &lt;em&gt;staked&lt;/em&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Territory Capture&lt;/strong&gt; — Run a closed loop, and the exact shape of your route becomes claimed territory on the map. Not a grid, not circles — the actual polygon of your run, just like Strava route art.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Run Simulation&lt;/strong&gt; — A "Watch Demo Run" button animates a runner tracing an irregular street-level route in real-time. When the loop closes, the territory fills in with a capture animation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share Card&lt;/strong&gt; — After capturing territory, a share modal pops up with your route shape rendered as a graphic, plus your stats (distance, time, pace, area). One tap to share on WhatsApp, Telegram, X, or copy the link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive Map&lt;/strong&gt; — Full-screen Mapbox map with 4 styles (Dark, Streets, Satellite, Outdoors), 3D toggle, territory click popups showing owner and capture date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaderboard&lt;/strong&gt; — Runners ranked by territory held. Top 3 podium. Filters for All Time, This Week, Nearby.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile Dashboard&lt;/strong&gt; — Your stats, achievement badges, active streak, and activity feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sponsor Zones&lt;/strong&gt; — Brands (Nike, Adidas, Gatorade, Under Armour) place zones on the map. Run through them to unlock real coupon codes. Runners get rewarded. Brands get literal foot traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What makes this different from existing apps:&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;&lt;/th&gt;
&lt;th&gt;Strava&lt;/th&gt;
&lt;th&gt;Nike Run Club&lt;/th&gt;
&lt;th&gt;TerraRun&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tracks runs&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social competition&lt;/td&gt;
&lt;td&gt;Segments only&lt;/td&gt;
&lt;td&gt;Challenges&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Territory wars&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map ownership&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Your runs claim real map area&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reason to re-run a route&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Defend your territory&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brand engagement&lt;/td&gt;
&lt;td&gt;Ads&lt;/td&gt;
&lt;td&gt;Ads&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Physical sponsor zones you run to&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://youtu.be/Cr7n3qfzARM" rel="noopener noreferrer"&gt;YOUTUBE VIDEO LINK&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/Cr7n3qfzARM"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live deployment:&lt;/strong&gt; &lt;a href="https://terrarun-dev.vercel.app/" rel="noopener noreferrer"&gt;TerraRun on Vercel&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/manjunath5513" rel="noopener noreferrer"&gt;
        manjunath5513
      &lt;/a&gt; / &lt;a href="https://github.com/manjunath5513/Dev-Challenge" rel="noopener noreferrer"&gt;
        Dev-Challenge
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;TerraRun — Run. Loop. Conquer.&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A territory capture running app where every closed loop you run becomes your turf on the map. Built for the &lt;a href="https://dev.to/challenges/weekend-2026-02-28" rel="nofollow"&gt;DEV Weekend Challenge&lt;/a&gt; (Feb 27 – Mar 2, 2026).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://terrarun-dev.vercel.app/" rel="nofollow noopener noreferrer"&gt;terrarun-dev.vercel.app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Video Demo:&lt;/strong&gt; &lt;a href="https://youtu.be/Cr7n3qfzARM" rel="nofollow noopener noreferrer"&gt;youtu.be/Cr7n3qfzARM&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Idea&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Running apps like Strava and Nike Run Club are great at tracking — but once a run is done, all you have is a line on a map. TerraRun changes that. Run a closed loop, and the enclosed area fills in as your territory. Others can reclaim it by running the same loop. Every neighborhood becomes a game board.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Territory Capture&lt;/strong&gt; — Closed-loop runs become filled polygon territories on the map, matching the exact shape of your route&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Run Simulation&lt;/strong&gt; — Animated demo run traces an irregular street-level path, captures territory on loop close&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share Card&lt;/strong&gt; — Post-capture modal with route shape graphic, stats (distance, time…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/manjunath5513/Dev-Challenge" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;&lt;strong&gt;Project structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terrarun/
├── src/
│   ├── app/                    # Next.js App Router pages
│   │   ├── page.tsx            # Landing page
│   │   ├── map/page.tsx        # Main interactive map
│   │   ├── leaderboard/page.tsx
│   │   ├── profile/page.tsx
│   │   └── sponsors/page.tsx
│   ├── components/
│   │   ├── map/                # MapView, TerritoryLayer, RunSimulation, ShareModal
│   │   ├── landing/            # Hero, Features, HowItWorks
│   │   ├── layout/             # Navbar with mobile bottom nav
│   │   ├── leaderboard/        # LeaderboardTable
│   │   └── profile/            # ProfileStats
│   ├── lib/
│   │   ├── mock-data.ts        # 6 users, 7 territories, 4 sponsors
│   │   ├── territory.ts        # Polygon territory generation with Turf.js
│   │   ├── h3-utils.ts         # H3 hex utilities (available for future use)
│   │   └── constants.ts        # Map config, styles, colors
│   ├── store/
│   │   └── useMapStore.ts      # Zustand state management
│   └── types/
│       └── index.ts            # TypeScript interfaces
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Next.js 16&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Framework — App Router, SSR, file-based routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;React 19&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;UI rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Type safety across the entire codebase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tailwind CSS 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Styling — dark theme, glass morphism effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mapbox GL JS 3 + react-map-gl 8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Interactive map rendering with 4 style modes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Turf.js 7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Geospatial calculations — polygon area from GPS coordinates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Framer Motion 12&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Animations — page transitions, capture celebrations, share modal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zustand 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lightweight state management for map and simulation state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lucide React&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Icon system&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How the territory system works
&lt;/h3&gt;

&lt;p&gt;The core mechanic: a runner's GPS route forms a closed polygon. When the loop closes, I use &lt;strong&gt;Turf.js&lt;/strong&gt; to calculate the enclosed area in km², and render it as a filled GeoJSON polygon on the Mapbox map. The territory is the &lt;em&gt;exact shape&lt;/em&gt; of the run — no grids, no approximations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Convert route points to a territory polygon&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routePoints&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;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lat&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;poly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;turfPolygon&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;ring&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;areaKm2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;turfArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each territory stores: polygon coordinates, owner, color, area, capture date. Territories are rendered as GeoJSON with a 3-layer glow border effect (outer blur + mid blur + core line) for that neon territory look.&lt;/p&gt;

&lt;h3&gt;
  
  
  The run simulation
&lt;/h3&gt;

&lt;p&gt;The demo run isn't a perfect circle — it's an &lt;strong&gt;irregular street-level route&lt;/strong&gt; that zigzags through Central Park paths, because that's how real runs look. The animation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runner marker moves along the route at running pace&lt;/li&gt;
&lt;li&gt;A trail polyline draws behind in real-time&lt;/li&gt;
&lt;li&gt;When the loop closes → territory fill animation + capture stats&lt;/li&gt;
&lt;li&gt;Share card appears with the route shape, stats, and social share buttons&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Design decisions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polygons over hex grids&lt;/strong&gt; — I initially built with H3 hexagons but realized runners follow streets, not grids. Polygon fills matching the actual run shape feel natural and look like the Strava route screenshots people already share.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark theme default&lt;/strong&gt; — Territory colors glow best on dark backgrounds. The glass morphism UI keeps it clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile-first&lt;/strong&gt; — Bottom navigation bar, touch-friendly controls, responsive everything. Runners use phones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share-first capture flow&lt;/strong&gt; — The share modal appears immediately after capture because the screenshot moment is what drives virality in running apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Built in a weekend
&lt;/h3&gt;

&lt;p&gt;This was built from scratch during the DEV Weekend Challenge window. The entire app — 5 pages, interactive map, run simulation, territory system, share flow — was designed, coded, and deployed within the challenge timeframe.&lt;/p&gt;

&lt;p&gt;AI was used as a development tool throughout the process.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Run. Loop. Conquer.&lt;/em&gt; 🏃&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built a GeoGuessr for Languages — Here's How I Made It Speak 8 Languages Overnight</title>
      <dc:creator>Manjunath Patil</dc:creator>
      <pubDate>Mon, 23 Feb 2026 18:29:06 +0000</pubDate>
      <link>https://forem.com/manjunathpatil/i-built-a-geoguessr-for-languages-heres-how-i-made-it-speak-8-languages-overnight-4h1d</link>
      <guid>https://forem.com/manjunathpatil/i-built-a-geoguessr-for-languages-heres-how-i-made-it-speak-8-languages-overnight-4h1d</guid>
      <description>&lt;p&gt;I've sunk embarrassing hours into GeoGuessr. There's something deeply satisfying about squinting at a road sign in Cyrillic, spotting a right-hand-drive car, and triumphantly dropping a pin somewhere in rural Bulgaria. One evening, while half-listening to a Turkish podcast I didn't understand, it clicked — &lt;em&gt;what if the clue wasn't a photo of a street, but the sound of a language?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Think about it. You hear someone speaking. The rhythm, the vowels, the melody of the sentence. Can you tell Japanese from Korean? Portuguese from Spanish? Hindi from Urdu? That question became &lt;strong&gt;LinguaGuessr&lt;/strong&gt; — a game where you listen to a language, pin its origin on a world map, and get scored by how close you land.&lt;/p&gt;

&lt;p&gt;This is the story of how I built it, the tech decisions that shaped it, and how I made the entire UI speak 8 languages overnight — using &lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;Lingo.dev&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: language learning is boring, and i18n is painful
&lt;/h2&gt;

&lt;p&gt;Let's be honest — most language learning apps are glorified flashcard decks. Duolingo gamified vocabulary drills, but the core loop is still &lt;em&gt;memorize → recall → repeat&lt;/em&gt;. GeoGuessr proved that geography can become a game people play for fun, not obligation. Why hasn't anyone done that for linguistics?&lt;/p&gt;

&lt;p&gt;I wanted to build something where you &lt;em&gt;experience&lt;/em&gt; languages rather than study them. Hear a clip, feel the rhythm, take a guess, learn a fun fact. No textbooks, no streaks, no guilt.&lt;/p&gt;

&lt;p&gt;But there was a second problem lurking underneath: if you're building a game &lt;em&gt;about&lt;/em&gt; languages for a global audience, the UI itself needs to speak the player's language. And anyone who's shipped i18n knows the pain — JSON key files, missing translations, string interpolation bugs, a whole parallel codebase just for text. Building a game is hard enough. Making it multilingual felt like signing up for two projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is LinguaGuessr?
&lt;/h2&gt;

&lt;p&gt;The game loop is dead simple — three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Listen&lt;/strong&gt; — Hear a clip of someone speaking a mystery language&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin&lt;/strong&gt; — Click anywhere on the world map to place your guess&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt; — The closer your pin to the language's true origin, the more points you earn (max 5,000 per round)&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.amazonaws.com%2Fuploads%2Farticles%2Fvzokgf35ngtzakn4gpgn.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%2Fvzokgf35ngtzakn4gpgn.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are &lt;strong&gt;125+ languages&lt;/strong&gt; in the database — from the obvious (English, Spanish, Mandarin) to the obscure (Basque, Yoruba, Guarani, Corsican). Each language comes with geographic coordinates, a difficulty rating, and a fun cultural fact that shows up after you guess.&lt;/p&gt;

&lt;p&gt;The game supports &lt;strong&gt;solo mode&lt;/strong&gt; with a global leaderboard, and &lt;strong&gt;multiplayer mode&lt;/strong&gt; where you create a room, share a code, and compete in real time. Five rounds per game, max 25,000 points, and bragging rights for whoever knows their Amharic from their Tigrinya.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;Here's what powers LinguaGuessr under the hood:&lt;/p&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;Technology&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;&lt;strong&gt;Framework&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Next.js 16 (App Router)&lt;/td&gt;
&lt;td&gt;Server components, API routes, fast builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Styling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tailwind CSS v4&lt;/td&gt;
&lt;td&gt;Utility-first, dark theme, zero CSS files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Map&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Leaflet + react-leaflet&lt;/td&gt;
&lt;td&gt;Open-source, lightweight, great mobile support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Realtime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supabase (Postgres + Realtime)&lt;/td&gt;
&lt;td&gt;Room codes, presence, broadcast — all in one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;i18n&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lingo.dev (Compiler + SDK + CLI + CI/CD)&lt;/td&gt;
&lt;td&gt;Build-time JSX translation, runtime dynamic translation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Audio&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Web Speech API&lt;/td&gt;
&lt;td&gt;Zero-cost TTS with native browser voices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;Type safety across the entire stack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every choice was deliberate — I wanted a stack that could ship fast, scale to multiplayer, and handle i18n without a separate translation infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The game mechanics — under the hood
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Audio: a three-tier fallback
&lt;/h3&gt;

&lt;p&gt;Audio is the core mechanic. If the player can't hear the language, there's no game. So I built a three-tier fallback system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;MP3 files&lt;/strong&gt; — Pre-recorded clips for supported languages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Speech API&lt;/strong&gt; — Browser-native text-to-speech as a fallback (free, zero-cost, surprisingly good)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text display&lt;/strong&gt; — If both fail, show the phrase on screen and let the player guess from the script
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Try MP3 first, fall back to Web Speech API&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;audioUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;audioRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;Audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;audioRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&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="nf"&gt;setUseWebSpeech&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="nf"&gt;playWithWebSpeech&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;audioRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setUseWebSpeech&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="nf"&gt;playWithWebSpeech&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;playWithWebSpeech&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 Web Speech API is underrated. Every modern browser ships with dozens of language voices. It's not perfect — some voices sound robotic, and coverage varies by OS — but for a game where you just need to &lt;em&gt;hear&lt;/em&gt; the language, it's more than enough. And the price is right: free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Map: Leaflet with custom pins
&lt;/h3&gt;

&lt;p&gt;The map uses Leaflet with OpenStreetMap tiles. When a player clicks, a gradient pin drops at their guess location. After scoring, a dashed line draws from their guess to the correct location, giving immediate visual feedback on how close (or far off) they were.&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%2Fo0boyxbelnxpnd4nl6x2.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%2Fo0boyxbelnxpnd4nl6x2.png" alt=" " width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Scoring: Haversine formula + exponential decay
&lt;/h3&gt;

&lt;p&gt;The scoring uses the Haversine formula to calculate the great-circle distance between the player's guess and the language's true origin. Then an exponential decay curve converts distance to points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;haversineDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lng1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;lat2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lng2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6371&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Earth's radius in km&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dLat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lat1&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;dLng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lng2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lng1&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dLat&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dLng&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distanceKm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;distanceKm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Perfect score&lt;/span&gt;
  &lt;span class="c1"&gt;// Exponential decay: forgiving but steep&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;distanceKm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3000&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 curve is deliberately forgiving — you don't need to nail the exact country. Within 200km is a perfect 5,000. At 2,000km you still get ~2,500. But by 10,000km you're down to ~50 points. It rewards knowledge without punishing reasonable guesses.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multiplayer with Supabase Realtime
&lt;/h2&gt;

&lt;p&gt;I wanted multiplayer from day one. The idea of friends arguing about whether that clip was Finnish or Estonian is too good to skip.&lt;/p&gt;

&lt;p&gt;Supabase Realtime made this surprisingly simple. The entire multiplayer system runs on &lt;strong&gt;presence tracking&lt;/strong&gt; plus &lt;strong&gt;four broadcast events&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;Event&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;presence:sync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Track who's in the room&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;game_start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Host starts the game, sends the language list to all players&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;guess_submitted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A player submits their guess and score&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;next_round&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Advance everyone to the next round&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;game_finished&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show final rankings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the core channel setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`room:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;roomCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;presence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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="nx"&gt;channel&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;presence&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="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync&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="o"&gt;=&amp;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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;presenceState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Update player list from presence state&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broadcast&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="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;game_start&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="nx"&gt;payload&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="nf"&gt;setLanguages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playing&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broadcast&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="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;guess_submitted&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="nx"&gt;payload&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="c1"&gt;// Update scoreboard with player's round score&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broadcast&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="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next_round&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="nx"&gt;payload&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="nf"&gt;setCurrentRound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playing&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broadcast&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="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;game_finished&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finished&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fjwp8x6edfxhmfnumah5w.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%2Fjwp8x6edfxhmfnumah5w.png" alt=" " width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The entire multiplayer flow — lobby, gameplay sync, scoreboard — is handled by these events. No custom WebSocket server, no socket.io, no polling. And if Supabase is unavailable? The game gracefully degrades to solo mode with in-memory scores.&lt;/p&gt;




&lt;h2&gt;
  
  
  Making it multilingual with Lingo.dev
&lt;/h2&gt;

&lt;p&gt;Here's where it gets fun. I'm building a game &lt;em&gt;about&lt;/em&gt; languages. The irony of shipping it in English-only was not lost on me. But I also knew from past projects that i18n is a time sink — extracting strings into JSON files, maintaining translation keys, wiring up a provider, hoping nothing breaks when a new string shows up.&lt;/p&gt;

&lt;p&gt;Then I found &lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;Lingo.dev&lt;/a&gt;, and it changed my whole approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Compiler: auto-translate JSX at build time
&lt;/h3&gt;

&lt;p&gt;The Lingo.dev Compiler wraps your Next.js build and automatically translates all JSX text content. No string extraction. No JSON key files for your UI text. You write your components in English, and the compiler handles the rest.&lt;/p&gt;

&lt;p&gt;The setup is minimal — just wrap your Next.js config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withLingo&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/compiler/next&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;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;unoptimized&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NextConfig&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;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;withLingo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextConfig&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="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;targetLocales&lt;/span&gt;&lt;span class="p"&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;es&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;fr&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;de&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;ja&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;hi&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;ar&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;pt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lingo.dev&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Seven target languages. Every &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; in my React components now gets translated at build time into Spanish, French, German, Japanese, Hindi, Arabic, and Portuguese. No &lt;code&gt;t("key")&lt;/code&gt; calls. No &lt;code&gt;intl.formatMessage&lt;/code&gt;. Just write English and ship globally.&lt;/p&gt;

&lt;h3&gt;
  
  
  The SDK: runtime translation for dynamic content
&lt;/h3&gt;

&lt;p&gt;Static UI text is only half the story. LinguaGuessr has dynamic content — fun facts about each language that come from the database. These can't be translated at build time because they're loaded at runtime. The Lingo.dev SDK handles this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;engine&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;LINGODODEV_API_KEY&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;translated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&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;funFact&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="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;targetLocale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userLocale&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;So when a Japanese-speaking player finishes a round, the fun fact about Basque being a language isolate shows up in Japanese. The static UI was already in Japanese from the compiler. The dynamic content gets translated on-the-fly by the SDK. The player sees a fully localized experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI/CD: auto-translate on every push
&lt;/h3&gt;

&lt;p&gt;For static locale files (language names, country names, error messages), I use the Lingo.dev CLI paired with a GitHub Action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/translate.yml&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/locales/en.json'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;i18n.json'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;translate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Lingo.dev CLI&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx lingo.dev@latest i18n&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;LINGODODEV_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LINGODODEV_API_KEY }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Commit translations&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git add src/locales/&lt;/span&gt;
          &lt;span class="s"&gt;git diff --staged --quiet || git commit -m "chore: update translations"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time I update the English source strings and push to main, the Action translates everything and commits the results. No manual translation step, no stale translations, no forgotten locales.&lt;/p&gt;

&lt;h3&gt;
  
  
  The language switcher UX
&lt;/h3&gt;

&lt;p&gt;On the frontend, switching languages is instant. The &lt;code&gt;useLingoContext&lt;/code&gt; hook from Lingo's React integration provides &lt;code&gt;locale&lt;/code&gt; and &lt;code&gt;setLocale&lt;/code&gt;. A dropdown in the navbar lets you pick any of the 8 languages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLingoContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// When user picks a language&lt;/span&gt;
&lt;span class="nf"&gt;setLocale&lt;/span&gt;&lt;span class="p"&gt;(&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;// Switches entire UI to Japanese&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also built a custom toast notification that shows a brief "Translating to Japanese..." message with a spinner when switching — it gives the player feedback that something is happening, even though the switch is nearly instant.&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%2Feyt7pv5ryry9f1p2yove.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%2Feyt7pv5ryry9f1p2yove.png" alt=" " width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Bugs I hit (and how I fixed them)
&lt;/h2&gt;

&lt;p&gt;No project is complete without war stories. Here are the ones that cost me the most time.&lt;/p&gt;

&lt;h3&gt;
  
  
  SVG icons turning into gibberish
&lt;/h3&gt;

&lt;p&gt;After enabling the Lingo.dev Compiler, I noticed my SVG icons were broken. The globe icon in the navbar was rendering as literal text: &lt;em&gt;"SVG zero, polygon zero..."&lt;/em&gt; The compiler was treating SVG attributes like &lt;code&gt;viewBox&lt;/code&gt; and &lt;code&gt;strokeLinecap&lt;/code&gt; as translatable text and mangling them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Lingo.dev provides a &lt;code&gt;data-lingo-skip&lt;/code&gt; attribute. Slap it on any element you don't want translated. I went through every SVG in the codebase and added it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="na"&gt;data-lingo-skip&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;viewBox&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;
  &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;strokeWidth&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;circle&lt;/span&gt; &lt;span class="na"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"12"&lt;/span&gt; &lt;span class="na"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"12"&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"M2 12h20"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This became a pattern — every decorative SVG, every icon, every non-text element gets &lt;code&gt;data-lingo-skip&lt;/code&gt;. It's a small thing, but missing even one SVG can break a whole page.&lt;/p&gt;

&lt;h3&gt;
  
  
  No loading feedback on language switch
&lt;/h3&gt;

&lt;p&gt;The first time someone switched languages, nothing visually happened for a beat. The UI just... changed. Users thought it was broken. I built the &lt;code&gt;TranslationToast&lt;/code&gt; component — a small notification that slides in from the bottom-right with a spinner and auto-dismisses after 3 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;fixed&lt;/span&gt; &lt;span class="na"&gt;bottom-6&lt;/span&gt; &lt;span class="na"&gt;right-6&lt;/span&gt; &lt;span class="na"&gt;z-&lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="na"&gt;100&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt; &lt;span class="na"&gt;items-center&lt;/span&gt; &lt;span class="na"&gt;gap-3&lt;/span&gt;
  &lt;span class="na"&gt;rounded-xl&lt;/span&gt; &lt;span class="na"&gt;border&lt;/span&gt; &lt;span class="na"&gt;border-border&lt;/span&gt; &lt;span class="na"&gt;bg-surface&lt;/span&gt; &lt;span class="na"&gt;px-4&lt;/span&gt; &lt;span class="na"&gt;py-3&lt;/span&gt; &lt;span class="na"&gt;shadow-2xl&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="na"&gt;data-lingo-skip&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"h-4 w-4 animate-spin text-accent"&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Translating to &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;languageName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small touch, big UX difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dev widget stuck on screen
&lt;/h3&gt;

&lt;p&gt;Lingo.dev ships a developer widget that overlays your app in development — useful for debugging translations, but it kept showing up in production screenshots. The fix was a one-liner in the provider config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;devWidget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;enabled&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The lesson
&lt;/h3&gt;

&lt;p&gt;Compiler-based i18n tools are powerful. They eliminate the drudgery of string extraction and key management. But you need to tell them what &lt;em&gt;not&lt;/em&gt; to translate. SVGs, code blocks, brand names, technical terms — anything that shouldn't be localized needs an explicit skip marker. Once I internalized that pattern, the rest was smooth.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Compiler-based i18n is a different paradigm.&lt;/strong&gt; Traditional i18n (react-intl, next-intl, i18next) is key-based: extract every string, assign a key, look it up at runtime. Lingo.dev's compiler approach inverts this — you write natural JSX, and translation happens at the build layer. It's faster to set up, easier to maintain, and eliminates an entire category of "forgot to extract this string" bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supabase Realtime is underrated for quick multiplayer.&lt;/strong&gt; I expected to need a dedicated WebSocket server. Instead, four broadcast events and presence tracking gave me a complete multiplayer system. The channel API is clean, the latency is low, and the free tier is generous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web Speech API is a zero-cost audio solution.&lt;/strong&gt; It's not studio quality, but for a game where the point is to &lt;em&gt;identify&lt;/em&gt; a language, it's perfect. Dozens of language voices, built into every modern browser, no API keys, no usage fees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building for the world from day one changes how you think about UX.&lt;/strong&gt; When you know your UI will be in Arabic (right-to-left!) and Japanese (longer text strings!), you design differently. Buttons need flexible widths. Text can't be hardcoded into fixed layouts. It's a constraint that makes you a better designer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;LinguaGuessr is live and free to play.&lt;/p&gt;

&lt;p&gt;Live demo URL: &lt;a href="https://linguaguessr.vercel.app/" rel="noopener noreferrer"&gt;https://linguaguessr.vercel.app/&lt;/a&gt;&lt;br&gt;
GitHub repo URL : &lt;a href="https://github.com/Manjunath3155/linguaguessr" rel="noopener noreferrer"&gt;https://github.com/Manjunath3155/linguaguessr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick a language. Drop a pin. See how close you get.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you speak one of the 125+ languages in the database and catch a wrong coordinate or a bad fun fact, open an issue — the whole point is making this better together.&lt;/p&gt;

&lt;p&gt;Built for the &lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;Lingo.dev Hackathon&lt;/a&gt;. If you're building anything multilingual, seriously check out their compiler. It turned what I expected to be a week of i18n plumbing into an evening of configuration.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Manjunath Patil with Next.js, Supabase, Leaflet, and Lingo.dev.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
