fonika_translate 0.1.1 copy "fonika_translate: ^0.1.1" to clipboard
fonika_translate: ^0.1.1 copied to clipboard

Multilingual translation, TTS and ASR for Flutter — African languages (Fon, Yoruba, Hausa, Adja, Bariba) + 100 world languages. Offline-first with local translations support and automatic language detection.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:fonika_translate/fonika_translate.dart';

late final FonikaTranslate fonika;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await dotenv.load(fileName: '.env');

  fonika = FonikaTranslate(
    apiToken: dotenv.env['TOKEN'],
    maxRetries: 3,
    deviceCacheTtl: const Duration(days: 7),
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // FonikaProvider rend le client disponible dans tout l'arbre de widgets
    return FonikaProvider(
      client: fonika,
      child: MaterialApp(
        title: 'fonika_translate 0.1.0',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1B6CA8)),
          useMaterial3: true,
        ),
        home: const _InitWrapper(),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Wrapper d'initialisation — affiche un loader pendant fonika.init()
class _InitWrapper extends StatefulWidget {
  const _InitWrapper();

  @override
  State<_InitWrapper> createState() => _InitWrapperState();
}

class _InitWrapperState extends State<_InitWrapper> {
  bool _ready = false;

  @override
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    await fonika.init();

    // Traductions locales — priorité absolue sur tout (cache + API)
    fonika.loadTranslations({
      'fr': {
        'app': {'title': 'Démo fonika_translate'},
        'greeting': 'Bonjour depuis les traductions locales !',
        'farewell': 'Au revoir !',
      },
      'en': {
        'app': {'title': 'fonika_translate Demo'},
        'greeting': 'Hello from local translations!',
        'farewell': 'Goodbye!',
      },
    });

    if (mounted) setState(() => _ready = true);
  }

  @override
  Widget build(BuildContext context) {
    if (!_ready) {
      return const Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }
    return const DemoHome();
  }
}

// ---------------------------------------------------------------------------
// App principale avec 3 onglets
class DemoHome extends StatelessWidget {
  const DemoHome({super.key});

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('fonika_translate'),
          backgroundColor: Theme.of(context).colorScheme.primary,
          foregroundColor: Colors.white,
          bottom: const TabBar(
            labelColor: Colors.white,
            unselectedLabelColor: Colors.white70,
            indicatorColor: Colors.white,
            tabs: [
              Tab(icon: Icon(Icons.translate), text: 'Traduction'),
              Tab(icon: Icon(Icons.record_voice_over), text: 'Voix'),
              Tab(icon: Icon(Icons.storage), text: 'Cache'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            _TranslationTab(),
            _VoiceTab(),
            _CacheTab(),
          ],
        ),
      ),
    );
  }
}

// ===========================================================================
// ONGLET 1 — Traduction
// ===========================================================================
class _TranslationTab extends StatefulWidget {
  const _TranslationTab();

  @override
  State<_TranslationTab> createState() => _TranslationTabState();
}

class _TranslationTabState extends State<_TranslationTab> {
  final _controller = TextEditingController(text: 'Bonjour, comment allez-vous ?');
  String _result = '';
  String _source = '';
  bool _fromLocal = false;
  bool _loading = false;

  Future<void> _translate() async {
    setState(() { _loading = true; _result = ''; });
    try {
      final r = await fonika.translate(
        _controller.text,
        fromLang: 'auto',
        toLang: 'en',
      );
      setState(() {
        _loading = false;
        _result = r.translatedText;
        _source = r.fromLocal ? 'local' : 'API';
        _fromLocal = r.fromLocal;
      });
    } on FonikaNetworkException catch (e) {
      _showError('Network error: ${e.message}');
    } on FonikaAuthException catch (e) {
      _showError('Auth error: ${e.message}');
    } on FonikaException catch (e) {
      _showError('Fonika error: ${e.message}');
    } catch (e) {
      _showError('Unexpected error: $e');
    }
  }

  void _showError(String message) {
    setState(() {
      _loading = false;
      _result = '';
      _source = 'ERROR';
      _fromLocal = false;
    });
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: Colors.red,
        duration: const Duration(seconds: 4),
      ),
    );
  }

  Future<void> _translateLocalKey() async {
    setState(() { _loading = true; _result = ''; });
    // "greeting" est dans les traductions locales → zéro appel réseau
    final r = await fonika.translate('greeting', toLang: 'fr');
    setState(() {
      _loading = false;
      _result = r.translatedText;
      _source = 'local';
      _fromLocal = true;
    });
  }

  Future<void> _translateBatch() async {
    setState(() { _loading = true; _result = ''; });
    final r = await fonika.translateBatch(
      ['greeting', 'Merci beaucoup', 'farewell'],
      toLang: 'en',
    );
    setState(() {
      _loading = false;
      _result = r.items
          .map((i) => '${i.originalText}\n  → ${i.translatedText}')
          .join('\n\n');
      _source = 'batch (local + API)';
      _fromLocal = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // --- Widget FonikaTranslatedText en action ---
          _section('Widget FonikaTranslatedText'),
          const Text('Ces textes sont traduits automatiquement par le widget :',
              style: TextStyle(color: Colors.grey)),
          const SizedBox(height: 8),
          _translatedCard('app.title', 'fr'),
          _translatedCard('greeting', 'en'),
          _translatedCard('Bonjour le monde', 'en'),

          const SizedBox(height: 24),

          // --- Traduction manuelle ---
          _section('Traduction manuelle'),
          TextField(
            controller: _controller,
            decoration: const InputDecoration(
              labelText: 'Texte à traduire',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            children: [
              ElevatedButton.icon(
                onPressed: _loading ? null : _translate,
                icon: const Icon(Icons.cloud),
                label: const Text('Traduire (auto)'),
              ),
              ElevatedButton.icon(
                onPressed: _loading ? null : _translateLocalKey,
                icon: const Icon(Icons.offline_bolt),
                label: const Text('Clé locale'),
              ),
              ElevatedButton.icon(
                onPressed: _loading ? null : _translateBatch,
                icon: const Icon(Icons.list),
                label: const Text('Batch'),
              ),
            ],
          ),
          const SizedBox(height: 12),
          if (_loading) const LinearProgressIndicator(),
          if (_result.isNotEmpty)
            _resultCard(
              title: 'Source : $_source${_fromLocal ? ' [offline]' : ' [API]'}',
              body: _result,
              color: _fromLocal ? Colors.green.shade50 : Colors.blue.shade50,
            ),

          const SizedBox(height: 24),

          // --- Widget FonikaTranslationField (traduction en direct) ---
          _section('FonikaTranslationField (Traduction en direct)'),
          const Text('Tapez pour voir la traduction s\'afficher en temps réel :',
              style: TextStyle(color: Colors.grey)),
          const SizedBox(height: 12),
          FonikaTranslationField(
            controller: TextEditingController(text: 'Bonjour'),
            toLang: 'en',
            fromLang: 'fr',
            decoration: const InputDecoration(
              labelText: 'Texte (traduit en temps réel)',
              border: OutlineInputBorder(),
              prefixIcon: Icon(Icons.edit),
            ),
            debounceDuration: const Duration(milliseconds: 500),
          ),
        ],
      ),
    );
  }

  Widget _translatedCard(String key, String toLang) {
    return Card(
      margin: const EdgeInsets.only(bottom: 6),
      child: ListTile(
        dense: true,
        leading: const Icon(Icons.translate, size: 18),
        title: FonikaTranslatedText(
          key,
          toLang: toLang,
          style: const TextStyle(fontWeight: FontWeight.w500),
        ),
        subtitle: Text('"$key" → $toLang',
            style: const TextStyle(fontSize: 11, color: Colors.grey)),
      ),
    );
  }
}

// ===========================================================================
// ONGLET 2 — Voix
// ===========================================================================
class _VoiceTab extends StatefulWidget {
  const _VoiceTab();

  @override
  State<_VoiceTab> createState() => _VoiceTabState();
}

class _VoiceTabState extends State<_VoiceTab> {
  String _sttResult = 'Appuie sur le micro pour parler...';

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // --- TTS via widgets ---
          _section('TTS — Widget FonikaSpeakButton'),
          const Text(
            'Appuie sur l\'icône pour entendre la synthèse vocale.\n'
            'Langues africaines → API  |  Autres → plateforme',
            style: TextStyle(color: Colors.grey, fontSize: 13),
          ),
          const SizedBox(height: 12),
          ..._ttsItems.map((item) => Card(
            margin: const EdgeInsets.only(bottom: 8),
            child: ListTile(
              leading: FonikaSpeakButton(
                text: item['text']!,
                language: item['lang']!,
                iconSize: 28,
              ),
              title: Text(item['text']!,
                  style: const TextStyle(fontWeight: FontWeight.w500)),
              subtitle: Text(
                '${item['label']} — ${item['engine']}',
                style: const TextStyle(fontSize: 12, color: Colors.grey),
              ),
            ),
          )),

          const SizedBox(height: 24),

          // --- ASR via widget ---
          _section('ASR — Widget FonikaListenButton'),
          const Text(
            'Appuie sur le micro et parle en français.\nUtilise la reconnaissance vocale de la plateforme.',
            style: TextStyle(color: Colors.grey, fontSize: 13),
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              FonikaListenButton(
                language: 'fr',
                iconSize: 36,
                onResult: (text) =>
                    setState(() => _sttResult = '✓ "$text"'),
                onPartialResult: (text) =>
                    setState(() => _sttResult = '... $text'),
                onError: (e) =>
                    setState(() => _sttResult = 'Erreur: $e'),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Text(_sttResult,
                    style: const TextStyle(fontSize: 15)),
              ),
            ],
          ),
        ],
      ),
    );
  }

  static const _ttsItems = [
    {'text': 'Bonjour le monde', 'lang': 'fr', 'label': 'Français', 'engine': 'flutter_tts'},
    {'text': 'Hello world', 'lang': 'en', 'label': 'English', 'engine': 'flutter_tts'},
    {'text': 'Hola mundo', 'lang': 'es', 'label': 'Español', 'engine': 'flutter_tts'},
    {'text': 'È dó wɛ̀', 'lang': 'fon', 'label': 'Fon [africain]', 'engine': '229Langues API'},
    {'text': 'Ẹ káàárọ̀', 'lang': 'yoruba', 'label': 'Yoruba [africain]', 'engine': '229Langues API'},
  ];
}

// ===========================================================================
// ONGLET 3 — Cache & infos
// ===========================================================================
class _CacheTab extends StatefulWidget {
  const _CacheTab();

  @override
  State<_CacheTab> createState() => _CacheTabState();
}

class _CacheTabState extends State<_CacheTab> {
  int _deviceCacheCount = 0;
  String _healthStatus = '—';
  String _log = '';
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => _refreshCount());
  }

  Future<void> _refreshCount() async {
    final count = await fonika.getDeviceCacheCount();
    if (mounted) setState(() => _deviceCacheCount = count);
  }

  Future<void> _testCacheHit() async {
    setState(() { _loading = true; _log = ''; });
    final sw1 = Stopwatch()..start();
    final r1 = await fonika.translate('Bonjour', fromLang: 'fr', toLang: 'en');
    sw1.stop();

    final sw2 = Stopwatch()..start();
    final r2 = await fonika.translate('Bonjour', fromLang: 'fr', toLang: 'en');
    sw2.stop();

    await _refreshCount();
    setState(() {
      _loading = false;
      _log =
          'Requête 1 : ${sw1.elapsedMilliseconds}ms — ${r1.fromLocal ? "local" : "API"}\n'
          'Requête 2 : ${sw2.elapsedMilliseconds}ms — ${r2.fromLocal ? "local" : "cache device ✅"}';
    });
  }

  Future<void> _clearDeviceCache() async {
    await fonika.clearDeviceCache();
    await _refreshCount();
    setState(() => _log = 'Cache device vidé.');
  }

  Future<void> _evictExpired() async {
    final removed = await fonika.evictExpiredDeviceCache();
    await _refreshCount();
    setState(() => _log = '$removed entrée(s) expirée(s) supprimée(s).');
  }

  Future<void> _checkHealth() async {
    setState(() { _loading = true; });
    final s = await fonika.healthCheck();
    setState(() {
      _loading = false;
      _healthStatus = '${s.status} | DB: ${s.database}';
    });
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          _section('Cache device (SharedPreferences)'),
          _infoTile(Icons.cached, 'Entrées en cache', '$_deviceCacheCount'),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              ElevatedButton.icon(
                onPressed: _loading ? null : _testCacheHit,
                icon: const Icon(Icons.speed),
                label: const Text('Tester le cache hit'),
              ),
              OutlinedButton.icon(
                onPressed: _loading ? null : _evictExpired,
                icon: const Icon(Icons.auto_delete),
                label: const Text('Purger expirés'),
              ),
              OutlinedButton.icon(
                onPressed: _loading ? null : _clearDeviceCache,
                icon: const Icon(Icons.delete_outline),
                label: const Text('Vider tout'),
                style: OutlinedButton.styleFrom(
                    foregroundColor: Colors.red),
              ),
            ],
          ),
          if (_loading) ...[
            const SizedBox(height: 8),
            const LinearProgressIndicator(),
          ],
          if (_log.isNotEmpty) ...[
            const SizedBox(height: 12),
            _resultCard(title: 'Résultat', body: _log,
                color: Colors.orange.shade50),
          ],

          const SizedBox(height: 24),
          _section('Retry automatique'),
          const Text(
            'Le client est configuré avec maxRetries: 3.\n'
            'En cas d\'erreur 5xx ou cold start HuggingFace,\n'
            'la requête est relancée avec backoff : 1s → 2s → 4s.',
            style: TextStyle(color: Colors.grey, fontSize: 13),
          ),
          const SizedBox(height: 8),
          _infoTile(Icons.refresh, 'Max retries', '3'),
          _infoTile(Icons.timelapse, 'Backoff', '1s → 2s → 4s'),
          _infoTile(Icons.wifi_off, 'Codes retryables', '5xx, 429'),

          const SizedBox(height: 24),
          _section('Santé de l\'API'),
          _infoTile(Icons.monitor_heart, 'Status', _healthStatus),
          const SizedBox(height: 8),
          ElevatedButton.icon(
            onPressed: _loading ? null : _checkHealth,
            icon: const Icon(Icons.monitor_heart),
            label: const Text('Vérifier l\'API'),
          ),
        ],
      ),
    );
  }

  Widget _infoTile(IconData icon, String label, String value) {
    return ListTile(
      dense: true,
      leading: Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary),
      title: Text(label),
      trailing: Text(value,
          style: TextStyle(
              fontWeight: FontWeight.bold,
              color: Theme.of(context).colorScheme.primary)),
    );
  }
}

// ===========================================================================
// Helpers communs
// ===========================================================================
Widget _section(String title) => Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Text(title,
          style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
    );

Widget _resultCard(
    {required String title, required String body, required Color color}) {
  return Container(
    width: double.infinity,
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(
      color: color,
      borderRadius: BorderRadius.circular(8),
      border: Border.all(color: Colors.black12),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(title,
            style: const TextStyle(
                fontSize: 11, fontWeight: FontWeight.bold, color: Colors.grey)),
        const SizedBox(height: 4),
        Text(body, style: const TextStyle(fontFamily: 'monospace', fontSize: 13)),
      ],
    ),
  );
}
2
likes
135
points
196
downloads

Documentation

API reference

Publisher

verified publisher229langues.bj

Weekly Downloads

Multilingual translation, TTS and ASR for Flutter — African languages (Fon, Yoruba, Hausa, Adja, Bariba) + 100 world languages. Offline-first with local translations support and automatic language detection.

Homepage
Repository (GitHub)
View/report issues

Topics

#translation #localization #tts #speech-recognition #african-languages

License

MIT (license)

Dependencies

audioplayers, flutter, flutter_tts, http, mime, shared_preferences, speech_to_text

More

Packages that depend on fonika_translate