Note: Our tutorial assumes that you have a basic understanding of internalization in Flutter. If you don’t, please make sure to review the official internationalization tutorial before continuing.

Let’s get started with a simple localized app:

void main() => runApp(MyApp());



class MyApp extends StatlessWidget {



@override

Widget build(BuildContext context) {



return MaterialApp(

localizationsDelegates: [

MyLocalizationsDelegate,

GlobalMaterialLocalizations.delegate,

GlobalWidgetsLocalizations.delegate,

],

supportedLocales: [

const Locale('en', ''), // English

const Locale('ar', ''), // Arabic

],

home: HomeScreen()

);

}

}



class HomeScreen extends StatelessWidget {

@override

Widget build(BuildContext context) {

TextStyle selectedStyle = TextStyle(

color: Theme.of(context).accentColor

);



return Scaffold(

appBar: AppBar(),

body: Center(child: Text(MyLocalizations.of(context, 'foo'))),

);

}

}

The code above defines a simple app that supports both the English and Arabic Languages

The app defines its own localizations in a class called MyLocalizations (we will see it in a bit)

The app displays the localized word ‘foo’ in the middle of the screen. The word is displayed either in English or in Arabic depending on the chosen locale.

MyLocalizations simply looks like this:

class MyLocalizations {

MyLocalizations(this.locale);



final Locale locale;



static Map<String, Map<String, String>> _localizedValues = {

'en': {

'foo': 'Foo',

'bar': 'Bar'

},

'ar': {

'foo': 'فو',

'bar': 'بار'

}

};



String translate(key) {

return _localizedValues[locale.languageCode][key];

}



static String of(BuildContext context, String key) {

return Localizations.of<MyLocalizations>(context,

MyLocalizations).translate(key);

}

}



class MyLocalizationsDelegate extends

LocalizationsDelegate<MyLocalizations> {



const MyLocalizationsDelegate();



@override

bool isSupported(Locale locale) =>

['en', 'ar'].contains(locale.languageCode);



@override

Future<MyLocalizations> load(Locale locale) {

return SynchronousFuture<MyLocalizations>

(MyLocalizations(locale));

}



@override

bool shouldReload(MyLocalizationsDelegate old) => false;

}

This is almost a textbook localizations class like the one you learned in the internationalization tutorial

I just made my of function take a key and return the localized string immediately.

Now, let’s get started! We want to control which locale is fed into this app!

The locale property of the MaterialApp

The first, and 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. So we can write:

return MaterialApp(

locale: Locale('ar', ''),

localizationsDelegates: [

MyLocalizationsDelegate,

GlobalMaterialLocalizations.delegate,

GlobalWidgetsLocalizations.delegate,

],

supportedLocales: [

const Locale('en', ''), // English

const Locale('ar', ''), // Arabic

],

home: HomeScreen()

);

This locale property that we set here allows us to force the locale of the app to Arabic, regardless of the locale of the device.

Fetching the locale from a server (or anywhere basically)

Of course, in most cases we wouldn’t want a hard-coded value. We would want to fetch the locale from a server, from the SharedPreferences , or from some file. Let’s assume we have the following function which fetches the locale info from the shared preferences:

_fetchLocale() async {

var prefs = await SharedPreferences.getInstance();



return Locale(prefs.getString('language_code'),

prefs.getString('country_code'));

}

We want to call this function, and then use the value it returns as a locale for our MaterialApp . To do that, we can either use a FutureBuilder , or make MyApp stateful, and call _fetchLocale in the initState function. Let’s check out both approaches!

The first is to use FutureBuilder as follows:

return FutureBuilder(

future: this._fetchLocale(),

builder: (context, snapshot) {

switch (snapshot.connectionState) {

case ConnectionState.none:

case ConnectionState.waiting:

// Return some loading widget

return CircularProgressIndicator();

case ConnectionState.done:

if (snapshot.hasError) {

// Return some error widget

return Text('Error: ${snapshot.error}');

} else {



Locale fetchedLocale = snapshot.data;



return MaterialApp(

locale: fetchedLocale,

localizationsDelegates: [

MyLocalizationsDelegate(),

GlobalMaterialLocalizations.delegate,

GlobalWidgetsLocalizations.delegate,

],

supportedLocales: [

const Locale('en', 'US'), // English

const Locale('ar', ''), // Arabic

],

home: HomeScreen()

);

}

}

},

);

This FutureBuilder , is calling the _fetchLocale function, and when it returns its value, the FutreBuilder feeds that value into our MaterialApp through snapshot.data .

, is calling the function, and when it returns its value, the feeds that value into our through . This approach is elegant, it does not require defining variables to detect loading, error, etc.

The other approach uses the initState function:

class MyApp extends StatefulWidget {

@override

_MyAppState createState() => _MyAppState();

}



class _MyAppState extends State<MyApp> {



Locale locale;



@override

void initState() {

super.initState();

this._fetchLocale().then((locale) {

setState(() {

this.locale = locale;

});

});

}



@override

Widget build(BuildContext context) {



if (this.locale == null) {

return CircularProgressIndicator();

} else {

return MaterialApp(

locale: this.locale,

localizationsDelegates: [

MyLocalizationsDelegate(),

GlobalMaterialLocalizations.delegate,

GlobalWidgetsLocalizations.delegate,

],

supportedLocales: [

const Locale('en', 'US'), // English

const Locale('ar', ''), // Arabic

],

home: HomeScreen()

);

}

}



_fetchLocale() async {

var prefs = await SharedPreferences.getInstance();



return Locale(prefs.getString('language_code'),

prefs.getString('country_code'));

} }

Notice that we are keeping an instance of the locale inside the class

In initState , we are calling _fetchLocale and storing its result in the locale instance. We call setState to make sure the app rebuilds when we receive the locale.

, we are calling and storing its result in the locale instance. We call to make sure the app rebuilds when we receive the locale. In the build function, as long as the locale is still null, we show a progress indicator. Otherwise, we show the MaterialApp with the locale instance fed into it.

with the locale instance fed into it. Using FutureBuilder might be cleaner and more preferred in most cases, but we will see why it might not be the best idea, when we get to talking about changing the locale upon the user’s request.

Showing the user the selected locale

Now that our app effectively fetches and uses the desired locale. Let’s see how we can display to the user the available locales, while highlighting the one in use. Let’s show our locales in the side drawer, and display the selected locale with the accent color, while the other locale is displayed with the default color.

To find out the chosen locale from anywhere in our application, we can use Localizations.localeOf(context) . This function provides us with the selected locale for any build context (Typically, the locale will be the same for the whole application, but it is possible to define a different locale for only part of the app). So, we will add this side drawer to our HomeScreen widget, and show the active locale distinctly:

class HomeScreen extends StatelessWidget {

@override

Widget build(BuildContext context) {

TextStyle selectedStyle = TextStyle(

color: Theme.of(context).accentColor

);



return Scaffold(

appBar: AppBar(),

drawer: Drawer(

child: ListView(

children: <Widget>[

ListTile(

title: Text(

'العربية', // which means Arabic

style: _getLanguageCode(context) == 'ar'?

selectedStyle : null,

)

),

ListTile(

title: Text(

'English',

style: _getLanguageCode(context) == 'en'?

selectedStyle : null,

),

),

],

),

),

body: Center(child: Text('Managing your locale')),

);

}



_getLanguageCode(BuildContext context) {

return Localizations.localeOf(context).languageCode;

}

}

We have added a drawer which displays the available locales, and highlights the color of the selected locale using the the _getLanguageCode function.

function. The _getLanguageCode function simply returns the language code returned by Localizations.localeOf(context) .

Allowing the user to change the locale

The final step is to allow the user to change the displayed locale. When the user clicks on the language name in the drawer, we want to rebuild the app using the chosen locale. To do this, we will implement the onTap event for the two entries in the drawer. When an entry is tapped, we do two things:

store the new locale in the shared preferences or on the server or wherever we choose, so that when the application is restarted, the new locale is shown. Rebuild the whole MaterialApp to show it with the new locale

So, the onTap event for the English entry, for example, will look like:

onTap: () async {

// If the already-selected language is not English

// Then, change it to English

if (_getLanguageCode(context) != 'en') {

// step one, save the chosen locale

var prefs = await SharedPreferences.getInstance();

await prefs.setString('language_code', 'en');

await prefs.setString('country_code', '');



// step two, rebuild the whole app, with the new locale

}



}

But how will we rebuild the whole app? To do that, we need to call setState in _MyAppState , and provide it with the new locale. One way to achieve that is to create an event that bubbles all the way from the tapped entry, to _MyAppState widget, where we call setState to change the locale. But this is too much of a hassle, because what if the tapped entry where we are changing the locale is buried too deep in the widget tree, we will then have to add code everywhere just to bubble the event up to the main app widget. We can definitely do better than this!

Let’s add the following function inside MyApp widget:

class MyApp extends StatefulWidget {

@override

_MyAppState createState() => _MyAppState();



static void setLocale(BuildContext context, Locale newLocale) {

_MyAppState state =

context.ancestorStateOfType(TypeMatcher<_MyAppState>());



state.setState(() {

state.locale = newLocale;

});

}

}

The setLocale static function can be called from any context. It uses the ancestorStateOfType method to locate the _MyAppState object.

static function can be called from any context. It uses the method to locate the object. Once the objectis located, the function simply calls setState on it, and provides it with the new locale.

We can use this function then from the onTap event:

onTap: () async {

// If the chosen language is not English

// Then, let's change it to English

if (_getLanguageCode(context) != 'en') {

// step one, save the chosen locale

var prefs = await SharedPreferences.getInstance();

await prefs.setString('languageCode', 'en');

await prefs.setString('countryCode', '');



// step two, rebuild the whole app, with the new locale

MyApp.setLocale(context, Locale('en', ''));

}

}

This will rebuild our MaterialApp with the new locale.

You can notice here that this is the reason why I preferred not to use FutureBuilder , and to handle the future in initState instead. Using FutureBuilder would have meant that the future will be called every time the app is rebuilt, which is not what we want. Thus we used initState instead, which, by definition, will only be called once.

Taking the device locale into account

We have spent this whole article running away from the device locale. But let’s face it, we might still need it. What if nothing is stored in the shared preferences yet? What if our server did not reply? can’t we fall back to using the device locale then?

Of course we can! All we have to do is to replace the locale property that we learned in the beginning of the tutorial, with the more powerful localeResolutionCallback property, which is a callback that accepts the device locale as a parameter, and let’s you return whatever locale you want. Thus, let’s play a little bit with _MyAppState class to make it look as follows:

class _MyAppState extends State<MyApp> {



Locale locale;

bool localeLoaded = false;



@override

void initState() {

super.initState();

this._fetchLocale().then((locale) {

setState(() {

this.localeLoaded = true;

this.locale = locale;

});

});

}



@override

Widget build(BuildContext context) {



if (this.localeLoaded == false) {

return CircularProgressIndicator();

} else {

return MaterialApp(

localeResolutionCallback: (deviceLocale, supportedLocales) {

if (this.locale == null) {

this.locale = deviceLocale;

}

return this.locale;

},

localizationsDelegates: [

MyLocalizationsDelegate(),

GlobalMaterialLocalizations.delegate,

GlobalWidgetsLocalizations.delegate,

],

supportedLocales: [

const Locale('en', 'US'), // English

const Locale('ar', ''), // Arabic

],

home: HomeScreen()

);

}

} _fetchLocale() async {

var prefs = await SharedPreferences.getInstance(); if (prefs.getString('language_code') == null) {

return null;

} return Locale(prefs.getString('language_code'),

prefs.getString('country_code'));

}

}