Flutter: Multi-lingual application - how to override the locale? - flutter

I followed the explanations given in the official Flutter pages (see here) to make my application work in different languages.
According to the documentation, it retrieves the user's locale and this works fine.
Let's now suppose that my application supports different languages (such as EN, FR, ES, ...) and that the user could select one of these languages to use the application (the selected language would then be different than the one defined in the phone's settings), how can I achieve this?
How may I force the application Locale and dynamically "reload" all the translations?
The Flutter page does not explain this and I haven't seen anything that help me in the documentation...
Here is the current implementation:
class Translations {
Translations(this.locale);
final Locale locale;
static Translations of(BuildContext context){
return Localizations.of<Translations>(context, Translations);
}
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'title': 'Hello',
},
'fr': {
'title': 'Bonjour',
},
'es': {
'title': 'Hola',
}
};
String text(String key){
return _localizedValues[locale.languageCode][key] ?? '** ${key} not found';
}
}
class TranslationsDelegate extends LocalizationsDelegate<Translations> {
const TranslationsDelegate();
#override
bool isSupported(Locale locale) => ['en', 'fr','es'].contains(locale.languageCode);
#override
Future<Translations> load(Locale locale) {
return new SynchronousFuture<Translations>(new Translations(locale));
}
#override
bool shouldReload(TranslationsDelegate old) => false;
}
In the main.dart:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: Translations.of(context).text('title'),
theme: new ThemeData(
primarySwatch: Colors.blue,
),
localizationsDelegates: [
const TranslationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''), // English
const Locale('fr', ''), // French
const Locale('fr', ''), // French
],
home: new LandingPage(),
);
}
}
Many thanks for your help.

This can be accomplished by
creating a new LocalizationsDelegate that either translates to a
single locale or defers completely depending on a parameter
converting the base app (MyApp) to a stateful widget and inserting the new delegate above into the localizationsDelegates list
managing the base app (MyApp) state with a new delegate targeting a specific locale based on some event
A simple implementation for 1) might be:
class SpecifiedLocalizationDelegate
extends LocalizationsDelegate<Translations> {
final Locale overriddenLocale;
const SpecifiedLocalizationDelegate(this.overriddenLocale);
#override
bool isSupported(Locale locale) => overriddenLocale != null;
#override
Future<Translations> load(Locale locale) =>
Translations.load(overriddenLocale);
#override
bool shouldReload(SpecifiedLocalizationDelegate old) => true;
}
Next for 2) and 3), convert the MyApp to stateful and include the new delegate (initially just deferring everything), plus some event handlers to change the state with a new delegate that specifies a new Locale.
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
SpecifiedLocalizationDelegate _localeOverrideDelegate;
#override
void initState() {
super.initState();
_localeOverrideDelegate = new SpecifiedLocalizationDelegate(null);
}
onLocaleChange(Locale l) {
setState(() {
_localeOverrideDelegate = new SpecifiedLocalizationDelegate(l);
});
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
localizationsDelegates: [
_localeOverrideDelegate,
const TranslationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''), // English
const Locale('fr', ''), // French
],
home: new LandingPage(onLocaleSwitch: onLocaleChange),
);
}
}
With these changes, in children widgets you could now use Translations.of(context).myLocalizedString to retrieve the translations.
More complete gist: https://gist.github.com/ilikerobots/474b414138f3f99150dbb3d0cc4cc721

To control the locale of the app, you can use the locale property of the MaterialApp:
return MaterialApp(
...
locale: _myLocal,
...
);
This, combined with #ilikerobots StatefulWidget approach shall provide you with what you need.

using one of the Providers should do the job, I am not really familiar with providers but this got me working easily
wrap your material app using ChangeNotifierProvider
return ChangeNotifierProvider(
create: (_) => new LocaleModel(),
child: Consumer<LocaleModel>(
builder: (context, provider, child) => MaterialApp(
title: 'myapp',
locale: Provider.of<LocaleModel>(context).locale
...
...
...
create A model class with getters and setters to get & set the locale as\
import 'package:iborganic/const/page_exports.dart';
class LocaleModel with ChangeNotifier {
Locale locale = Locale('en');
Locale get getlocale => locale;
void changelocale(Locale l) {
locale = l;
notifyListeners();
}
}
Change the locale on some event (button click) as
Provider.of<LocaleModel>(context).changelocale(Locale("kn"));
The benefit of wrapping the material app within Provider is you can have access to the locale value from any part of your app

The easiest way, which weirdly enough is not mentioned in the internationalization tutorial, is using the locale property. This property of the MaterialApp class allows us to immediately specify what locale we want our app to use
return MaterialApp(
locale: Locale('ar', ''),
localizationsDelegates: [
MyLocalizationsDelegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''), // English
const Locale('ar', ''), // Arabic
],
home: HomeScreen()
);
This tutorial explained it better
It also explained how to load the locale preference from sharedPreferences

Related

UnsupportedError thrown when setting localizationsDelegates

I'm trying to implement internationalization for my Flutter app but it won't let me use my preferred language (Indonesian).
From the docs, it told me to add localizationsDelegates to my MaterialApp. Since I am using GetX, the MaterialApp here are wrapped inside the GetMaterialApp.
But when I did that, it throws me an error:
Exception has occurred.
UnsupportedError (Unsupported operation: Cannot modify unmodifiable map)
I tried to remove the localizationsDelegates, it throws me yet another error:
Exception has occurred.
FlutterError (No MaterialLocalizations found.
TabBar widgets require MaterialLocalizations to be provided by a Localizations widget ancestor.
The material library uses Localizations to generate messages, labels, and abbreviations.
To introduce a MaterialLocalizations, either use a MaterialApp at the root of your application to include them automatically, or add a Localization widget with a MaterialLocalizations delegate.
The specific widget that could not find a MaterialLocalizations ancestor was:
TabBar
But when I hard code the locale property of my GetMaterialApp to Locale('en', 'US') and commented the localizationsDelegates, it works.
Do you guys know why and how to fix this?
Anyways, here is how my main.dart (and some related files) look like.
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Get.putAsync(() => EnvService().init());
await Get.putAsync(() => IntlService().init());
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return GetMaterialApp(
title: "MyApp",
initialRoute: AppPages.INITIAL,
getPages: AppPages.routes,
themeMode: ThemeMode.dark,
theme: MyAppTheme.light,
darkTheme: MyAppTheme.dark,
debugShowCheckedModeBanner: false,
// translations: AppTranslations(),
locale: const Locale('en', 'US'),
fallbackLocale: const Locale('id', 'ID'),
supportedLocales: const [
Locale('en', 'US'),
Locale('id', 'ID'),
],
//
//
// UNCOMMENT THIS LINE, YOUR PHONE WILL EXPLODE!
// localizationsDelegates: GlobalMaterialLocalizations.delegates,
);
}
}
env_service.dart
class EnvService extends GetxService {
static EnvService get instance => Get.find();
Future<EnvService> init() async {
await dotenv.load();
return this;
}
Locale get defaultLocale {
final locale = dotenv.get('DEFAULT_LOCALE', fallback: 'id');
if (locale != 'id') {
return const Locale('en', 'US');
}
return Locale(locale, locale.toUpperCase());
}
}
intl_service.dart
class IntlService extends GetxService {
static IntlService get instance => Get.find();
Locale get _locale {
return Get.locale ?? EnvService.instance.defaultLocale;
}
Future<IntlService> init() async {
await initializeDateFormatting(_locale.countryCode);
return this;
}
String formatCurrency(double number) {
final formatter = NumberFormat.simpleCurrency(decimalDigits: 0);
return formatter.format(number);
}
String formatDate(DateTime? dateTime, [String? format]) {
if (dateTime == null) {
return '';
}
return DateFormat(format).format(dateTime);
}
}
try add this in GetMaterialApp:
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
It should work if you remove initializeDateFormatting(). And be aware that Hot Reload might not work if you make this change.
This works because initializeDateFormatting() calls initializeDatePatterns() with a const Map setting dateTimePatterns which is immutable after that. But GlobalMaterialLocalizations.delegate calls initializeDateFormattingCustom() which tries to write its map into dateTimePatterns at the end. That results in an "Unsupported operation: Cannot modify unmodifiable map". So if we do not call initializeDateFormatting(), this error does not happen because dateTimePatterns is not immutable when initializeDateFormattingCustom() is called. But the date formatting is initialized anyway because that is what initializeDateFormattingCustom() does with the formatting from GlobalMaterialLocalizations.
This depends on intl ^0.17.0.

Flutter - change locale and persisting with Provider

I am really out of options here. I am currently trying to have an option in my settings that enable the user to change language on a Button tap with Provider.
Changing the language currently works just fine, but it is not persisted, because I am passing the provider locale to the locale in my MaterialApp:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: providers,
child: ChangeNotifierProvider(
create:(_) => LocaleProvider(),
builder: (context, child) {
final provider = Provider.of<LocaleProvider>(context, listen: true);
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
onGenerateRoute: RouteGenerator.generateRoute,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: L10n.supportedLocales,
locale: provider.locale,
);
},
),
);
}
}
My locale Provider currently looks like this:
class LocaleProvider extends ChangeNotifier {
Locale _locale = const Locale('de');
Locale get locale => _locale;
void setLocale(Locale locale) async {
_locale = await saveLocale(locale.languageCode);
notifyListeners();
}
The setLocale method is called from my Settings. I tried using sharedPreferences to store my locale, the Problem I have here is that I cannot call an async method on the locale property in the MaterialApp to await the value stored in sharedPreferences.
What I want to achieve is, that the user can click on the button, the language gets changed instantly and is then saved and persisted until the user changes it again or uninstalls the app.
Any help would be much appreciated here.
simple solution...
before you run the app in main.dart initiate the active local
//get active locale from persisted memory
Locale persistedLocale = //get from persisted memory
//set for provider usage
LocaleProvider().setLocale(persistedLocale);
//run the app
runApp(MyApp());
Of course, after posting this question I found a solution myself...
I am currently retrieving the SharedPreferences in my main function, and then passing them down to MyApp:
final prefs = await SharedPreferences.getInstance();
runApp(MyApp(prefs: prefs,));
In the MaterialApp, locale is set as follows:
locale: Locale(_prefs.getString('locale') ?? provider.locale.languageCode),
This will ensure, that on App Startup the locale isnt null, because in the provider, the locale property is initialized with a default value.

Flutter: how to translate tabs on BottomNavigationBarItem

I'm new to Flutter (and the Dart programming language) and I'm struggling with translating the tabs on the BottomNavigationBarItem.
I'm currently basing my code heavily on Andrea Bizzotto's Bottom Navigation Bar with Multiple Navigators.
Here's what I have so far:
Setup:
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'CovidSafe extended',
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const <Locale>[
Locale('nl'),
Locale('fr'),
Locale('de'),
Locale('en'),
],
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const App(),
);
}
}
class App extends StatefulWidget {
const App({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() => AppState();
}
class AppState extends State<App> {
var _currentTab = TabItem.red;
final _navigatorKeys = {
TabItem.red: GlobalKey<NavigatorState>(),
TabItem.green: GlobalKey<NavigatorState>(),
TabItem.blue: GlobalKey<NavigatorState>(),
};
...
}
It uses a TabItem object, which is an enumerable, which has 2 properties: tabName and activeTabColor:
enum TabItem { red, green, blue }
const Map<TabItem, String> tabName = {
TabItem.red: 'red',
TabItem.green: 'green',
TabItem.blue: 'blue',
};
const Map<TabItem, MaterialColor> activeTabColor = {
TabItem.red: Colors.red,
TabItem.green: Colors.green,
TabItem.blue: Colors.blue,
};
The navbar items get built from the _buildItem function inside the BottomNavigation class, like so:
class BottomNavigation extends StatelessWidget {
const BottomNavigation(
{Key? key, required this.currentTab, required this.onSelectTab})
: super(key: key);
final TabItem currentTab;
final ValueChanged<TabItem> onSelectTab;
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: [
_buildItem(TabItem.red, context),
_buildItem(TabItem.green, context),
_buildItem(TabItem.blue, context),
],
onTap: (index) => onSelectTab(
TabItem.values[index],
),
currentIndex: currentTab.index,
selectedItemColor: activeTabColor[currentTab]!,
);
}
BottomNavigationBarItem _buildItem(TabItem tabItem, BuildContext context) {
return BottomNavigationBarItem(
icon: Icon(
Icons.layers,
color: _colorTabMatching(tabItem),
),
// I think this is where I need to somehow translate the tabName
label: tabName[tabItem],
);
}
Color _colorTabMatching(TabItem item) {
return currentTab == item ? activeTabColor[item]! : Colors.grey;
}
}
I've tried using the following code to translate the label/tabName:
// Option 1:
// won't work because "there's no getter for tabName"
label: AppLocalizations.of(context)!.tabName[tabItem],
// Option 2:
// tried some variants of this, this will just print "AppLocalizations.of..." as a string
label: '$AppLocalizations.of($context)!.$tabName[$tabItem]',
// Option 3:
label: AppLocalizations.of(context)!.bottomNavBarLabel(tabName[tabItem])
// and added the following to my `app_en.arb` file:
"bottomNavBarLabel": "{label}",
"#bottomNavBarLabel": {
"type": "text",
"placeholders": {
"label": {}
}
},
// but that doesn't actually translate the variable passed along (which does makes sense, but I figured I'd at least try it)
I've also tried passing the BuildContext along to the TabName Map, but couldn't get it to work.
I'm sure this is something relatively simple that I just can seem to figure out. Maybe working with a Map is actually not the way to go for this, I don't know...
So how do I make it so that the label (tabName[tabItem]) is translatable?
In my personal experience so far, you don't change the labels through passing data between widgets (not that you can't, but IMHO this is much more easier and pragmatic, since the user can have an app in his native language, if you support it that is, without having to change the apps language inside the app settings), but with locale language that is detected through app localization/internationalization.
You might wanna try a few things. First, try adding this callback in your MaterialApp() :
UPDATE: I have tested it in my emulator and it is working, updated asnwer is below.
// here you define the languages that you want to support, so it is important to first put the language you wish your app to be in
supportedLocales: const <Locale>[
Locale('en'),
Locale('nl'),
],
localeResolutionCallback: (locale, supportedLocales) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode &&
supportedLocale.countryCode == locale.countryCode) {
return supportedLocale;
}
}
return supportedLocales.first; // because of this it will always return the first locale from the list
},
Here is where you check if the current locale is supported in your app, if it is, change the text to your locale.
If you haven't yet, in your lib folder create a new folder called l10n, and add a file app_en.arb where you can define the text you wanna change in English. Then do the same with another file, I made one for German, so app_de.arb. In those files, define the text you wish to change throughout the app with locale language change:
app_en.arb
{
"##locale": "en",
"hello": "Hello",
"#hello": {
"description": "The conventional newborn programmer greeting"
},
}
You do the same for German (or any other language for that matter) but you only define the main EN you wish to change:
{
"##locale": "de",
"hello": "Hallo", // from English to German
}
Then you define where is the text you want to change with:
AppLocalizations.of(context).hello // here you define the word you want to change on app locale change
UPDATE: You are not passing context to the AppLocalizations.of(context).hello that it why it is not working for you and also, you have made your Map<TabItem, String> tabName a const and since you are switching values of the string, it can't be a const value.
// without a const value and you must put it in a separate class
//or in the class in which you are using it so you can pass the context
Map<TabItem, String> tabName = {
TabItem.red: AppLocalizations.of(context).red,
TabItem.green: AppLocalizations.of(context).green,
TabItem.blue: AppLocalizations.of(context).blue,
};
If this doesn't work, let me know and I will try to find another solution for you. Cheers!

Flutter Localization with ARB - The getter was called on null

I've been having this error for a while and I think I need some second eyes of an expert to solve it and I'm really new in this language ^^.
I added localizable to my project in Flutter, added all the files.arb in different languages and tries to import it following Google's tutorial and other just different work around but keep getting the same error:
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following NoSuchMethodError was thrown building StyleguideScreen(dirty):
The getter 'welcomeGeneralInfoTitle' was called on null.
Receiver: null
Tried calling: welcomeGeneralInfoTitle
This is my AppLocalizations.dart class I'm using for the localicationDelegates
class AppLocalizations {
static const AppLocalizationsDelegate delegate = AppLocalizationsDelegate();
static Future<AppLocalizations> load(Locale locale) {
final String name = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
final String localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
return AppLocalizations();
});
}
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
String get welcomeGeneralInfoTitle {
return Intl.message('Bet Master', name: 'title', desc: 'App Title');
}
}
class AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const AppLocalizationsDelegate();
List<Locale> get supportedLocales {
return const <Locale>[
Locale('en', ''),
Locale('de', ''),
Locale('es', ''),
Locale('es', 'ES'),
]; //Still need to add 18 languages, is there a better way to add them?
}
#override
bool isSupported(Locale locale) => _isSupported(locale);
#override
Future<AppLocalizations> load(Locale locale) => AppLocalizations.load(locale);
#override
bool shouldReload(AppLocalizationsDelegate old) => false;
bool _isSupported(Locale locale) {
if (locale != null) {
for (Locale supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return true;
}
}
}
return false;
}
}
and here is where I added to the root of the project
return MaterialApp(
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.delegate.supportedLocales,
title: 'AMP',
theme: Theme.darkTheme,
home: StyleguideScreen(),
);
And here is how I try to implement it and it crashes
class StyleguideScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final welcome = AppStrings.current.welcomeGeneralInfoTitle;
return Scaffold(...)
}
}
The app generates correctly all the generated files per language that it needs to import and I think it looks pretty straight forward, when I debug it, it is getting the locale correctly. Has anyone any idea why could this be happening? Thanks in advance :pray:
FIX: I just needed to add into the localizationsDelegates the auto-generated AppStrings.delegate, from the file import 'generated/l10n.dart'; instead of creating a new AppLocalizations.delegate.
like this:
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
AppStrings.delegate,
],
and remove completely the AppLocationzations class I did and it works smooth! :)
PD: I add this new library flutter_localized_locales
I use Android Studio Flutter with Intl plugin and the problem was the same
In main.dart add import import 'generated/l10n.dart';
Also add S.delegate in list of localizationsDelegates
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
S.delegate,
],

How to change a Flutter app language without restarting the app?

In the settings page of my app, I would like to add an option that controls the app language.
I can set the language before starting the app like this:
#override
Widget build(BuildContext context) {
return MaterialApp(
// other arguments
locale: Locale('ar'),
);
}
But is it possible to change the language without restarting the app?
If you want to change app language without restarting the app and also without any plugin, you can follow the bellow steps:
In main file of the application, change the default MyHomePage to a StatefullWidget, in StatefullWedget for example MyHomePage create a static method setLocal as follow
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
static void setLocale(BuildContext context, Locale newLocale) async {
_MyHomePageState state = context.findAncestorStateOfType<_MyHomePageState>();
state.changeLanguage(newLocale);
}
#override
_MyHomePageState createState() => _MyHomePageState();
}
where _MyHomePageState is the state of your MyHomePage widget
In your state create a static method changeLanguage:
class _MyHomePageState extends State<MyHomePage> {
Locale _locale;
changeLanguage(Locale locale) {
setState(() {
_locale = locale;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Afghanistan',
theme: ThemeData(primaryColor: Colors.blue[800]),
supportedLocales: [
Locale('fa', 'IR'),
Locale('en', 'US'),
Locale('ps', 'AFG'),
],
locale: _locale,
localizationsDelegates: [
AppLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
localeResolutionCallback: (locale, supportedLocales) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode &&
supportedLocale.countryCode == locale.countryCode) {
return supportedLocale;
}
}
return supportedLocales.first;
},
initialRoute: splashRoute,
onGenerateRoute: Router.generatedRoute,
);
}
}
Now from pages of your application you can change the language by calling the setLocal method and pass a new Locale as follow:
Locale newLocale = Locale('ps', 'AFG');
MyHomePage.setLocale(context, newLocale);
Please remember you need to create a LocalizationDelegate,
Here is the link to the Written Tutorial and Demo Application
Wrap your MaterialApp into a StreamBuilder which will be responsible for providing the Locale value to your application. And it will enable you to dynamically change it without restarting your app. This is an example using the rxdart package to implement the stream:
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: setLocale,
initialData: Locale('ar',''),
builder: (context, localeSnapshot) {
return MaterialApp(
// other arguments
locale: localeSnapshot.data,
);
}
);
}
Stream<Locale> setLocale(int choice) {
var localeSubject = BehaviorSubject<Locale>() ;
choice == 0 ? localeSubject.sink.add( Locale('ar','') ) : localeSubject.sink.add( Locale('en','') ) ;
return localeSubject.stream.distinct() ;
}
The above demonstration is just a basic way of how to achieve what you want to, but for a proper implementation of streams in your app you should consider using app-wide BloCs, which will significantly improve the quality of your app by reducing the number of unnecessary builds.
You can wrap the MaterialApp widget with a ChangeNotifierProvider and a Consumer widgets and control the language from the model.
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
builder: (context) => MainModel(context: context),
child: Consumer<MainModel>(builder: (context, mainModel, child) {
return MaterialApp(
locale: Locale(mainModel.preferredLanguageCode),
....
On the MainModel, all you need to do is change the preferredLanguageCode variable to whatever you want ('en', 'ar', 'es', etc). Don't forget to call NotifyListeners() once you change the language.
This and the other answer have only one problem: Any context above MaterialApp can't get the device language (for example when the app is started for the first time) with Localizations.localeOf(context). This method required a context bellow MaterialApp.
To fix this issue, I used this plugin to get the device language without the need of a context.
Once the app starts, you can change the language any way you want that this approach will work. I also use SharedPreferences to store the preferred language once the user changes it.
It's easier to use easy_localization package.
For changing language, for example:
onTap: (){
EasyLocalization.of(context).locale = Locale('en', 'US');
}
I learned using this package by this video: Youtube Video Link
UPDATE:
In version 3.0.0:
EasyLocalization.of(context).setLocale(Locale('en', ''));
You can use the most popular GetX library as well.
Call Get.updateLocale(locale) to update the locale. Translations then automatically use the new locale.
var locale = Locale('en', 'US');
Get.updateLocale(locale);