How to Internationalize a Flutter App Ising intl and intl_translation

Flutter is Google's hot, up-and-coming cross-platform mobile development framework written in Dart. Today, we'll explore how to internationalize and localize a Flutter app by using a relatively simple demo. We'll use Dart's intl and intl_translation libraries to wire up language files to our app. We'll work with simple translation strings, complex plurals, and dates. Then we'll sit back and relax as Flutter takes care of locale layout direction and its own localized widgets for us. Let's get cooking.

Google’s Flutter mobile development framework makes an enticing promise. It allows you to develop cross-platform mobile apps with a single code base that compiles down to machine language for great performance. It’s also built with Dart, a get-things-done, single-threaded language using JavaScript-like asynchronous callbacks to prevent thread blocking. Flutter features a rich array of component “widgets” that make it possible to compose a mobile app with lightning speed. While it’s too early to be sure, it looks like Flutter could be the future of mobile app development – especially when it comes to startups and small teams on a budget. At times, when we use Flutter, we’ll want to present our apps to users from different countries, which means we will want to internationalize and localize our apps. This article is a foray into just that and tries to show how to approach flutter i18n and flutter l10n.

Note » I assume that you already know the basics of Flutter and Dart. While we’ll cover some Flutter concepts like InheritedWidget, we’ll mostly be looking at the specifics of Flutter i18n and l10n here.

Framework & Libraries Used

At the time of writing, we’re using the following versions of Flutter and our i18n libraries:

  • Flutter 0.8.2 (Release Preview 2)
  • intl 0.15.7 – provides i18n capabilities, including translation messages
  • intl_translation 0.17.0 – provides message extraction into language files that can be used by translators

Our Demo App

We’ll start with a simple English-only “Stay in Touch” demo app presenting a list of contacts – each with a name and a date to contact them next. The app will also allow us to add a contact (in memory, since persistence is a bit outside of the scope of this article). We’ll build our i18n solution on top of this app and, by the end of this article, we’ll have the app working in both English and Arabic. You can choose to localize the app in the language of your choice, of course.

Here’s what our starter app looks like:

 

Here’s a simplified diagram of our app’s widget tree:

You can grab the code for the starter app at its Github repo: https://github.com/ashour/flutter-i18n-starter.

Once you clone the repo, and assuming you have Flutter installed, a quick flutter packages get on the command line from the project root directory should have you all set. You should be able to run the app on an Android simulator or iOS emulator after that. Let’s go over the business logic of our starter app.

Our main model is a simple Contact list.

lib/src/models/contact.dart


A Contact represents each row in our main list. The contactNextAt is supposedly when we would like to get a reminder about contacting a person. For brevity, we don’t have a reminder functionality in the current version of our app.

We export a List<Contact> to our app’s widgets through an InheritedWidget.

lib/src/shared_state/contacts_provider.dart

Our InheritedWidget subclass, ContactsProvider, exposes our list of contacts to the concerned widgets. Let’s see how this is wired up in our app before we look a bit more closely at InheritedWidgets.

lib/src/app.dart

Since our app is small and we want our List<Contact> to be available to its both screens, we wrap our entire MaterialApp with our ContactsProvider. But what exactly is this doing and what is an InheritedWidget? Our internationalization code will make use of the inherited pattern, so let’s dive into that a bit.

Shared State & InheritedWidget

If you’ve ever used a reactive, component-based framework like Flutter or React, you know that, inevitably, you have pieces of state that you want to share across different components or widgets. This can mean passing these bits of state down widget sub-trees in your app, which can quickly become a maintenance headache. If you want to change the shape or interface of your shared state, you may have to update several pieces of your app. Another problem is that you may need a bit of shared state several levels down a widget sub-tree. To do so, you may have to pass that state down each widget in the sub-tree until you get to the one concerned with the respective state. These in-between widgets shouldn’t need to know about the shared state, since they do nothing with it. This can create confusing widget APIs, where a widget’s parameters don’t directly reflect its needs.

To deal with this, Flutter provides InheritedWidgets. An InheritedWidget carries a state that can be shared with a specific widget sub-tree in your app, and Flutter is designed so, that a widget can ask for an InheritedWidget that was provided to its sub-tree. Flutter will use the InheritedWidget.updateShouldNotify method to inform a concerned widget whether it should rebuild as a reaction to a change in the information held by the InheritedWidget.

In our app’s case, we return true from updateShouldNotify only when our contact list changes. We also provide our shared state to our entire app, since our app is quite small and only has the contact list to share. However, to keep a bigger app efficient, we would want to be more granular when we share our InheritedWidgets.

Flutter makes use of the inherited pattern in its i18n, and we’ll see that in action, when we begin internationalizing our app, which incidentally is what we’ll be doing next.

Flutter i18n: Internationalizing the App

Installing our i18n Packages

The first step to internationalizing our app is to add three packages.

  • flutter_localizations is included with Flutter and contains several localizations for Flutter’s own widgets (a full list of the available localizations can be found in the Flutter documentation).
  • intl, an official Dart package, provides many of the i18n and l10n capabilities we need. It supports working with translation messages among other i18n-related things.
  • intl_translation, another package provided by the Dart team, provides command-line tools for generating code and translation files which we’ll use to localize our app.

We can add these three files to our pubspec.yaml file.

Running flutter packages get should install the packages and have us good to go.

Updating Info.plist for iOS

We’ll be working mostly right in Dart and Flutter here. However, iOS won’t see our supported locales unless we explicitly set them in our Info.plist file.

ios/Runner/Info.plist

Since we’re supporting English and Arabic, we set their ISO 639-1 codes in the file.

Creating the Localizations and LocalizationsDelegate Classes

Next, let’s create a localizations class that we can provide to our app. Later, this class will include our internationalized messages.

lib/src/lang/sit_localizations.dart

Our class is called SitLocalizations to differentiate it from Flutter’s own Localizations class. “Sit” is just a namespace and it stands for “Stay in Touch”, which is our demo app’s name.

The SitLocalizations.of method takes a BuildContext and returns an instance of SitLocalizations, much like an InheritedWidget would. We’ll use this method in our widgets to retrieve our translated messages.

Note » We need to comment out our import of the messages_all.dart file and its initializeMessages() function call until we generate the file through intl_translations. We’ll do that a bit later and come back to un-comment these lines.

This class is pretty useless without a LocalizationDelegate, which we’ll pass to our app. Let’s take a look at the delegate before we wire it up to our app.

lib/src/lang/sit_localizations_delegate.dart

Our SitLocalizationsDelegate class derives from Flutter’s LocalizationDelegate, and its job is to provide basic localization functions to our app. The isSupported method returns true if a given locale is supported by our app. load will get the current locale’s messages ready for usage. Our delegate’s load method itself delegates to our SitLocalizations.load method, which extracts the given locale’s ISO code, loads the locale’s translated messages and forces the Intl package to use the locale. shouldReload is similar to InheritedWidget.updateShouldNotify, and should return true when our localizations change. Since our app’s localizations won’t change after they’ve initially loaded, we always return false from shouldReload.

Wiring Our LocalizationDelegate Up to Our App

Okay, with that in place, let’s go to our app.dart file and connect our SitLocalizationDelegate to our app. We’ll also bring in Flutter’s built-in localizations and tell our app which locales we support while we’re at it.

lib/src/app.dart

Note » The supportedLocales List will be Flutter’s source of truth for supported locales. So if a user switches his or her device’s language, our app will only follow suit if that language is in our ‘supportedLocales’ list.

The great thing about using Flutter is that it has a lot of built-in niceties. One of them is first-class support for i18n and l10n. The provided MaterialApp widget’s constructor takes two params relevant to our solution: localizationDelegates and supportedLocales. These are where we register our delegates and our app’s supported locales, respectively. Notice that we added GlobalMaterialLocalizations.delegate and GlobalWidgetsLocalizations.delegate to our localizationDelegates. These provide Flutter’s built-in localizations for its own widgets, and we’ll see that in action a bit later. Before those, we provide an instance of our own SitLocalizationsDelegate, and that wires us all up to use our translations. Speaking of which…

The Flutter l10n Workflow

We’ve wired up our Flutter localization delegate, which will provide our SitLocalizations to our app’s widgets. However, we haven’t really used SitLocalizations to provide any localizations. Let’s do that. Our app’s title is a good place to start. Let’s add it as a localized message.

lib/src/lang/sit_localizations.dart

We can add the message at the bottom of our SitLocalizations class. We use the intl package’s Intl.message method to specify the default string, name, and description of the message. The latter is for the benefit of translators, but the message’s name is required by default by intl.

Generating the Messages ARB file

The intl_translation package provides a command line tool to create an ARB (application resource bundle) file. We can run it from our command line, in our project’s root directory, to generate the file.

Note » The command won’t create the lib/l10n directory by itself, and will squawk if it doesn’t find the directory. So make sure to create the directory manually before running the command.

This will generate a lib/l10n/intl_messages.arb file for us. If you open the file, you should see something like the following:

Let’s make a copy of this file for each locale we support. Our app supports English and Arabic, so we can create two copies and name them intl_messages_ar.arb and intl_messages_en.arb. Both files can live in the lib/l10n directory, just like the original ARB file. Now let’s open the English intl_message_en.arb file and add its locale key. When we’re done, it should look like the following:

lib/l10n/intl_messages_en.arb

Similarly, let’s open the Arabic version and edit it, specifying the locale key and adding our translated version of the string.

lib/l10n/intl_messages_ar.arb

Generating the Dart Message Files from the ARB Files

We need a second step to have our messages ready to use by our app. The intl_translation package provides a command that generates Dart code files from our ARB files. We can run it like this:

This will generate four additional files in our lib/l10n directory:

  • messages_all.dart
  • messages_ar.dart
  • messages_en.dart
  • messages_messages.dart

We don’t really need to be too concerned with what these files do, since intl and intl_packages are responsible for them, given that we’ve created our ARB files correctly. For the curious ones, however, suffice it to say that these files help to load our localized messages and provide them as a Dart code to our app.

Wiring Up Our Messages Dart File to our Localizations Class

We can now return our SitLocalizations class and un-comment the lines we had commented out before.

lib/src/lang/sit_localizations.dart

Okay, we’re ready to bring in our first translation.

Accessing our Flutter Localization Data

Let’s return to our main App widget and swap out our hard-coded title for the localized one.

lib/src/app.dart

Notice that we can’t use the title param in our MaterialApp‘s constructor anymore. That’s because we won’t have access to our SitLocalizations before the MaterialApp is constructed itself. To deal with that, MaterialApp provides the onGenerateTitle param, which accepts a function that is passed a BuildContext that we can use to access our SitLocalizations. We pull our title message out of the SitLocalizations, and do so again in our home route to pass the title to the HomeScreen.

Now if we go into our device’s settings and switch the language to Arabic, we should see our Arabic title in the home screen when we return to our app.

Also, notice that the app’s direction is right-to-left in Arabic. That is so sweet! Flutter’s built-in support for Arabic and locale direction can save us a lot of time when internationalizing.

Handling Interpolation & Plurals

Our home screen’s app bar has a counter that displays the number of contacts in our list. Let’s internationalize this counter. We’ll have to pass the count in as a param to our message, and we’ll have to handle pluralization. Arabic can be a bit tricky with plurals since it has different plural forms for zero, one, two, three to ten, and eleven and up. Luckily, the intl package can handle all this. Let’s add a plural message to our SitLocalizations class.

bin/src/lang/sit_localizations.dart

The Intl.plural takes the count param, called howMany, as its first param. Note that when using interpolation with Intl messages, we have to provide the arguments we’re accepting as a list to the args param. Just like before, the name param is required for ARB file generation. You can skip the desc param if you want to, although I’m choosing to keep it here.

The pluralization params, zero, one, two, etc. are optional, with the exception of the other param. This gives us great flexibility when dealing with plurals. For example, if we were dealing only with languages that had three plural forms, like English, we could have built our message as follows:

Now let’s run the command to regenerate our ARB file.

This will update our lib/l10n/intl_messages.arb file to look like this:

We can copy our new contactCount key-value pairs to our intl_messages_en.arb and intl_messages_ar.arb files. Our Arabic version will need to be translated and can look like this when we’re done with it:

lib/l10n/intl_messages_ar.arb

We can now re-run our command to generate the Dart code from our ARB files.

Let’s bring it all together by updating our HomeScreen to make use of our new message.

lib/src/screens/home_screen.dart

After importing our sit_localizations.dart file, we can access our current localizations through the handy SitLocalizations.of method. This just returns an instance of SitLocalizations, so we can store it in a variable, l10n. We then use this object to access our new message, passing it the current number of contacts so that it can do its magic.

If we restart the app, we should now see this:

Working With Dates

Our app is starting to look 🌍-friendly, but we still have that “Contact next YYYY/MM/DD” string in each contact row. Let’s get that string i18n-ized, shall we? You’ll know the drill by now. We start by adding a new message to our SitLocalizations class.

lib/src/lang/sit_localizations.dart

Just like before, we do our ARB generation, copy the message to each locale’s ARB file, and generate the Dart files. After that, we can incorporate the message in our ContactListItem widget.

lib/src/widgets/contact_list_item.dart

After we make that swap, the Arabic version of our app should look like the following.

Notice that while the alphabetic part is translated now, we’re still seeing the English date form. We can correct this by using the intl package’s date formatting functions. First, we need to initialize date formatting in our locale loading method.

lib/src/lang/sit_localizations.dart

date_symbol_data_local.dart is imported from the intl package, and its initialzeDateFormatting() function is called in our SitLocalizations.load method.

Note » At time of writing, date formatting seemed to work fine for me even if I didn’t call initialzeDateFormatting(). However, the official intl documentation states that the function should be called at least once before any of the package’s date formatting methods are called. I suppose it’s safe enough to make the call, so I’ve left it in here.

Now let’s update our ContactListItem to use intl’s date formatting.

lib/src/widgets/contact_list_item.dart

intl’s DateFormat class takes a format string and returns a formatter function. We can pass our Date object to this formatter function to get a formatted date in the current locale.

Et voilà!

Note » intl’s documentation covers all the format characters that DateFormat() accepts.

Using Flutter’s Localized Widgets

When we add a new contact in our AddContactScreen, we use Flutter’s own showDatePicker function behind the scenes.

We didn’t have to localize the date picker widget ourselves. The showDatePicker function is part of Flutter’s material library, and the Flutter team has localized it in Arabic and English for us. Several other localizations come out of the box with Flutter, which can be a huge time saver.

I won’t bore you with internationalizing the rest of the app, since it’s basically “rinse and repeat” from here. However, if you want to see the fully internationalized and localized app, check out the Git repo for the completed project.

Do More with intl

The intl library can do more than what we covered here. It can help you work with numbers, genders, bidirectional text, and money. Check out the intl documentation to see how you can save time in your i18n work by using the package.

And Now I Flutter Away (Sorry!)

Writing code to localize your app is one task, but working with translations is a completely different story. Many translations for multiple languages may quickly overwhelm you which will lead to the user’s confusion. Fortunately, PhraseApp can make your life as a developer easier! Feel free to learn more about PhraseApp, referring to the Getting Started Guide.

I hope this article helped you get started with Flutter i18n and l10n. If you have any questions or would like to see different Flutter or non-Flutter i18n topics covered, please let us know in the comments. Happy Coding 🙂

How to Internationalize a Flutter App Ising intl and intl_translation
5 (100%) 5 votes
Comments