How PoquitoTalk Turns English Into Panamanian Spanish Voice Notes

PoquitoTalk logo
PoquitoTalk, built in Bocas del Toro, Panama

Updated Sep 28, 2026. An earlier version of this post described the architecture I planned in the first week (an on-device Gemma model, Firestore, WebSockets). This version describes what actually shipped in v1.5.10 on Google Play.

The problem: WhatsApp voice notes

In Bocas del Toro, almost everything runs on WhatsApp: the water taxi, the plumber, the A/C technician, the landlord. And a lot of it happens in voice notes, not text. Local workers often don't read long messages; they listen.

For expats who only have textbook Spanish, that is hard. Translation apps give you formal Spanish text, and then you still have to read it out loud into your phone. PoquitoTalk skips that step: you type or say what you need in English, and it sends a natural Panamanian Spanish voice note to WhatsApp.

The second half is the reply. Most two-way translation apps expect both people to install the app. A boat captain at the tiller is not going to do that, so the contractor side had to work from a link in a browser.

What the system looks like

There are three parts, and none of them need a database server:

  1. The mobile app: Expo SDK 57 and React Native 0.86, published on Google Play.
  2. The contractor page: a single HTML page at /talk that the contractor opens from a WhatsApp link. No install, no account.
  3. A small PHP API on the same shared LiteSpeed hosting as the website. It proxies translation and speech-to-text, stores voice notes as files, and keeps each walkie-talkie room as a JSON file.
TEXT
Expat's phone (Expo app)                    Contractor's phone (any browser)
┌──────────────────────────┐                ┌──────────────────────────────┐
│ 1. Speak or type English │                │ /talk?r=room_xxxx            │
│ 2. Speech-to-text        │                │ Tap Poquito to talk          │
│ 3. Translate + Panama    │                └──────────────┬───────────────┘
│    dialect rules         │                               │
│ 4. Spanish voice (TTS)   │                               │
│ 5. Share to WhatsApp  ───┼── link ──────────────────────▶│
└────────────┬─────────────┘                               │
             │           poll every 2.5 s                  │
             ▼                                             ▼
┌───────────────────────────────────────────────────────────────────────┐
│ PHP API on shared LiteSpeed hosting (poquitotalk.hero-apps.com/api)   │
│ translate.php · transcribe.php · audio.php · walkie.php               │
│ Rooms = JSON files · voice notes = audio files on disk                │
└───────────────────────────────────────────────────────────────────────┘

Step 1: Speech-to-text with fallbacks

Island internet is unreliable and every speech API has bad days, so transcription tries up to four engines in order and stops at the first one that works:

typescript
// src/services/transcriptionService.ts (simplified)
// 1. ElevenLabs Scribe (scribe_v1)
// 2. Groq Whisper (whisper-large-v3), if a key is set
// 3. OpenAI Whisper (whisper-1), if a key is set
// 4. Own PHP proxy: /api/transcribe.php
// 5. Give up and let the user type

const BOCAS_WHISPER_PROMPT =
  "Bocas del Toro, Isla Colón, Bastimentos, Carenero, Isla Solarte, " +
  "Red Frog, Old Bank, Playa Bluff, Bocas Town, lancha, muelle Taxi 25, " +
  "capitán, dólares, WhatsApp";

The prompt lists local place names and island words like lancha and muelle, so the engine expects them instead of guessing. After transcription, two small cleanup steps run: one removes stutters and repeats ("I, I need the the boat"), and one normalizes local place names.

Step 2: Translation with a phrase map and dialect rules

The first plan was an on-device LLM; the translation file was called gemma.ts until v1.5.11, when it became translation.ts. What shipped is simpler and more predictable for short service messages:

  1. A hand-written phrase map of about 85 common island requests (water pressure, Starlink outages, table reservations, plants for the garden), each with a natural Panamanian version.
  2. Language direction detection, so an English sentence never gets "translated" from Spanish to English by accident.
  3. Machine translation with fallbacks: MyMemory first, then my own PHP proxy, then Google Translate.
  4. Panamanian dialect rules that run on every result. Generic Spanish says barco or taxi acuático; in Bocas everyone says lancha.
typescript
// src/services/translation.ts: polishPanamaSpanish() (excerpt)
text = text.replace(/\bel\s+barco\b/gi, 'la lancha');
text = text.replace(/\btaxi\s+acuático\b/gi, 'lancha');
text = text.replace(/\bwater\s+taxi\b/gi, 'lancha');
text = text.replace(/\bwater truck\b/gi, 'camión cisterna');
text = text.replace(/\bwater tank\b/gi, 'tanque de reserva');

If every online service fails, a small local fallback still returns something usable, so the user is never left with an empty screen.

Step 3: A voice that sounds like a person

Spanish text becomes audio with ElevenLabs (eleven_multilingual_v2) in one of two voices: Diego (male) or Sofia (female). The user picks the voice that matches them. If ElevenLabs is unavailable, Google Cloud Text-to-Speech takes over.

The ready-made phrases in the Presets tab don't call any API at all. Their audio is generated once and served as files (over 300 clips on the server, plus a set bundled in the app), so the most common requests play instantly, even on a weak connection.

Step 4: A walkie-talkie the contractor can use without installing anything

When the expat starts a live conversation, the app creates a room ID and a link like poquitotalk.hero-apps.com/talk?r=room_…, which goes out in WhatsApp. The contractor taps it and sees Poquito with one instruction: "Toca a Poquito para hablar" (tap Poquito to talk). Their voice is recorded in the browser with the standard MediaRecorder API.

There are no WebSockets. Both sides post messages to walkie.php and poll for new ones every 2.5 seconds:

javascript
// talk.html: the contractor's side
async function pollServerMessages() {
  const res = await fetch(
    `/api/walkie.php?action=poll&room=${encodeURIComponent(roomId)}&since=${lastPollTimestamp}`
  );
  // ...append new messages, play incoming audio
}
setInterval(pollServerMessages, 2500);

On cheap shared hosting, polling is the boring option that works. Each room is a JSON file; each voice note is an audio file next to it.

Because live sessions cost real API money, they have limits. A session only starts counting when the first voice message is sent (opening the link does nothing), and then:

typescript
// src/services/walkieTalkie.ts
const SESSION_MAX_DURATION_MS = 15 * 60 * 1000;     // 15 minutes max active session
const SESSION_INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes silence auto-close
const SESSION_MAX_TURNS = 15;                        // 15 voice exchanges per session

Pricing that covers the API costs

Every voice note costs money to generate, so nothing is truly unlimited without a plan behind it. The current plans on Google Play:

PlanPriceWhat you get
Annual Explorer Pass$39.99 / year (7 days free)Unlimited voice notes and live sessions
Monthly Resident Pass$9.99 / monthUnlimited voice notes and live sessions
7-Day Travel Pass$4.99, non-renewing100 voice notes and 20 live sessions
50 Credits Pack$4.99 once, never expires50 voice notes or 10 live sessions (1 live session = 5 credits)

Subscriptions run through RevenueCat. The credits pack is also sold on a web funnel through Stripe, and web purchases are redeemed in the app through RevenueCat.

Small detail: WhatsApp link previews

WhatsApp caches link preview images for days. When I updated the logo, old previews kept showing up. The fix is a version number on the image URL:

html
<meta property="og:image" content="https://poquitotalk.hero-apps.com/logo_v6_og.png?v=1.0.4" />

Tech stack

  • App: Expo SDK 57, React Native 0.86, expo-audio.
  • Speech-to-text: ElevenLabs Scribe, with Groq Whisper, OpenAI Whisper and a PHP proxy as fallbacks.
  • Translation: phrase map, MyMemory, Google Translate, and custom Panamanian dialect rules.
  • Voices: ElevenLabs (Diego, Sofia), with Google Cloud Text-to-Speech as fallback.
  • Backend: plain PHP on Namecheap LiteSpeed hosting. No database server.
  • Payments: RevenueCat, Google Play Billing, Stripe.

What I'd tell other builders

  1. Ship the boring version. I planned an LLM, a cloud database and WebSockets. A phrase map, dialect rules, JSON files and polling shipped instead, and they work.
  2. Design for the person on the other end. The contractor never installs anything. That one decision shaped the whole walkie-talkie.
  3. Always have a fallback. On an island with spotty internet, every external API in this app has a second or third option behind it.

Follow the build on X: @DorienVibecodes

Dorien Van den Abbeele

Written by Dorien Van den Abbeele

Solo developer building PoquitoTalk in Bocas del Toro, Panama. Sharing real-world lessons in mobile monetization, voice AI engineering, and product distribution in public.

← Prev: Surviving a Google Play Rejection All Articles →