Flutter Localization: Dynamic Language Switching
Building cross-cultural applications often requires supporting multiple languages. Flutter, with its robust internationalization (i18n) and localization (l10n) capabilities, makes it straightforward to adapt your app for different locales. While static localization ensures your app loads with the correct language based on device settings, dynamic language switching empowers users to change the app's language at runtime, enhancing user experience and accessibility. This article will guide you through implementing dynamic language switching in Flutter.
Understanding Flutter Localization Fundamentals
Before diving into dynamic switching, it's essential to understand the basics of Flutter localization. Flutter relies on the flutter_localizations package and the intl package for i18n. The process typically involves:
- Defining translatable strings in Application Resource Bundle (.arb) files.
- Using Flutter's `gen_l10n` tool to generate Dart code from these .arb files.
- Integrating the generated `AppLocalizations` class into your `MaterialApp`.
Setting Up Initial Localization
First, ensure your project is configured for localization.
1. Add Dependencies
Add the necessary dependencies to your
pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_localizations: # Add this
sdk: flutter
intl: ^0.18.0 # Add this
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
intl_translation: ^0.18.0+1 # Add this for older workflows, but `gen_l10n` is preferred
flutter:
uses-material-design: true
generate: true # Enable automatic generation of localizations
2. Configure l10n.yaml
l10n.yamlCreate a file named
l10n.yaml in the root of your project:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
3. Create .arb Files
Create a directory
lib/l10n and add your localization files. For example:
lib/l10n/app_en.arb (English):
{
"helloWorld": "Hello World!",
"currentLanguage": "Current Language: {language}",
"@currentLanguage": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"changeLanguage": "Change Language"
}
lib/l10n/app_id.arb (Indonesian):
{
"helloWorld": "Halo Dunia!",
"currentLanguage": "Bahasa Saat Ini: {language}",
"@currentLanguage": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"changeLanguage": "Ganti Bahasa"
}
4. Generate Localization Code
Run
flutter gen-l10n (or simply `flutter run`, as `generate: true` in `pubspec.yaml` will trigger it). This generates lib/generated/l10n.dart and related files, including the `AppLocalizations` class.
5. Integrate into MaterialApp
MaterialAppSet up your `MaterialApp` to use the generated localizations:
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:your_app_name/generated/l10n.dart'; // Adjust import path
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Localization Demo',
supportedLocales: L10n.all, // L10n.all is a static list of supported locales
localizationsDelegates: [
S.delegate, // Generated delegate
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final s = S.of(context); // Access localized strings
return Scaffold(
appBar: AppBar(
title: Text('Localization Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(s.helloWorld),
Text(s.currentLanguage(Localizations.localeOf(context).languageCode)),
],
),
),
);
}
}
At this point, your app will load with the language matching the device's locale (if supported).
Implementing Dynamic Language Switching
To allow users to change the language at runtime, we need a mechanism to update the `locale` property of `MaterialApp` and rebuild the widget tree. A common and effective pattern is to use a state management solution, such as `Provider` (which leverages `ChangeNotifier`), to manage the current locale.
1. Create a `LocaleProvider`
This class will hold the current locale and notify listeners when it changes.
// lib/locale_provider.dart
import 'package:flutter/material.dart';
class LocaleProvider extends ChangeNotifier {
Locale? _locale;
Locale? get locale => _locale;
void setLocale(Locale newLocale) {
if (!L10n.all.contains(newLocale)) return; // Ensure locale is supported
_locale = newLocale;
notifyListeners();
}
void clearLocale() {
_locale = null;
notifyListeners();
}
}
2. Wrap MyApp
with ChangeNotifierProvider
MyAppChangeNotifierProviderUse `Provider` (add `provider: ^6.0.0` to `pubspec.yaml` dependencies) to make your `LocaleProvider` accessible throughout the widget tree.
// main.dart (updated)
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:your_app_name/generated/l10n.dart';
import 'package:your_app_name/locale_provider.dart'; // Import your provider
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => LocaleProvider(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final localeProvider = Provider.of(context);
return MaterialApp(
title: 'Flutter Localization Demo',
locale: localeProvider.locale, // Use the locale from the provider
supportedLocales: L10n.all,
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: MyHomePage(),
);
}
}
// ... MyHomePage remains the same for now ...
The key change here is `locale: localeProvider.locale`. When `localeProvider.setLocale()` is called, `notifyListeners()` triggers a rebuild of `MyApp` and thus `MaterialApp`, updating the entire app's language.
3. Create a Language Switching UI
Now, let's add buttons or a dropdown to change the language.
// main.dart (updated MyHomePage)
// ... (imports and MyApp class as above) ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final s = S.of(context);
final localeProvider = Provider.of(context, listen: false); // listen: false because we only need to call methods
return Scaffold(
appBar: AppBar(
title: Text(s.changeLanguage),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(s.helloWorld),
SizedBox(height: 20),
Text(s.currentLanguage(Localizations.localeOf(context).languageCode)),
SizedBox(height: 40),
ElevatedButton(
onPressed: () {
localeProvider.setLocale(Locale('en'));
},
child: Text('English'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
localeProvider.setLocale(Locale('id'));
},
child: Text('Bahasa Indonesia'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
// Clear locale to revert to device's default
localeProvider.clearLocale();
},
child: Text('System Default'),
),
],
),
),
);
}
}
Full Code Example (main.dart
)
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:your_app_name/generated/l10n.dart'; // Adjust import path
import 'package:your_app_name/locale_provider.dart'; // Ensure this file exists
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => LocaleProvider(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final localeProvider = Provider.of(context);
return MaterialApp(
title: 'Flutter Localization Demo',
locale: localeProvider.locale, // Locale from provider
supportedLocales: L10n.all, // All supported locales from generated L10n
localizationsDelegates: [
S.delegate, // Generated delegate
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final s = S.of(context); // Access localized strings
final localeProvider = Provider.of(context, listen: false);
return Scaffold(
appBar: AppBar(
title: Text(s.changeLanguage),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(s.helloWorld),
SizedBox(height: 20),
Text(s.currentLanguage(Localizations.localeOf(context).languageCode)),
SizedBox(height: 40),
ElevatedButton(
onPressed: () {
localeProvider.setLocale(Locale('en'));
},
child: Text('English'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
localeProvider.setLocale(Locale('id'));
},
child: Text('Bahasa Indonesia'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
// Clear locale to revert to device's default
localeProvider.clearLocale();
},
child: Text('System Default'),
),
],
),
),
);
}
}
Persistence (Optional but Recommended)
The current implementation resets the language to the system default whenever the app restarts. To persist the user's chosen language, you can save the `Locale` to local storage (e.g., using shared_preferences) and load it when the `LocaleProvider` is initialized.
// lib/locale_provider.dart (with persistence)
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:your_app_name/generated/l10n.dart'; // For L10n.all
class LocaleProvider extends ChangeNotifier {
Locale? _locale;
Locale? get locale => _locale;
LocaleProvider() {
_loadPreferredLocale();
}
Future _loadPreferredLocale() async {
final prefs = await SharedPreferences.getInstance();
final langCode = prefs.getString('languageCode');
if (langCode != null) {
_locale = Locale(langCode);
if (!L10n.all.contains(_locale)) { // Validate loaded locale
_locale = null; // Fallback to system if not supported
prefs.remove('languageCode');
}
notifyListeners();
}
}
Future setLocale(Locale newLocale) async {
if (!L10n.all.contains(newLocale)) return;
_locale = newLocale;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('languageCode', newLocale.languageCode);
notifyListeners();
}
Future clearLocale() async {
_locale = null;
final prefs = await SharedPreferences.getInstance();
await prefs.remove('languageCode');
notifyListeners();
}
}
Remember to add `shared_preferences: ^2.0.0` to your `pubspec.yaml` dependencies for this feature.
Conclusion
Dynamic language switching is a powerful feature that significantly improves the user experience of localized Flutter applications. By combining Flutter's built-in localization tools with a simple state management pattern like `Provider` and `ChangeNotifier`, you can effortlessly give users control over the app's language, even persisting their preference across app sessions. This approach is scalable, maintainable, and integrates seamlessly with Flutter's reactive widget system.