# How to Add Real-Time Translation to Your App — Copy-Paste Prompts Included

- Tool: App Features
- Last updated: July 2026

## TL;DR

Real-time translation needs four pieces: a debounced text input (500ms pause before API fires), a DeepL or Google Translate Edge Function with auto-detect, a translation cache to avoid re-billing identical text, and a TTS play button for the translated output. With Lovable you can ship a working translator in 4–6 hours. Costs run $0/month at 100 users — DeepL's free tier covers 500K characters per month — and $0–20/month at 1,000 users once you approach that limit.

## What Real-Time Translation Actually Is

Real-time translation converts typed or spoken text to another language as the user pauses — not on every keystroke, and not after they press a button. The 500ms debounce is what makes it feel instant rather than laggy. The engine is a DeepL or Google Translate API call routed through a Supabase Edge Function, which keeps your API key out of the client. The product differentiators that matter in 2026 are language auto-detect (so users don't have to select their source language), a swap button for two-person conversation mode, TTS playback of the translated output, and a translation cache that prevents re-billing the same sentence twice. History per session completes the feature for users who want to review or copy previous translations.

## Anatomy of the Feature

Eight components across UI, backend, service, and data layers. The debounce logic and the DeepL auto-detect wiring are where most first builds get it wrong.

- **Language Selector** (ui): Two dropdowns side by side: source language and target language. The source dropdown includes an 'Auto-detect' option as the first item. Populated from a static list of DeepL or Google Translate supported language codes with display names (English, Spanish, French, German, Japanese, etc.) — at minimum 20 options. Selection is stored in component state and persisted to localStorage for returning users.
- **Text Input with Debounce** (ui): A textarea with a useDebounce hook (500ms delay) that fires the translation call only when the user pauses typing. Shows a character count in the bottom-right corner. The translation call fires on the debounced value change, not on onChange. Maximum input: 5,000 characters for DeepL (API limit per request).
- **Translation Engine** (backend): A Supabase Edge Function calling DeepL API (/v2/translate) or Google Cloud Translation API. The request body includes: text (the source string), target_lang (user-selected code), and source_lang only when a specific language is chosen (omit entirely for auto-detect). The response includes translated_text and detected_source_language from DeepL. The API key lives in Cloud tab → Secrets.
- **Voice Input Integration** (service): Optional but commonly requested: a mic button next to the text input that uses SpeechRecognition API (web) or speech_to_text (Flutter) to populate the input field with spoken text. The transcript feeds into the same debounced translation pipeline — no separate code path needed.
- **TTS Output** (service): A play button next to the translated output text. On click, creates a SpeechSynthesisUtterance with the translated text and the target language code (e.g. 'fr-FR', 'es-ES'). Uses Web Speech API SpeechSynthesis on web or flutter_tts on Flutter. The voice is selected from available voices matching the target language code.
- **Translation Cache** (data): A Supabase table with a SHA-256 hash of (source_text + source_lang + target_lang) as the primary key. Before each API call, the Edge Function queries this table. Cache hits save both the API cost and the ~200ms round-trip to the translation service.
- **Swap Languages Button** (ui): An icon button (two arrows in a circle) between the source and target selectors. On click: exchanges source and target language codes, moves the current translated output text into the input textarea, clears the translation output, and triggers the debounced translation call immediately (override the 500ms debounce on swap).
- **History List** (data): A Supabase table storing each completed translation with source text, translated text, language pair, and timestamp. Shown in a scrollable list below or in a side panel. RLS ensures users see only their own history. Users can tap a history item to load it back into the input/output fields.

## Data model

Two tables: a global translation cache (deduplicates API calls for identical content) and per-user translation history. Run this in the Supabase SQL editor:

```sql
create table public.translation_cache (
  text_hash text primary key,
  source_lang text not null,
  target_lang text not null,
  translated_text text not null,
  created_at timestamptz not null default now()
);

create table public.translation_history (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  source_text text not null,
  translated_text text not null,
  source_lang char(5) not null,
  target_lang char(5) not null,
  created_at timestamptz not null default now()
);

alter table public.translation_cache enable row level security;
alter table public.translation_history enable row level security;

create policy "Authenticated users can read cache"
  on public.translation_cache for select
  to authenticated
  using (true);

create policy "Service role inserts cache"
  on public.translation_cache for insert
  to service_role
  with check (true);

create policy "Users see own history"
  on public.translation_history for select
  using (auth.uid() = user_id);

create policy "Users insert own history"
  on public.translation_history for insert
  with check (auth.uid() = user_id);

create index translation_history_user_idx
  on public.translation_history (user_id, created_at desc);
```

The Edge Function uses the service_role key to write to translation_cache (global dedup table). The frontend client uses the anon key to read cache and write to translation_history. The text_hash primary key on translation_cache ensures duplicate translations are never stored even under concurrent requests.

## Build paths

### Lovable — fit 4/10, 4–6 hours

Lovable builds the React UI quickly and the Edge Function calling DeepL is a natural Supabase-first pattern. The translation cache and auto-detect wiring may need follow-up prompts to get exactly right.

1. Create a new Lovable project with Lovable Cloud connected; add your DEEPL_API_KEY to Cloud tab → Secrets
2. Run the data model SQL in Cloud tab → Database → SQL Editor to create translation_cache and translation_history tables with RLS
3. Paste the prompt below; Agent Mode will build the debounced input, language selectors, Edge Function, swap button, TTS playback, and history list
4. Test the auto-detect path: type French text without selecting a source language and verify the returned translated_text and detected_source_language are correct
5. Verify the debounce by typing rapidly and confirming the Edge Function is called only once per 500ms pause, not on every keystroke (check Supabase Edge Function logs)

Starter prompt:

```
Build a real-time translation feature. UI layout: two panels side by side (source on left, translated output on right). 1) Source panel: a textarea with character counter (max 5,000), a language selector dropdown above it with 'Auto-detect' as the first option followed by at least 20 languages (EN, FR, ES, DE, IT, PT, NL, PL, RU, JA, ZH, AR, KO, TR, SV, DA, FI, NB, CS, HU), and a microphone icon button (use Web Speech API SpeechRecognition to populate the textarea). 2) In the center, a swap button (two arrows icon) that: exchanges source and target language codes, moves translated output text to source textarea, clears output, triggers immediate translation. 3) Target panel: shows translated text (read-only), a play button using Web Speech API SpeechSynthesis with the target language code, a copy-to-clipboard button. 4) Debounce: use a useDebounce hook (500ms) on the source textarea value. When debounced value changes and length > 0, call Supabase Edge Function 'translate'. 5) Edge Function 'translate': compute SHA-256 of (source_text + source_lang + target_lang), check translation_cache table for this hash. If found, return cached result. If not found, call DeepL API POST /v2/translate with {text: [source_text], target_lang} — IMPORTANT: only include source_lang in the body when a specific language is selected; omit it entirely (do not send 'auto') when Auto-detect is chosen. Extract translated_text and detected_source_language from DeepL response, insert to translation_cache with service_role key, insert to translation_history for the current user, return {translated_text, detected_source_language}. 6) Show detected source language label under source dropdown when auto-detect is active. 7) Below the panels, show the last 10 translation_history rows in a scrollable list; clicking a row loads source/output back into the panels. 8) Loading state: spinner in target panel while Edge Function is running. Error state: 'Translation failed — quota may be exceeded' when DeepL returns 403.
```

Limitations:

- Language auto-detect requires that source_lang is completely absent from the DeepL request — Lovable may set it to 'auto' on first pass, which DeepL rejects with a 400 error; add a follow-up prompt if auto-detect returns errors
- TTS output for less common languages (Arabic, Hebrew, Thai) depends on the browser's installed voice library; Web Speech API may not have voices for all 20+ target languages
- Lovable preview iframe may block clipboard access for the copy button; test copy functionality on the published URL

### Flutterflow — fit 3/10, 6–8 hours

FlutterFlow handles the UI layout and flutter_tts reads translated text aloud. The DeepL API call and debounce logic require custom Dart code actions. Best when the app is already in FlutterFlow.

1. Build the two-panel layout in FlutterFlow: a TextField for source input, two DropdownButton widgets for language selection, a read-only TextField for translated output, and action buttons (swap, copy, play)
2. Create a Custom Action 'TranslateText' that implements a debounce timer in Dart (Timer from dart:async, cancel previous timer on each call), calls your Supabase Edge Function 'translate' via http.post, and updates Page State variables 'translatedText' and 'detectedLang'
3. Add a Custom Action 'SpeakTranslation' that calls flutterTts.setLanguage(targetLang), checks flutterTts.isLanguageAvailable(targetLang) first, and calls flutterTts.speak(translatedText)
4. Wire the TextField onChanged to trigger TranslateText; the debounce in the Custom Action prevents API calls on every keystroke
5. Add a Custom Action 'SwapLanguages' that updates Page State variables for source/target language and source/translated text, then immediately calls TranslateText without the debounce

Limitations:

- Debounce logic in FlutterFlow requires a Custom Dart Action using dart:async Timer; there is no built-in debounce widget or action in the visual builder
- Language auto-detect requires parsing the DeepL response detectedSourceLanguage field and updating a Page State variable in the Custom Action — this is custom Dart code, not a visual action
- flutter_tts fails silently for RTL languages (Arabic, Hebrew) on devices that don't have the language TTS pack installed; always check isLanguageAvailable before calling speak()

### Custom — fit 5/10, 1–2 weeks

Required for streaming word-by-word output, two-person conversation mode with speaker detection, offline translation via on-device NLLB-200 model, or custom glossary enforcement for brand terms.

1. Implement streaming translation using Google Cloud Translation's Streaming API or DeepL's streaming endpoint for word-by-word output as the user types
2. Build conversation mode: alternate UI showing Person A / Person B turns with automatic language swap after each turn; TTS reads each turn aloud in the target language
3. Integrate NLLB-200 (Meta's multilingual model) via WASM for offline translation of the top 10 most common language pairs; cache model files locally on first use
4. Implement custom glossary: define brand terms that must never be translated (product names, trademarks); inject glossary into DeepL API calls via the glossary_id parameter

Limitations:

- NLLB-200 models for offline translation are 100MB–600MB each depending on compression — shipping them in a Flutter app requires careful size management and conditional downloading
- Streaming from Google Cloud Translation requires a WebSocket connection; Supabase Edge Functions have a 150-second timeout, which limits very long streaming sessions

## Gotchas

- **Translation fires on every keystroke causing 20+ API calls per sentence** — Without a debounce, the onChange handler fires on every character typed. A 20-word sentence generates 80+ character events, each triggering an API call. At DeepL pricing this is still cheap, but the UI flickers on every character as translations stream in — making the feature feel broken even when it's technically working. Fix: Wrap the API call in a useDebounce hook (500ms). The hook cancels the previous timer whenever the input changes and only fires the API call when the user pauses for 500ms. On Flutter, use a dart:async Timer with cancel() on each new input event. This single change is the difference between a laggy feature and a smooth one.
- **Translated text is always in English regardless of target language selection** — This is the most common first-build failure: the target_lang parameter is hardcoded as 'EN' in the Edge Function from an early test, and the AI didn't wire the dropdown selection to the API call correctly. The translation appears to work — it translates — but always to English. Users assume the feature is broken. Fix: Log the Edge Function request payload to verify target_lang is being received with the correct value. In the Supabase Dashboard, go to Edge Functions → Logs and check the request body for each call. Ensure the frontend state variable holding the target language is being sent in the request body, not a hardcoded value.
- **Language auto-detect always returns English even for French or Spanish input** — DeepL's auto-detect only activates when the source_lang parameter is absent from the API request. If the frontend sends source_lang: 'auto', DeepL returns a 400 Bad Request. If it sends source_lang: '' or source_lang: null, some implementations still include the key. The result is no auto-detection — DeepL defaults to English or rejects the request entirely. Fix: When the user selects 'Auto-detect', build the request body without the source_lang key at all: {text: [source_text], target_lang: target}. Use conditional object spreading: const body = {text: [text], target_lang, ...(sourceLang !== 'auto' && {source_lang: sourceLang})}. Verify by omitting source_lang in a direct API test and confirming detectedSourceLanguage is returned.
- **Translation history loads for all users, not just the current user** — If RLS is not enabled on the translation_history table, or the policy is set to allow all authenticated users to read all rows, every logged-in user sees everyone's translation history. This is a data privacy issue and a confusing UX — users see translations they didn't make. Fix: Enable RLS on translation_history and add the policy: CREATE POLICY 'Users see own history' ON translation_history FOR SELECT USING (auth.uid() = user_id). Verify in the Supabase Table Editor by logging in as two different test users and confirming each sees only their own rows.
- **flutter_tts fails to play translated Arabic or Hebrew text** — RTL language TTS (Arabic, Hebrew, Persian) requires specific locale codes (ar-SA, he-IL) and the corresponding TTS language pack installed on the device. On Android, many devices don't ship with Arabic or Hebrew TTS. The flutter_tts speak() call completes without error but produces no audio, which looks like a silent crash. Fix: Call flutterTts.isLanguageAvailable(targetLang) before any speak() call. If the result is false, show an inline message: 'Tap to download the Arabic language pack in your device settings.' On Android, link to Settings → Language and Input → Text-to-speech options. Never call speak() if the language is unavailable.

## Best practices

- Implement debounce at 500ms — wire the translation call to a debounced value, never to onChange directly
- Omit source_lang from the DeepL request entirely when Auto-detect is selected — sending 'auto' or null is invalid and breaks detection
- Cache translations by SHA-256(source_text + source_lang + target_lang) in a global Supabase table — cache hit rate of 60–70% is realistic for common phrases and eliminates most API cost
- Check flutter_tts.isLanguageAvailable(locale) before every speak() call for non-English languages — silently fails on devices without the TTS pack installed
- Log Edge Function request payloads during development to verify target_lang and source_lang are being sent with correct values, not hardcoded strings
- Limit history inserts to completed translations (500ms pause + API response received) — not every debounce trigger, or history fills with partial mid-sentence text
- Set a 5,000-character limit on the source textarea (DeepL's per-request limit) and show a character counter so users don't hit the limit mid-sentence without warning

## Frequently asked questions

### Which translation API is most accurate?

DeepL consistently outperforms Google Translate and Microsoft Translator for European languages (English, French, German, Spanish, Italian, Portuguese, Dutch, Polish) based on independent benchmarks. For Asian languages (Japanese, Chinese, Korean) and less common languages, Google Cloud Translation has broader coverage and comparable quality. For budget-conscious apps, Azure Cognitive Services Translator offers a generous 2M free character tier monthly.

### How do I add auto-detect language?

For DeepL: add 'Auto-detect' as the first option in the source language dropdown. When selected, build the API request body without the source_lang key entirely — use conditional spreading to omit it. DeepL's response includes detectedSourceLanguage which you display below the source dropdown. For Google Translate: omit the source parameter from the request body. Never send source_lang: 'auto' — that string is not a valid language code and returns a 400 error.

### Can users translate speech in real time?

Yes, by combining voice input with the translation pipeline. Add a mic button using the Web Speech API SpeechRecognition with interimResults: true. The live transcript populates the source textarea, and the 500ms debounce fires the translation Edge Function on each pause. Users hear their speech transcribed and see the translation appear within half a second of finishing a phrase. Add flutter_tts or SpeechSynthesis to read the translation aloud for a full hands-free translation experience.

### How much does the translation API cost?

DeepL: free for 500K characters/month, then approximately $0.0000025/character. A 100-word paragraph is ~600 characters — that's 833 free paragraphs per month. Google Cloud Translation: free 500K chars/month, then $20/1M chars. At 1,000 users averaging 2,000 characters of translation per month, you're at 2M chars — $40 on Google or $5 on DeepL with caching. Caching identical translations typically reduces effective API usage by 60–70%.

### How do I support offline translation?

Two options: (1) Cache the most common translations in the translation_cache Supabase table and pre-load them locally in a service worker or SQLite database on first app use. This covers the top 500–1,000 phrases offline without ML models. (2) Bundle Meta's NLLB-200 model compiled to WebAssembly (web) or TensorFlow Lite (Flutter) for true on-device translation of any text. NLLB-200 supports 200 languages but the model files are 150MB–600MB. For most apps, option 1 (phrase caching) is the practical choice.

### Can I add a swap languages button?

Yes, and it's essential UX for conversation mode. The swap button should: exchange source and target language state values, move the current translated output text into the source textarea, clear the translation output, and trigger an immediate translation call (bypassing the 500ms debounce so the response feels instant). On FlutterFlow, implement this in a Custom Action that updates all relevant Page State variables sequentially and then calls the translation action.

### How do I save translation history?

Insert a row to the translation_history table after a successful translation completes — when the user has paused typing for 500ms and the Edge Function has returned a result. Include user_id (from auth), source_text, translated_text, source_lang, and target_lang. With RLS enabled (auth.uid() = user_id), users only ever see their own history. Display the last 10–20 rows in a scrollable list sorted by created_at descending.

### Does this work for right-to-left languages like Arabic?

The translation API handles Arabic, Hebrew, and Persian correctly. The UI requires text direction handling: set dir='rtl' on the output textarea when target_lang is an RTL language code (ar, he, fa). On Flutter, use TextDirection.rtl in the Text widget for the translated output. For TTS, check flutter_tts.isLanguageAvailable('ar-SA') before calling speak() — Arabic TTS requires a language pack that many Android devices don't ship with by default.

---

Source: https://www.rapidevelopers.com/app-features/real-time-translation
© RapidDev — https://www.rapidevelopers.com/app-features/real-time-translation
