<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Manjunath Patil</title>
    <description>The latest articles on DEV Community by Manjunath Patil (@manjunathpatil).</description>
    <link>https://dev.to/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>DEV Community: Manjunath Patil</title>
      <link>https://dev.to/manjunathpatil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manjunathpatil"/>
    <language>en</language>
    <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://dev.to/manjunathpatil/terrarun-a-territory-capture-running-app-where-every-loop-you-run-becomes-your-turf-ie</link>
      <guid>https://dev.to/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://dev.to/manjunathpatil/i-built-a-geoguessr-for-languages-heres-how-i-made-it-speak-8-languages-overnight-4h1d</link>
      <guid>https://dev.to/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>
