Flutter Internationalization the Easy Way — using Provider and JSON

Mohamed Hassan Kadri
Flutter Community
Published in
7 min readOct 8, 2019

If you are going to deploy your app to users who speak another language then you’ll need to “internationalize” it. Flutter offers good documentation on how to set up your localized values for each locale that your app supports. but it can be sometimes unclear and hard to implement or hard to manage.

In this tutorial, we are going to learn how to localize our apps the simple way by using JSON files to store your data in multiple languages.

Getting Started

Let’s create a new Flutter project and clean your main.dart file. In this tutorial, my app has 2 languages which are English(LTR) and Arabic(RTL). The App will show sentences by device language.let’s do it

Prepare Your Project

before writing any actual code we need to create the files containing translated strings first and update the pubspec.yaml file.

Create the language files

Create a new folder i18n in the project root. This folder will hold all of the language files. Our project, we will have 2 Language Arabic and English so we will create en.json and ar.json.

These files will contain only simple key-value pairs of strings. Using JSON is beneficial, because similar to XML, you can just give it to a translator without them needing to access your code.

{
"title": "Hello!",
"Message" : "This is English"
}
{
"title": "مرحبا!",
"Message" : "هذه هي اللغة العربي"
}

Update pubspec.yaml

Flutter has a localization package for translating own component by languages. so we need to add it .open pubspec.yaml file and add flutter_localizations to your dependencies.

since you’ve just added new language JSON files, you need to specify them as assets in order to access them in code.

Create DemoLocalizations Class And Delegate Class

DemoLocalizations Class is the main class of localization. This class will load all sentences by given language then this class gives sentences by the loaded languages. This class uses load a method for loading the sentences from json files and it can get sentences from loaded sentences with translate method. My Language service will work by sentence keys.

class AppLocalizations {
final Locale locale;

AppLocalizations(this.locale);

// Helper method to keep the code in the widgets concise
// Localizations are accessed using an InheritedWidget "of" syntax
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}

// Static member to have a simple access to the delegate from the MaterialApp
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();

Map<String, String> _localizedStrings;

Future<bool> load() async {
// Load the language JSON file from the "lang" folder
String jsonString =
await rootBundle.loadString('i18n/${locale.languageCode}.json');
Map<String, dynamic> jsonMap = json.decode(jsonString);

_localizedStrings = jsonMap.map((key, value) {
return MapEntry(key, value.toString());
});

return true;
}

// This method will be called from every widget which needs a localized text
String translate(String key) {
return _localizedStrings[key];
}
}

you still need to provide a way for Flutter to step in and decide when the load() method will be called and which locale will be used. Flutter localization uses delegates for initialization. this is why we will create a delegate class to be able to use that class.

class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
// This delegate instance will never change (it doesn't even have fields!)
// It can provide a constant constructor.
const _AppLocalizationsDelegate();

@override
bool isSupported(Locale locale) {
// Include all of your supported language codes here
return ['en', 'ar'].contains(locale.languageCode);
}

@override
Future<AppLocalizations> load(Locale locale) async {
// AppLocalizations class is where the JSON loading actually runs
AppLocalizations localizations = new AppLocalizations(locale);
await localizations.load();
return localizations;
}

@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}

Make the MaterialApp localized

The setup of the localization happens inside the MaterialApp widget, which is the root of a Flutter app. Here you specify which languages are supported

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
supportedLocales: [
Locale('en', 'US'),
Locale('ar', ''),
],
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
home: AppLang(),
);
}}

Now we have all the setup done to start working.

Translating text in the UI

to translate your Just call the of() helper static method on AppLocalizations() to get the instance properly set up by Flutter and then translate using the keys you’ve specified in the JSON files!

class AppLang extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).translate('title')),
),
body: Center(
child: Text(AppLocalizations.of(context).translate('Message')),
),
);
}
}

Now to test if this works. Go to the app Settings and change the language to Arabic and re-open the app and you will see that the message and title are translated immediately to Arabic.

changing App language manually

Let’s be honest. When you’re building an app which you plan to release to the public, having it responsive to locale is not enough. you need to give the user the option to change the language and store his preferred language. MaterialApp allows us to immediately specify what locale we want our app using the locale property . to change this property I will be using provider and shared_preferences to store user choice.

Update pubspec.yaml

Go ahead and install the 2 new dependencies into our pubspec.yaml

  • shared_preferences
  • provider

Creating AppLanguage() Provider to manage Locale

Our AppLanguage will have 2 responsibilities

  • fetch the locale from the SharedPreferences whenever the app loads for the first time
  • Calling changeLanguage() from anywhere in the app to change the locale and store it in SharedPreferences .
class AppLanguage extends ChangeNotifier {
Locale _appLocale = Locale('en');

Locale get appLocal => _appLocale ?? Locale("en");
fetchLocale() async {
var prefs = await SharedPreferences.getInstance();
if (prefs.getString('language_code') == null) {
_appLocale = Locale('en');
return Null;
}
_appLocale = Locale(prefs.getString('language_code'));
return Null;
}


void changeLanguage(Locale type) async {
var prefs = await SharedPreferences.getInstance();
if (_appLocale == type) {
return;
}
if (type == Locale("ar")) {
_appLocale = Locale("ar");
await prefs.setString('language_code', 'ar');
await prefs.setString('countryCode', '');
} else {
_appLocale = Locale("en");
await prefs.setString('language_code', 'en');
await prefs.setString('countryCode', 'US');
}
notifyListeners();
}
}

Updating Main And Ui

to force our app to change language we will use locale property.

locale property: is the locale used when .the app first run. The initial locale for this app’s Localizations widget. If the locale is null the system’s locale value is used.

before starting our app we will create AppLanguage() instance and call our function fetchLocale() and pass to it our app to make sure it's unique and we will create two buttons to change the language.

void main() async {
AppLanguage appLanguage = AppLanguage();
await appLanguage.fetchLocale();
runApp(MyApp(
appLanguage: appLanguage,
));
}

class MyApp extends StatelessWidget {
final AppLanguage appLanguage;

MyApp({this.appLanguage});

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<AppLanguage>(
create: (_) => appLanguage,
child: Consumer<AppLanguage>(builder: (context, model, child) {
return MaterialApp(
//locale: model.appLocal,
supportedLocales: [
Locale('en', 'US'),
Locale('ar', ''),
],
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],

home: AppLang(),
);
}),
);
}
}

class AppLang extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appLanguage = Provider.of<AppLanguage>(context);
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).translate('title')),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
AppLocalizations.of(context).translate('Message'),
style: TextStyle(fontSize: 32),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () {
appLanguage.changeLanguage(Locale("en"));
},
child: Text('English'),
),
RaisedButton(
onPressed: () {
appLanguage.changeLanguage(Locale("ar"));
},
child: Text('العربي'),
)
],
)
],
),
),
);
}
}

That’s all our app Should Be ready to rock.

Final Thoughts

The sole objective of this article is to share a solution that works for me, which I hope could also help others. I am sure other solutions exist using a different approach than JSON Files so I hope you check them.

That’s all, thanks! If you liked this post, don’t forget to leave a 👏!

If you found this article interesting and Flutter development interests you, Or you need help to consider following me on Github, or LinkedIn.

--

--