Skip to main content
All guides
Guide

Over-the-air translation updates in Flutter

Flutter's built-in localization compiles your text into the app, so every change needs a new release. This guide loads your litei18n translations when the app starts instead, with the bundled copy as a fallback.

Setup steps

  1. Create a release

    In litei18n, commit your translations and create a release on the Releases page (Starter plan and above). Copy your project ID from the release URL shown there. Export your translations as JSON too, and add them to your app as the bundled copy.

  2. Load the release when the app starts

    Add the http package to pubspec.yaml, put your exported JSON files in assets/i18n/ and list that folder under flutter: assets: in pubspec.yaml, or the app cannot read them. This class reads the bundled file first, then replaces its values with the latest release when the network allows.

    lib/translations.dart
    import 'dart:convert';
    
    import 'package:flutter/services.dart' show rootBundle;
    import 'package:http/http.dart' as http;
    
    const projectId = 'YOUR_PROJECT_ID';
    const releases =
        'https://ttksowxkfdohtprtfarf.supabase.co/storage/v1/object/public/releases/locales';
    
    class Translations {
      Translations(this._values);
    
      final Map<String, String> _values;
    
      String t(String key) => _values[key] ?? key;
    
      static Future<Translations> load(String locale) async {
        // The copy bundled with the app: works offline and on first launch.
        final values = Map<String, String>.from(
          jsonDecode(await rootBundle.loadString('assets/i18n/$locale.json')),
        );
        try {
          final res = await http
              .get(Uri.parse('$releases/$projectId/latest/$locale.json'))
              .timeout(const Duration(seconds: 5));
          if (res.statusCode == 200) {
            values.addAll(Map<String, String>.from(jsonDecode(utf8.decode(res.bodyBytes))));
          }
        } catch (_) {
          // Offline or slow: keep the bundled copy.
        }
        return Translations(values);
      }
    }
  3. Use the strings

    Load the translations before runApp and pass them down, or keep them in the state management you already use. A missing key shows its own name, so it is easy to spot.

    lib/main.dart
    import 'package:flutter/material.dart';
    
    import 'translations.dart';
    
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      final tr = await Translations.load('es');
      runApp(MyApp(tr: tr));
    }
    
    // In a widget:
    Text(tr.t('home.title'))
  4. Change text without a new build

    Edit a translation, commit it and create a new release. The latest URL now serves the new text, and your app picks it up the next time it loads translations. No new build, no app store review.

Good to know

  • Release files are cached on a CDN, so a new release can take a few minutes to show everywhere.
  • To pin an app version to one release, use the versioned URL (…/v3/es.json) instead of …/latest/es.json.
  • To keep the downloaded text offline, save the response with shared_preferences or path_provider and read it before the bundled file next time.

Start shipping globally in 5 minutes

Join developers who ship their apps in every language with litei18n.

Free plan available · No credit card required · Set up in under 5 minutes