← Back to Blog

Flutter Provider Localization: Complete Guide to i18n with Provider Pattern

flutterproviderlocalizationi18nstate-managementreactive

Flutter Provider Localization: Simple State Management for Multi-Language Apps

Provider is Flutter's recommended simple state management solution. This guide shows you how to implement language switching with Provider - no complex setup required.

Why Use Provider for Localization?

Provider is perfect for localization because:

  • Simple API - Easy to understand and implement
  • Lightweight - Minimal boilerplate code
  • Built-in Flutter support - Recommended by the Flutter team
  • Scoped access - Access locale anywhere in the widget tree

Project Setup

1. Add Dependencies

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  provider: ^6.1.1
  shared_preferences: ^2.2.2
  intl: ^0.18.1

2. Configure l10n.yaml

# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

3. Create ARB Files

lib/l10n/app_en.arb:

{
  "@@locale": "en",
  "appTitle": "My App",
  "welcome": "Welcome",
  "settings": "Settings",
  "language": "Language",
  "selectLanguage": "Select Language",
  "english": "English",
  "spanish": "Spanish",
  "french": "French",
  "german": "German",
  "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
  "@itemCount": {
    "placeholders": {
      "count": {"type": "int"}
    }
  }
}

lib/l10n/app_es.arb:

{
  "@@locale": "es",
  "appTitle": "Mi Aplicación",
  "welcome": "Bienvenido",
  "settings": "Configuración",
  "language": "Idioma",
  "selectLanguage": "Seleccionar Idioma",
  "english": "Inglés",
  "spanish": "Español",
  "french": "Francés",
  "german": "Alemán",
  "itemCount": "{count, plural, =0{Sin artículos} =1{1 artículo} other{{count} artículos}}"
}

Creating the Locale Provider

Basic LocaleProvider

// lib/providers/locale_provider.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class LocaleProvider extends ChangeNotifier {
  static const String _localeKey = 'selected_locale';

  Locale _locale = const Locale('en');
  bool _isLoading = true;

  Locale get locale => _locale;
  bool get isLoading => _isLoading;

  // Supported locales
  static const List<Locale> supportedLocales = [
    Locale('en'),
    Locale('es'),
    Locale('fr'),
    Locale('de'),
  ];

  LocaleProvider() {
    _loadSavedLocale();
  }

  Future<void> _loadSavedLocale() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final languageCode = prefs.getString(_localeKey);

      if (languageCode != null) {
        _locale = Locale(languageCode);
      }
    } catch (e) {
      debugPrint('Error loading locale: $e');
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

  Future<void> setLocale(Locale locale) async {
    if (!supportedLocales.contains(locale)) return;
    if (_locale == locale) return;

    _locale = locale;
    notifyListeners();

    try {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString(_localeKey, locale.languageCode);
    } catch (e) {
      debugPrint('Error saving locale: $e');
    }
  }

  void clearLocale() async {
    _locale = const Locale('en');
    notifyListeners();

    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_localeKey);
  }
}

Provider with Country Code Support

// lib/providers/locale_provider.dart
class LocaleProvider extends ChangeNotifier {
  static const String _languageKey = 'language_code';
  static const String _countryKey = 'country_code';

  Locale _locale = const Locale('en', 'US');

  Locale get locale => _locale;

  static const List<Locale> supportedLocales = [
    Locale('en', 'US'),
    Locale('en', 'GB'),
    Locale('es', 'ES'),
    Locale('es', 'MX'),
    Locale('fr', 'FR'),
    Locale('de', 'DE'),
    Locale('pt', 'BR'),
    Locale('zh', 'CN'),
  ];

  LocaleProvider() {
    _loadSavedLocale();
  }

  Future<void> _loadSavedLocale() async {
    final prefs = await SharedPreferences.getInstance();
    final languageCode = prefs.getString(_languageKey);
    final countryCode = prefs.getString(_countryKey);

    if (languageCode != null) {
      _locale = Locale(languageCode, countryCode);
      notifyListeners();
    }
  }

  Future<void> setLocale(Locale locale) async {
    if (_locale == locale) return;

    _locale = locale;
    notifyListeners();

    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_languageKey, locale.languageCode);
    if (locale.countryCode != null) {
      await prefs.setString(_countryKey, locale.countryCode!);
    } else {
      await prefs.remove(_countryKey);
    }
  }
}

Setting Up the App

Main App Configuration

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'providers/locale_provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => LocaleProvider(),
      child: const MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return Consumer<LocaleProvider>(
      builder: (context, localeProvider, _) {
        // Show loading while locale is being loaded
        if (localeProvider.isLoading) {
          return const MaterialApp(
            home: Scaffold(
              body: Center(child: CircularProgressIndicator()),
            ),
          );
        }

        return MaterialApp(
          title: 'Flutter Provider Localization',

          // Localization configuration
          locale: localeProvider.locale,
          supportedLocales: LocaleProvider.supportedLocales,
          localizationsDelegates: const [
            AppLocalizations.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],

          home: const HomePage(),
        );
      },
    );
  }
}

Using Multiple Providers

// lib/main.dart
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => LocaleProvider()),
        ChangeNotifierProvider(create: (_) => ThemeProvider()),
        ChangeNotifierProvider(create: (_) => AuthProvider()),
      ],
      child: const MyApp(),
    ),
  );
}

Accessing Translations

In Widgets

// lib/pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.appTitle),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              l10n.welcome,
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            const SizedBox(height: 16),
            Text(l10n.itemCount(5)),
          ],
        ),
      ),
    );
  }
}

Extension for Cleaner Access

// lib/extensions/context_extensions.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import '../providers/locale_provider.dart';

extension LocalizationExtension on BuildContext {
  AppLocalizations get l10n => AppLocalizations.of(this)!;

  LocaleProvider get localeProvider => read<LocaleProvider>();

  Locale get currentLocale => watch<LocaleProvider>().locale;

  void setLocale(Locale locale) {
    read<LocaleProvider>().setLocale(locale);
  }
}

// Usage:
Text(context.l10n.welcome)
context.setLocale(const Locale('es'));

Language Selector Widget

Dropdown Selector

// lib/widgets/language_dropdown.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../providers/locale_provider.dart';

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Consumer<LocaleProvider>(
      builder: (context, provider, _) {
        return DropdownButton<Locale>(
          value: provider.locale,
          onChanged: (Locale? newLocale) {
            if (newLocale != null) {
              provider.setLocale(newLocale);
            }
          },
          items: LocaleProvider.supportedLocales.map((locale) {
            return DropdownMenuItem<Locale>(
              value: locale,
              child: Text(_getLanguageName(locale, l10n)),
            );
          }).toList(),
        );
      },
    );
  }

  String _getLanguageName(Locale locale, AppLocalizations l10n) {
    switch (locale.languageCode) {
      case 'en':
        return l10n.english;
      case 'es':
        return l10n.spanish;
      case 'fr':
        return l10n.french;
      case 'de':
        return l10n.german;
      default:
        return locale.languageCode;
    }
  }
}

ListTile Selector

// lib/widgets/language_list_tile.dart
class LanguageListTile extends StatelessWidget {
  const LanguageListTile({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Consumer<LocaleProvider>(
      builder: (context, provider, _) {
        return ListTile(
          leading: const Icon(Icons.language),
          title: Text(l10n.language),
          subtitle: Text(_getLanguageName(provider.locale)),
          trailing: const Icon(Icons.chevron_right),
          onTap: () => _showLanguageDialog(context, provider),
        );
      },
    );
  }

  void _showLanguageDialog(BuildContext context, LocaleProvider provider) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(AppLocalizations.of(context)!.selectLanguage),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: LocaleProvider.supportedLocales.map((locale) {
            final isSelected = locale == provider.locale;
            return ListTile(
              title: Text(_getLanguageName(locale)),
              trailing: isSelected ? const Icon(Icons.check) : null,
              onTap: () {
                provider.setLocale(locale);
                Navigator.pop(context);
              },
            );
          }).toList(),
        ),
      ),
    );
  }

  String _getLanguageName(Locale locale) {
    const names = {
      'en': 'English',
      'es': 'Español',
      'fr': 'Français',
      'de': 'Deutsch',
    };
    return names[locale.languageCode] ?? locale.languageCode;
  }
}

Settings Page Example

// lib/pages/settings_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../providers/locale_provider.dart';

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.settings),
      ),
      body: ListView(
        children: [
          // Language Section
          const _SectionHeader(title: 'Language'),
          Consumer<LocaleProvider>(
            builder: (context, provider, _) {
              return Column(
                children: LocaleProvider.supportedLocales.map((locale) {
                  final isSelected = locale == provider.locale;
                  return RadioListTile<Locale>(
                    title: Text(_getLanguageName(locale)),
                    subtitle: Text(_getNativeName(locale)),
                    value: locale,
                    groupValue: provider.locale,
                    onChanged: (value) {
                      if (value != null) {
                        provider.setLocale(value);
                      }
                    },
                    secondary: isSelected
                        ? const Icon(Icons.check, color: Colors.green)
                        : null,
                  );
                }).toList(),
              );
            },
          ),
        ],
      ),
    );
  }

  String _getLanguageName(Locale locale) {
    const names = {
      'en': 'English',
      'es': 'Spanish',
      'fr': 'French',
      'de': 'German',
    };
    return names[locale.languageCode] ?? locale.languageCode;
  }

  String _getNativeName(Locale locale) {
    const names = {
      'en': 'English',
      'es': 'Español',
      'fr': 'Français',
      'de': 'Deutsch',
    };
    return names[locale.languageCode] ?? locale.languageCode;
  }
}

class _SectionHeader extends StatelessWidget {
  final String title;

  const _SectionHeader({required this.title});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
      child: Text(
        title,
        style: TextStyle(
          fontSize: 14,
          fontWeight: FontWeight.w500,
          color: Theme.of(context).colorScheme.primary,
        ),
      ),
    );
  }
}

System Locale Detection

// lib/providers/locale_provider.dart
class LocaleProvider extends ChangeNotifier {
  // ...

  Future<void> _loadSavedLocale() async {
    final prefs = await SharedPreferences.getInstance();
    final savedLanguageCode = prefs.getString(_localeKey);

    if (savedLanguageCode != null) {
      // Use saved preference
      _locale = Locale(savedLanguageCode);
    } else {
      // Fall back to system locale
      _locale = _getSystemLocale();
    }

    _isLoading = false;
    notifyListeners();
  }

  Locale _getSystemLocale() {
    final systemLocale = WidgetsBinding.instance.platformDispatcher.locale;

    // Check if system locale is supported
    for (final supported in supportedLocales) {
      if (supported.languageCode == systemLocale.languageCode) {
        return supported;
      }
    }

    // Default to English
    return const Locale('en');
  }

  // Follow system locale
  Future<void> useSystemLocale() async {
    _locale = _getSystemLocale();
    notifyListeners();

    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_localeKey);
  }
}

Testing with Provider

// test/locale_provider_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/providers/locale_provider.dart';

void main() {
  group('LocaleProvider', () {
    test('initial locale is English', () {
      final provider = LocaleProvider();
      expect(provider.locale.languageCode, 'en');
    });

    test('setLocale changes the locale', () async {
      final provider = LocaleProvider();

      await provider.setLocale(const Locale('es'));

      expect(provider.locale.languageCode, 'es');
    });

    test('notifies listeners on locale change', () async {
      final provider = LocaleProvider();
      var notified = false;

      provider.addListener(() => notified = true);
      await provider.setLocale(const Locale('fr'));

      expect(notified, true);
    });
  });
}

Widget Testing

// test/widgets/language_dropdown_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() {
  testWidgets('LanguageDropdown changes locale', (tester) async {
    final provider = LocaleProvider();

    await tester.pumpWidget(
      ChangeNotifierProvider.value(
        value: provider,
        child: Consumer<LocaleProvider>(
          builder: (context, provider, _) {
            return MaterialApp(
              locale: provider.locale,
              supportedLocales: LocaleProvider.supportedLocales,
              localizationsDelegates: [
                AppLocalizations.delegate,
                GlobalMaterialLocalizations.delegate,
                GlobalWidgetsLocalizations.delegate,
              ],
              home: Scaffold(body: LanguageDropdown()),
            );
          },
        ),
      ),
    );

    // Open dropdown
    await tester.tap(find.byType(DropdownButton<Locale>));
    await tester.pumpAndSettle();

    // Select Spanish
    await tester.tap(find.text('Español').last);
    await tester.pumpAndSettle();

    expect(provider.locale.languageCode, 'es');
  });
}

Best Practices

1. Initialize Early

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Pre-load locale before app starts
  final localeProvider = LocaleProvider();
  await localeProvider.initialize();

  runApp(
    ChangeNotifierProvider.value(
      value: localeProvider,
      child: const MyApp(),
    ),
  );
}

2. Handle Loading State

Consumer<LocaleProvider>(
  builder: (context, provider, child) {
    if (provider.isLoading) {
      return const SplashScreen();
    }
    return child!;
  },
  child: const MainApp(),
)

3. Avoid Rebuilding Entire App

// Good - Only locale-dependent widgets rebuild
Selector<LocaleProvider, Locale>(
  selector: (_, provider) => provider.locale,
  builder: (context, locale, child) {
    return Text(AppLocalizations.of(context)!.welcome);
  },
)

Conclusion

Provider makes Flutter localization simple and maintainable. The pattern shown here:

  • Persists user language preference
  • Falls back to system locale
  • Provides clean widget access
  • Is easy to test

For more complex apps with many features, consider Riverpod which offers additional features like compile-time safety and better testing support.

Need help managing your ARB translations? FlutterLocalisation provides a complete platform for managing Flutter localization files with team collaboration and AI-powered translations.