Skip to main content
RapidDev - Software Development Agency
App Featuresmedia-content18 min read

How to Add a Multi-Language Audio Guide to Your App (Copy-Paste Prompts Included)

A multi-language audio guide needs four pieces: a Supabase table storing audio URLs keyed by language code, just_audio for playback, a dio-powered offline download manager, and transcript text synced to the playback position. FlutterFlow builds the guide stop list and language selector visually; playback and downloads need a Custom Action Dart block. Expect 2–4 days with FlutterFlow, 2–4 weeks custom. Running costs start at $0–25/mo for small guide sets and scale with audio bandwidth and language count.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

media-content

Build with AI

2–4 days with FlutterFlow + manual audio wiring

Custom build

2–4 weeks custom dev

Running cost

$0–25/mo up to ~50 audio files; $75–200/mo at 10K users with CDN bandwidth

Works on

Mobile

Everything it takes to ship a Multi-Language Audio Guide — parts, prompts, and real costs.

TL;DR

A multi-language audio guide needs four pieces: a Supabase table storing audio URLs keyed by language code, just_audio for playback, a dio-powered offline download manager, and transcript text synced to the playback position. FlutterFlow builds the guide stop list and language selector visually; playback and downloads need a Custom Action Dart block. Expect 2–4 days with FlutterFlow, 2–4 weeks custom. Running costs start at $0–25/mo for small guide sets and scale with audio bandwidth and language count.

What a Multi-Language Audio Guide Feature Actually Is

A multi-language audio guide lets users tap a stop on a list — a museum exhibit, a trail waypoint, a lesson chapter — and hear narrated commentary in their chosen language, with a synchronized transcript scrolling alongside. The audio files live in Supabase Storage, keyed by language code in a jsonb column. The app downloads them to the device so the guide works without connectivity, tracks which stops the user has completed, and lets them switch languages at any point without losing their place. The product decisions that matter most are whether guides are free or paywalled, how many languages you support at launch (each multiplies storage and bandwidth), and whether you trigger stop playback manually or automatically by GPS proximity.

What users consider table stakes in 2026

  • Language selector with flag icons shows every available language before the guide starts, not buried in settings
  • Offline download indicator per stop — users need to know what they've downloaded before entering a museum or going underground
  • Transcript text scrolls in sync with audio playback, with the current sentence highlighted
  • Previous and next stop buttons with auto-advance option at clip end so hands-free listening works
  • Download progress bar with cancel option — a 50 MB language pack takes time on a slow connection
  • Clear 'not available in this language' state when a specific stop hasn't been recorded in the selected language

Anatomy of the Feature

Six components. FlutterFlow handles the UI layer visually; the audio engine and download manager require Custom Action Dart blocks. The data model is the linchpin — get the jsonb schema right up front and the rest snaps together.

Layers:UIDataBackend

Language selector

UI

A Flutter DropdownButton or CupertinoPicker listing available languages pulled from the guide's available_languages text[] column in Supabase. Flag icons rendered with flutter_svg. Selected language code written to shared_preferences so the choice persists across screen navigations.

Note: Store the selected language in shared_preferences or FlutterFlow App State — not a page-local variable — or it resets every time the user navigates to a stop detail screen and back.

Audio player engine

Backend

just_audio (^0.9.x) handles HTTP streaming and local file playback. AudioPlayer.setUrl() for streaming; AudioPlayer.setFilePath() for downloaded files. ConcatenatingAudioSource enables playlist mode so auto-advance between stops works without re-wiring the player.

Note: just_audio requires the iOS UIBackgroundModes audio entry in Info.plist or audio silences the moment the screen locks. Add this in FlutterFlow Project Settings before testing on a real device.

Guide stop list

UI

Flutter ListView of stop cards showing thumbnail, stop number, title, duration chip, and download status icon. Tapping a card calls player.setUrl(audioUrl) with the URL from audio_urls[selectedLanguage] for that stop and begins playback. Built visually in FlutterFlow using a Backend Query on the guide_stops table.

Note: Always null-check the jsonb field — audio_urls?[selectedLanguage] — before passing to the player. A missing language code returns null and crashes the player with an unhelpful 'source is null' error.

Offline download manager

Backend

dio (^5.x) downloads audio files to getApplicationDocumentsDirectory() via path_provider. Download progress tracked as a double (0.0–1.0) in state. Completed paths saved to shared_preferences keyed by stop_id + language code. Before playback, the app checks for a local file first and falls back to the streaming URL.

Note: This component cannot be built visually in FlutterFlow — it requires a Custom Action of approximately 50 lines of Dart. The payoff is full offline capability without a CDN dependency.

Content data layer

Data

Supabase table guide_stops with audio_urls and transcript columns stored as jsonb objects keyed by ISO 639-1 language code ('en', 'es', 'fr'). A single Supabase query with .eq('guide_id', guideId).order('stop_number') fetches all stops for the guide in one call.

Note: Store lat/lng on each stop row even if GPS auto-play isn't in v1 — retrofitting geolocation later requires a migration, and the columns cost nothing unused.

Transcript sync layer

UI

StreamBuilder listening to just_audio's positionStream emits the current playback position as a Duration. The transcript text is split into timed segments (stored in the transcript jsonb as an array of {start_ms, end_ms, text} objects). A comparison against position.inMilliseconds highlights the active segment in the Flutter RichText or AnimatedContainer.

Note: If transcripts aren't timed at launch, display the full transcript as static text that scrolls independently — users still find it useful, and you can add timed sync later without changing the data model.

The data model

Three tables cover guides, stops with multilingual content, and user progress. Run this in the Supabase SQL editor. The jsonb columns for audio_urls and transcript use ISO 639-1 language codes as keys.

schema.sql
1create table public.audio_guides (
2 id uuid primary key default gen_random_uuid(),
3 name text not null,
4 category text,
5 thumbnail_url text,
6 available_languages text[] not null default '{}',
7 is_published boolean not null default false,
8 created_at timestamptz not null default now()
9);
10
11create table public.guide_stops (
12 id uuid primary key default gen_random_uuid(),
13 guide_id uuid references public.audio_guides(id) on delete cascade not null,
14 stop_number int not null,
15 title text not null,
16 audio_urls jsonb not null default '{}',
17 transcript jsonb not null default '{}',
18 thumbnail_url text,
19 lat decimal(9,6),
20 lng decimal(9,6),
21 duration_seconds int,
22 created_at timestamptz not null default now()
23);
24
25create table public.user_progress (
26 id uuid primary key default gen_random_uuid(),
27 user_id uuid references auth.users(id) on delete cascade not null,
28 guide_id uuid references public.audio_guides(id) on delete cascade not null,
29 last_stop_number int not null default 1,
30 completed_at timestamptz,
31 unique(user_id, guide_id)
32);
33
34alter table public.audio_guides enable row level security;
35alter table public.guide_stops enable row level security;
36alter table public.user_progress enable row level security;
37
38create policy "Public can view published guides"
39 on public.audio_guides for select
40 using (is_published = true);
41
42create policy "Public can view stops for published guides"
43 on public.guide_stops for select
44 using (
45 exists (
46 select 1 from public.audio_guides
47 where id = guide_stops.guide_id
48 and is_published = true
49 )
50 );
51
52create policy "Users can read own progress"
53 on public.user_progress for select
54 using (auth.uid() = user_id);
55
56create policy "Users can write own progress"
57 on public.user_progress for insert
58 with check (auth.uid() = user_id);
59
60create policy "Users can update own progress"
61 on public.user_progress for update
62 using (auth.uid() = user_id);
63
64create index guide_stops_guide_order_idx
65 on public.guide_stops (guide_id, stop_number);

Heads up: The unique constraint on user_progress(user_id, guide_id) enables upsert logic — call supabase.from('user_progress').upsert({...}, { onConflict: 'user_id,guide_id' }) on every stop completion without checking for existing rows first.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Hand-built by developersFit for this feature:

Full native Flutter build gives complete control over background audio entitlements, offline sync with WorkManager (Android) and BGTaskScheduler (iOS), and GPS geofencing for auto-play triggers.

Step by step

  1. 1Set up iOS background audio capability in Xcode: Signing & Capabilities → Background Modes → check Audio, AirPlay, and Picture in Picture
  2. 2Implement just_audio with audio_session package for proper iOS interruption handling (phone calls, Siri) and Android audio focus management
  3. 3Build the download manager with WorkManager (Android) and BGTaskScheduler (iOS) for reliable background downloads that survive app suspension
  4. 4Add geolocator and geofence_foreground_service packages for GPS-triggered auto-play when users physically approach a guide stop

Where this path bites

  • iOS background audio entitlements and geofencing require Apple Developer account and device provisioning — cannot be tested in a simulator for background modes
  • Full timeline of 2–4 weeks assumes prior Flutter experience; audio session management and background task scheduling are non-trivial to get right across both platforms

Third-party services you'll need

The guide runs on Supabase Storage for audio files. ElevenLabs is optional for AI-generated voiceovers instead of recorded narration:

ServiceWhat it doesFree tierPaid from
Supabase StorageHosts audio files (2–5 MB each per language) and serves them over HTTPS for streaming and download1 GB storage, 2 GB bandwidth$25/mo (Pro) includes 100 GB storage + 200 GB bandwidth (approx)
Supabase Databaseguide_stops, audio_guides, user_progress tables with jsonb multilingual content500 MB storage, 2 projects$25/mo (Pro) includes 8 GB (approx)
ElevenLabsOptional AI voiceover synthesis per language — generate audio guide narration from text scripts without recording studios10,000 characters/moStarter $5/mo (30,000 chars); Creator $22/mo (100,000 chars) (approx)

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$15/mo

Supabase free tier covers ~50 audio files at 3 MB each (150 MB well within free storage). Small bandwidth at 100 users. ElevenLabs free tier sufficient for 1–2 language scripts at launch.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Audio silences the moment the screen locks on iOS

Symptom: On iOS, audio stops when the screen locks unless the app declares the audio background mode. just_audio handles the playback session correctly, but without UIBackgroundModes audio in Info.plist, iOS suspends the app and kills the audio. This manifests as audio stopping exactly when the screen lock timeout fires — users report it as 'the app keeps pausing'.

Fix: In FlutterFlow, add UIBackgroundModes with value 'audio' in Project Settings → iOS → Info.plist entries. For custom builds, enable Background Modes → Audio in Xcode Signing & Capabilities. Also add the audio_session package and call AudioSession.instance.configure(AudioSessionConfiguration.speech()) on app start to handle phone call interruptions gracefully.

just_audio silently fails on HTTP URLs on Android 9+

Symptom: Android 9 (API 28) and above block cleartext HTTP traffic by default. If Supabase Storage URLs are HTTP instead of HTTPS — which can happen with custom domain configurations or local dev tunnels — just_audio fails silently: the player state shows 'loading' indefinitely with no error event.

Fix: Confirm all audio URLs use HTTPS. In FlutterFlow Project Settings, verify the Supabase URL is https://. For any testing environment, ensure the URL scheme is https:// before passing it to player.setUrl(). Never ship with android:usesCleartextTraffic='true' in the production AndroidManifest.

Language selector resets to default on screen navigation

Symptom: Storing the selected language in a page-local Flutter state variable or FlutterFlow page state causes it to reset when the user taps into a stop detail screen and presses back. The user returns to the guide list to find English selected again even though they chose Spanish. This is one of the most common complaints in audio guide apps built with FlutterFlow.

Fix: Store the selected language code in FlutterFlow App State (not page state) or in shared_preferences via a Custom Action. Read it on initState of every screen that needs it. In the Custom Action for playback, always read the language code from App State rather than receiving it as a parameter that could be stale.

Supabase jsonb audio_urls returns null for missing languages

Symptom: The audio_urls jsonb column returns a Map<String, dynamic> in Dart. If a guide stop hasn't been recorded in the selected language, audio_urls['fr'] returns null. Passing null to player.setUrl() throws 'source is null' — an opaque crash that doesn't tell the user what actually went wrong.

Fix: Always null-check before playback: final audioUrl = (stop['audio_urls'] as Map?)?[selectedLanguage] as String?. If audioUrl is null, disable the play button and show a localized message like 'Not available in French — tap to switch language'. This pattern catches missing languages gracefully instead of crashing.

Offline downloads bloat device storage with no cleanup path

Symptom: Users who download guides in multiple languages can accumulate 500 MB+ of audio files on their device over time. Because the download paths are stored in shared_preferences with no expiry, there's no mechanism to clean them up. This becomes a 1-star review: 'this app ate 1 GB of my storage'.

Fix: Add a Downloads screen listing each downloaded stop with its file size (File(path).lengthSync()) and a delete button that calls File(path).deleteSync() and removes the key from shared_preferences. Show total download size in the header. Consider adding a 'Delete all downloads for this guide' option so users can free space in one tap.

Best practices

1

Store audio_urls as a jsonb object keyed by ISO 639-1 codes ('en', 'es', 'fr') from day one — adding languages later is a data entry task, not a schema migration

2

Check for a local file before streaming on every play action — users in underground museums or airplanes will lose connectivity mid-guide

3

Show download size next to each language option before the user starts downloading — a 5-language pack can be 75 MB; set expectations

4

Persist the selected language in shared_preferences or App State at the app level, not page level — language choice should outlive any navigation stack pop

5

Handle the 'not available in this language' state explicitly with a message and a language-switch shortcut rather than a silent empty player

6

Save user_progress on every stop completion, not just at the end of the guide — guides are interrupted constantly and users expect to resume exactly where they left off

7

Use ConcatenatingAudioSource for the stop playlist so just_audio can buffer the next stop while the current one plays — eliminates the gap between stops on auto-advance

When You Need Custom Development

FlutterFlow with Custom Actions covers the core guide experience. These requirements push past what visual builders handle:

  • GPS-triggered auto-play: automatically start the audio when a user walks within 30 meters of a guide stop using geofencing — requires WorkManager (Android) and BGTaskScheduler (iOS) for background location monitoring
  • AI-generated voiceover on demand per language using ElevenLabs voice cloning with a custom institutional voice — involves a server-side generation pipeline, not a one-time ElevenLabs export
  • Time-limited signed URLs for paid guide protection — audio URLs that expire after 24 hours so downloaded files can't be redistributed, requiring a custom URL rotation mechanism
  • Real-time live audio commentary: a human guide speaking live and streaming to multiple app users simultaneously, which requires WebRTC or HLS streaming infrastructure far beyond static file playback

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

How do I store audio files for multiple languages in Supabase?

Store audio files in Supabase Storage in a folder structure like /guides/{guide_id}/{stop_id}/{language_code}.mp3. In the guide_stops table, keep a jsonb column called audio_urls with entries like {'en': 'https://...en.mp3', 'es': 'https://...es.mp3'}. Query the column in Flutter and access the URL with audioUrls[selectedLanguage]. This lets you add languages without changing the schema or the app code.

Can audio guides work offline on mobile?

Yes, and for museum and travel guides, offline is usually a requirement — many locations have poor connectivity. Use the dio package to download audio files to getApplicationDocumentsDirectory() before the user enters the venue. Store the local file path in shared_preferences keyed by stop ID and language code. On playback, check for the local file first with File(path).existsSync() and fall back to the streaming URL if not found.

How do I sync transcript text to audio playback position in Flutter?

Store transcripts as a jsonb array of timed segments: [{start_ms: 0, end_ms: 4200, text: 'Welcome to gallery one.'}, ...]. In Flutter, wrap your transcript widget in a StreamBuilder listening to just_audio's positionStream. On each position update, find the segment where position.inMilliseconds is between start_ms and end_ms and apply highlighting to that element. For launch without timed transcripts, display the full text statically — users still benefit from the text, and you can add timing data later.

What Flutter package is best for audio playback in a guide app?

just_audio (^0.9.x) is the clear choice for guide apps. It supports HTTP streaming and local file playback from the same API, handles background audio correctly with the audio_session package, and provides positionStream for transcript sync. ConcatenatingAudioSource enables playlist mode for smooth auto-advance between stops. The main alternative, audioplayers, is simpler but lacks the streaming and playlist features a multi-stop guide needs.

How do I auto-play the next stop when the current one finishes?

The cleanest approach is ConcatenatingAudioSource: create an AudioSource for each stop in order and pass the full list to player.setAudioSource(). just_audio advances automatically. For more control — pausing between stops to show a summary screen, for example — listen to player.playerStateStream and watch for PlayerState.completed, then call player.setUrl(nextStop.audioUrl) and player.play() in the handler.

How do I handle language selection persisting across screens?

Store the selected language code in FlutterFlow App State (not page state) or in shared_preferences. In shared_preferences, write the code on every change with prefs.setString('selected_language', code) and read it on initState of any screen that needs it. In FlutterFlow, use a global App State variable and update it from the DropdownButton's onChange action — this survives any navigation stack pop.

Can I use AI to generate audio guide voiceovers in multiple languages?

Yes. ElevenLabs is the most practical option: write your stop scripts in the source language, translate them (DeepL or GPT-4), then generate audio via the ElevenLabs API for each language. The Starter plan ($5/mo) covers 30,000 characters — enough for roughly 10 stops at 200 words each across 3 languages. Store the generated MP3s in Supabase Storage. The audio quality is good enough for informational guides; for flagship cultural institutions, recorded human narration still wins.

How much does it cost to store audio files for 10 stops in 5 languages?

At roughly 3 MB per audio file: 10 stops × 5 languages = 50 files = 150 MB of storage. That fits comfortably within Supabase's free tier (1 GB storage). Bandwidth is where cost appears — if 1,000 users each stream rather than download, that's roughly 150 GB of bandwidth per cycle through all stops, pushing you toward Supabase Pro ($25/mo). Encouraging offline download actually reduces bandwidth costs because each user downloads once rather than streaming repeatedly.

RapidDev

Need this feature production-ready?

RapidDev builds a multi-language audio guide into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.