fonika_translate
Multilingual translation, TTS and ASR for Flutter — built by 229Langues.
Supports African languages (Fon, Yoruba, Hausa, Adja, Bariba) + 100+ world languages.
Offline-first: local keys → device cache → API, with automatic retry and exponential backoff.
Features
| Feature | African languages | European / World languages |
|---|---|---|
| Text translation | ✅ API (100+ langs incl. Bariba) | ✅ Same API |
| Batch translation | ✅ | ✅ |
| TTS (audio synthesis) | ✅ API → Fon, Yoruba, Hausa | ✅ Platform TTS (flutter_tts) |
| ASR (speech-to-text) | ✅ API → Fon, Adja, Yoruba, Hausa | ✅ Platform ASR (speech_to_text) |
| Local translations (offline) | ✅ | ✅ |
| Device cache (SharedPreferences) | ✅ | ✅ |
| Retry with backoff | ✅ | ✅ |
| Flutter widgets | ✅ | ✅ |
| PDF translation | ✅ | ✅ |
| TXT file translation | ✅ | ✅ |
| PDF text extraction | ✅ | ✅ |
Installation
dependencies:
fonika_translate: ^0.1.1
Android permissions
Add to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>
iOS permissions
Add to ios/Runner/Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is needed for speech recognition.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Speech recognition is used for voice input.</string>
Quick start
import 'package:fonika_translate/fonika_translate.dart';
final fonika = FonikaTranslate(
apiToken: 'YOUR_TOKEN', // API token
maxRetries: 3, // retries on 5xx / 429
deviceCacheTtl: const Duration(days: 7),
);
await fonika.init();
Getting a token:
- For testing: Contact 229Langues for a public test token
Flutter widgets
FonikaProvider
Wrap your app once with FonikaProvider to make the client available anywhere in the widget tree:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final fonika = FonikaTranslate(apiToken: 'YOUR_TOKEN');
await fonika.init();
// Load your local translations (optional)
fonika.loadTranslations({
'fr': {'greeting': 'Bonjour !', 'app': {'title': 'Mon App'}},
'en': {'greeting': 'Hello!', 'app': {'title': 'My App'}},
});
runApp(
FonikaProvider(
client: fonika,
child: const MyApp(),
),
);
}
Retrieve the client anywhere below in the tree:
final fonika = FonikaProvider.of(context);
FonikaTranslatedText
Translates a key or plain text and renders it as a Text widget. Checks local translations first — no network call if the key is found:
// Resolves 'greeting' from local translations → 'Hello!' (toLang: en)
FonikaTranslatedText(
'greeting',
toLang: 'en',
style: const TextStyle(fontWeight: FontWeight.bold),
)
// Falls back to API for arbitrary text
FonikaTranslatedText(
'Bonjour le monde',
toLang: 'en',
)
FonikaSpeakButton
A ready-to-use speak icon button. Automatically routes to the 229Langues API for African languages and to the platform TTS for all other languages:
FonikaSpeakButton(
text: 'Bonjour le monde',
language: 'fr', // platform TTS
iconSize: 28,
)
FonikaSpeakButton(
text: 'È dó wɛ̀',
language: 'fon', // 229Langues API
iconSize: 28,
)
FonikaListenButton
A mic toggle button that uses the platform ASR engine. Fires onResult with the final transcript and onPartialResult during speech:
FonikaListenButton(
language: 'fr',
iconSize: 36,
onResult: (text) => print('Final: $text'),
onPartialResult: (text) => print('Partial: $text'),
onError: (e) => print('Error: $e'),
)
FonikaTranslationField
A TextField that automatically translates its content in real-time as the user types. Perfect for forms where users need live translation feedback:
FonikaTranslationField(
controller: myController,
toLang: 'en',
fromLang: 'fr',
decoration: const InputDecoration(
labelText: 'Type to translate',
border: OutlineInputBorder(),
),
debounceDuration: const Duration(milliseconds: 500),
)
The widget displays:
- Loading state while translating
- Translated text below the field in gray italic text
- Error messages if translation fails
- Custom translation display via
translationBuilderparameter
Translation
Priority chain
Every translate() call follows this order — no API call is made unless necessary:
- Local translations — keys loaded via
loadTranslations()or assets - Device cache — SharedPreferences cache (configurable TTL)
- 229Langues API — network call with automatic retry/backoff
Single text
final result = await fonika.translate(
'Bonjour, comment allez-vous ?',
fromLang: 'fr',
toLang: 'en',
);
print(result.translatedText); // Hello, how are you?
print(result.fromLocal); // true if from local/cache, false if from API
Batch
final result = await fonika.translateBatch(
['Bonjour', 'Merci', 'Au revoir'],
toLang: 'en',
);
for (final item in result.items) {
print('${item.originalText} → ${item.translatedText}');
}
Advanced options
await fonika.translate(
'text',
toLang: 'en',
skipLocal: false, // set true to bypass local translations
skipDeviceCache: false, // set true to bypass device cache
saveToCache: true, // set false to skip writing to device cache
);
Supported languages
final langs = await fonika.getLanguages();
print('${langs.total} languages available');
Local translations (offline-first)
Local translations are always checked first. A matching key never triggers a network call.
Load from a Map
fonika.loadTranslations({
'fr': {
'app': {'title': 'Mon Application'},
'greeting': 'Bonjour !',
},
'en': {
'app': {'title': 'My Application'},
'greeting': 'Hello!',
},
});
final t = await fonika.translate('greeting', toLang: 'fr');
print(t.translatedText); // Bonjour !
print(t.fromLocal); // true
Nested keys are flattened with dot-notation: app.title, auth.login.button, etc.
Load from Flutter assets
# pubspec.yaml
flutter:
assets:
- assets/i18n/fr.json
- assets/i18n/en.json
await fonika.init(assetPaths: [
'assets/i18n/fr.json',
'assets/i18n/en.json',
]);
// Or load additional locales after init:
await fonika.loadTranslationAssets(['assets/i18n/de.json']);
JSON format (flat or nested):
{
"greeting": "Bonjour !",
"auth": {
"login": "Se connecter",
"logout": "Se déconnecter"
}
}
Direct local lookup (no API fallback)
final value = fonika.localTranslate('auth.login', 'fr');
// Returns null if not found
Device cache
Translations returned by the API are automatically stored in SharedPreferences and reused on subsequent calls (until TTL expires). Configure the TTL in the constructor:
final fonika = FonikaTranslate(
apiToken: 'YOUR_TOKEN',
deviceCacheTtl: const Duration(days: 7), // default: 7 days
);
Cache management
// Number of entries currently cached
final count = await fonika.getDeviceCacheCount();
// Remove only expired entries
final removed = await fonika.evictExpiredDeviceCache();
// Wipe the entire device cache
await fonika.clearDeviceCache();
Retry and backoff
The client automatically retries failed requests on HTTP 5xx and 429 (rate limit) responses — useful for API cold starts:
final fonika = FonikaTranslate(
apiToken: 'YOUR_TOKEN',
maxRetries: 3, // attempts: 1 initial + 3 retries
);
Backoff delays: 1 s → 2 s → 4 s (exponential doubling).
TTS (Text-to-Speech)
Speak — auto routing
speak() automatically selects the right engine based on language:
// French → platform TTS (flutter_tts)
await fonika.speak('Bonjour le monde', 'fr');
// Fon → 229Langues API (plays via audioplayers)
await fonika.speak('È dó wɛ̀', 'fon');
African TTS languages: fon, yoruba, hausa
Get raw audio bytes (African languages only)
final tts = await fonika.tts('È dó wɛ̀', 'fon');
// tts.audioBytes — Uint8List (WAV for Fon, MP3 for Yoruba/Hausa)
// tts.audioFormat — 'wav' or 'mp3'
Platform TTS controls
await fonika.stopSpeaking();
await fonika.pauseSpeaking();
final langs = await fonika.getDeviceTtsLanguages();
ASR (Speech-to-Text)
Transcribe an audio file (African languages)
import 'dart:io';
final result = await fonika.transcribeAudio(
File('/path/to/audio.wav'),
'fon',
);
print(result.transcription);
print(result.duration); // seconds
African ASR languages: fon, adja, yoruba, hausa
Live microphone recognition (platform ASR)
await fonika.startListening(
'fr',
onResult: (text, isFinal) {
print('$text${isFinal ? ' [final]' : '...'}');
},
onDone: () => print('Done'),
listenFor: const Duration(seconds: 30),
pauseFor: const Duration(seconds: 3),
);
// Stop early
await fonika.stopListening();
// Check state
print(fonika.isListening);
PDF & file translation
import 'dart:io';
// Translate PDF — returns translated PDF bytes
final pdf = await fonika.translatePdf(File('doc.pdf'), toLang: 'fr');
// Translate PDF — returns JSON with translated text
final json = await fonika.translatePdf(
File('doc.pdf'),
toLang: 'en',
returnJson: true,
);
print(json.translatedText);
// Extract text from PDF (no translation)
final extract = await fonika.extractPdfText(File('doc.pdf'));
print('${extract.totalPages} pages, ${extract.totalCharacters} chars');
// Translate a .txt file
final txt = await fonika.translateTxtFile(File('doc.txt'), toLang: 'en');
API cache & health
// Server-side cache stats
final stats = await fonika.getCacheStats();
print('${stats.totalCached} cached, hit rate: ${stats.hitRate}');
// Clear server-side cache
await fonika.clearApiCache();
// API health
final health = await fonika.healthCheck();
print(health.isHealthy); // true
Error handling
The package provides specific exception types for granular error handling:
try {
await fonika.translate('Hello', toLang: 'fr');
} on FonikaInitException catch (e) {
// fonika.init() was not called
print('Initialize first: ${e.message}');
} on FonikaAuthException catch (e) {
// Missing or invalid API token
print('Auth error: ${e.message}');
} on LanguageNotSupportedException catch (e) {
// Language code is not supported
print('Language "${e.languageCode}" not supported');
} on FonikaNetworkException catch (e) {
// Network timeout or connection issues
print('Network error (${e.statusCode}): ${e.message}');
} on FonikaAsrException catch (e) {
// Speech-to-text failed
print('ASR error: ${e.message}');
} on FonikaTtsException catch (e) {
// Text-to-speech failed
print('TTS error: ${e.message}');
} on FonikaPdfException catch (e) {
// PDF operation failed
print('PDF error: ${e.message}');
} on FonikaApiException catch (e) {
// Generic API error
print('API error ${e.statusCode}: ${e.message}');
} on FonikaException catch (e) {
// Catch all fonika exceptions
print('Fonika error: ${e.message}');
} catch (e) {
// Unexpected error
print('Unexpected error: $e');
}
Exception hierarchy:
FonikaException(base class)FonikaInitException— client not initializedFonikaAuthException— authentication failedLanguageNotSupportedException— language not supportedFonikaNetworkException— network/timeout issuesFonikaAsrException— speech-to-text errorsFonikaTtsException— text-to-speech errorsFonikaPdfException— PDF operation errorsFonikaApiException— generic API errors (5xx, 4xx, etc)
Advanced: LocalTranslationsService
fonika.local.merge('fr', {'new.key': 'nouvelle valeur'});
fonika.local.unload('de');
print(fonika.local.loadedLanguages);
print(fonika.local.totalKeys);
Publisher
229Langues — open-source tools for African language processing.
- Author: AWADEME Finanfa Ronaldo
- Contact: awademeronaldoo@gmail.com
- GitHub: Ronaldo-F-dev