Flutter localization and internationalization i18n with examples

Flutter localization and internationalization i18n with examples

If you are a mobile developer, you may have already got the hang of Flutter. It is Google’s open-source UI software development toolkit for building beautiful applications, that are natively compiled for mobile as well as for web and desktop, from a single codebase. The first version of Flutter which was known as “Sky” only ran on Android OS, but now you can develop applications for iOS, Linux, Mac, Windows, and Google Fuchsia as well. Flutter i18n is an important topic for any Flutter developer.

You might be also interested in checking out our Flutter SDK which provides over-the-air support for your apps.

Special thanks to Illia Derevianko, Lokalise developer, for technical review.

    Flutter internationalization and localization

    In this article, we are going to see how we can introduce Flutter i18n to an application. Before diving into the internationalization part, let’s see how to get started with a simple Flutter application.

    The source code for this tutorial can be found on GitHub.

    If you have not installed Flutter on your machine yet, you can do it from hereI will walk you through how to install Flutter on Linux. The easiest way is to use snapd. First, open a terminal and install snapd using the following command:

    sudo apt update

    sudo apt install snapd

    Once you have installed snapd, you can install Flutter using the command below:

    sudo snap install flutter --classic

    Next, open Android Studio and configure an emulator if you need one. You will need Flutter plugins for the IDE to be able to initiate a Flutter project and identify it. Search for the Flutter plugin and install it. Here you will be prompted to install Dart which is also required. After installing the plugins, your IDE is now able to start a Flutter project.

    Installing Flutter on other operating systems is really easy: just follow the steps listed on the Installation page.

    Now that you know the basics of getting started with a Flutter application, let’s move on to Flutter i18n.

    Getting started with Flutter i18n

    Prerequisites

    This tutorial assumes that you have basic knowledge of Flutter and have installed both Android Studio and Flutter on your machine. Furthermore, you will need to have an emulator set up if you don’t have a mobile phone with debugging available. 

    Flutter Packages that enable i18n

    Flutter localizations package

    The flutter_localizations package comes with Flutter and it contains several localization options for Flutter widgets. See the Flutter documentation to see what localization settings are available with the package. 

    Intl library is the most important library on the market for Flutter i18n that is an official Dart package. The intl class is set with the default locale and many methods to access most of the i18n mechanisms. Some of the i18n and l10n facilities available in intl are DateFormat, NumberFormat, BidiFormatter classes, plurals, genders, and most importantly, message translations. 

    Before we move on to the localization process, we need to set the application design for the pages we are going to edit.

    Flutter i18n

    Our application will have the following pages, which we’ll be able to navigate to and from:

    • Personal information page (home page)
    • About page
    • Settings page, where you will be able to choose the languages

    Creating routes

    To make the navigation possible, we need to configure routes in our application. Take the main.dart file and remove everything from the code except for the stateless widget. If you hover over the MaterialApp class you will see that several routing options are available such as onGenerateRoute, InitialRoute, and onGenerateInitialRoute.

    We will be using the onGenerateRoute and InitialRoute here. Let’s put the below code lines in the MaterialApp:

    onGenerateRoute: CustomRouter.allRoutes,
    initialRoute: homeRoute,

    Now, keep in mind that the CustomRouter is a customized class and that is not yet created. allRoutes is a method inside the CustomRouter class. The initialRoute accepts a string value within it. This is the name of the first route to show if any navigators are built. If you hover over the keyword, you will see a whole lot of information from the documentation itself. Here, we have used a constant as homeRoute.

    Our next step is to create the CustomRouter class. Create a subdirectory called routes inside the lib directory. Then create a new file named custom_router.dart in the routes directory and include the following code in it: 

    import 'package:flutter/material.dart';
    import 'package:flutter_localization/pages/about_page.dart';
    import 'package:flutter_localization/pages/home_page.dart';
    import 'package:flutter_localization/pages/not_found_page.dart';
    import 'package:flutter_localization/pages/settings_page.dart';
    import 'package:flutter_localization/router/route_constants.dart';
    
    class CustomRouter {
     static Route<dynamic> generatedRoute(RouteSettings settings) {
       switch (settings.name) {
         case homeRoute:
           return MaterialPageRoute(builder: (_) => HomePage());
         case aboutRoute:
           return MaterialPageRoute(builder: (_) => AboutPage());
         case settingsRoute:
           return MaterialPageRoute(builder: (_) => SettingsPage());
         default:
           return MaterialPageRoute(builder: (_) => NotFoundPage());
       }
     }
    }
    

    Inside the CustomRouter class, we have created a static method called generatedRoute which returns the Route <dynamic>. This route, in turn, is included in the onGenerateRoute. The generatedRoute method also has the RouteSettings in place. What happens here is when we navigate from one page to another, this method will be called, and it will pass the router settings to us. When we manipulate these settings, we’ll get to know the user’s intended navigation page. The switch(settings.name), which contains case statements to choose where to navigate based on the user’s choice, is used for this purpose. 

    Create another file route_names.dart in the same directory (routes). This file will include all the route name constants we are using for the pages:

    const String homeRoute = "home";
    const String aboutRoute = "about";
    const String settingsRoute = "settings";

    We are adding these constants for flexibility of the application as these are used in several places.

    Creating pages

    Now, to include all the pages we are going to design, we’ll create another subdirectory for pages inside the lib folder. Create the following files in that subdirectory:

    • home_page.dart
    • about_page.dart
    • settings_page.dart
    • not_found_page.dart

    Include the following code in the home_page.dart:

    import 'package:flutter/material.dart';
    import 'package:flutter_localization/classes/language.dart';
    import 'package:flutter_localizationlocalization/language_constants.dart';
    import 'package:flutter_localization/main.dart';
    import 'package:flutter_localization/router/route_constants.dart';
    
    class HomePage extends StatefulWidget {
     HomePage({Key key}) : super(key: key);
    
     @override
     _HomePageState createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
     final GlobalKey<FormState> _key = GlobalKey<FormState>();
     void _changeLanguage(Language language) async {
       Locale _locale = await setLocale(language.languageCode);
       MyApp.setLocale(context, _locale);
     }
    
     void _showSuccessDialog() {
       showTimePicker(context: context, initialTime: TimeOfDay.now());
     }
    
     @override
     Widget build(BuildContext context) {
       return Scaffold(
         appBar: AppBar(
    
         ),
         drawer: Drawer(
           child: _drawerList(),
         ),
         body: Container(
           padding: EdgeInsets.all(20),
           child: _mainForm(context),
         ),
       );
     }
    

    Now let’s create the main form of our application as follows:

    Form _mainForm(BuildContext context) {
        return Form(
          key: _key,
          child: Column(
            children: <Widget>[
              Container(
                height: MediaQuery.of(context).size.height / 4,
                child: Center(
                  child: Text(S.of(context).homePageMainFormTitle,
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 30,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
              TextFormField(
                validator: (val) {
                  if (val.isEmpty) {
                    return S.of(context).formFieldRequired;
                  }
                  return null;
                },
                decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  labelText: S.of(context).formFieldName,
                  hintText: S.of(context).formFieldNameHint,
                ),
              ),
              SizedBox(
                height: 10,
              ),
              TextFormField(
                validator: (val) {
                  if (val.isEmpty) {
                    return S.of(context).formFieldRequired;
                  }
                  return null;
                },
                decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  labelText: S.of(context).formFieldEmail,
                  hintText: S.of(context).formFieldEmailHint,
                ),
              ),
              SizedBox(
                height: 10,
              ),
              TextFormField(
                decoration: InputDecoration(
                    border: OutlineInputBorder(),
                    hintText: S.of(context).formFieldDOB),
                onTap: () async {
                  FocusScope.of(context).requestFocus(FocusNode());
                  await showDatePicker(
                    context: context,
                    initialDate: DateTime.now(),
                    firstDate: DateTime(DateTime.now().year),
                    lastDate: DateTime(DateTime.now().year + 20),
                  );
                },
              ),
              SizedBox(
                height: 10,
              ),
              MaterialButton(
                onPressed: () {
                  if (_key.currentState.validate()) {
                    _showSuccessDialog();
                  }
                },
                height: 50,
                shape: StadiumBorder(),
                color: Theme.of(context).primaryColor,
                child: Center(
                  child: Text(
                    S.of(context).formFieldSubmitInfo,
                    style: TextStyle(color: Colors.white, fontSize: 20),
                  ),
                ),
              )
            ],
          ),
        );
      }
    

    And then we will include the navigations for the Settings page and the About us page:

     Container _drawerList() {
       TextStyle _textStyle = TextStyle(
         color: Colors.white,
         fontSize: 24,
       );
       return Container(
         color: Theme.of(context).primaryColor,
         child: ListView(
           padding: EdgeInsets.zero,
           children: <Widget>[
             DrawerHeader(
               child: Container(
                 height: 100,
                 child: CircleAvatar(),
               ),
             ),
             ListTile(
               leading: Icon(
                 Icons.info,
                 color: Colors.white,
                 size: 30,
               ),
               title: Text(
                 'about_us',
                 style: _textStyle,
               ),
               onTap: () {
                 // To close the Drawer
                 Navigator.pop(context);
                 // Navigating to About Page
                 Navigator.pushNamed(context, aboutRoute);
               },
             ),
             ListTile(
               leading: Icon(
                 Icons.settings,
                 color: Colors.white,
                 size: 30,
               ),
               title: Text(
                 'settings',
                 style: _textStyle,
               ),
               onTap: () {
                 // To close the Drawer
                 Navigator.pop(context);
                 // Navigating to About Page
                 Navigator.pushNamed(context, settingsRoute);
               },
             ),
           ],
         ),
       );
     }
    }
    

    Switching between locales with the dropdown menu

    In the build widget of the home page, add the following code in the AppBar as below:

    appBar: AppBar(
     title: Text( 'home page'),
     actions: <Widget>[
       Padding(
         padding: const EdgeInsets.all(8.0),
         child: DropdownButton<Language>(
           underline: SizedBox(),
           icon: Icon(
             Icons.language,
             color: Colors.white,
           ),
           onChanged: (Language language) {
             _changeLanguage(language);
           },
           items: Language.languageList()
               .map<DropdownMenuItem<Language>>(
                 (e) => DropdownMenuItem<Language>(
                   value: e,
                   child: Row(
                     mainAxisAlignment: MainAxisAlignment.spaceAround,
                     children: <Widget>[
                       Text(
                         e.flag,
                         style: TextStyle(fontSize: 30),
                       ),
                       Text(e.name)
                     ],
                   ),
                 ),
               )
               .toList(),
         ),
       ),
     ],
    )
    

    Now, we have to create a separate file to handle all the languages. We’ll create a subdirectory in the lib directory, this time named “classes”, and create a language.dart file containing the following code in it:

    class Language {
     final int id;
     final String flag;
     final String name;
     final String languageCode;
    Language(this.id, this.flag, this.name, this.languageCode);
    
     static List<Language> languageList() {
       return <Language>[
         Language(1, "????????", "French", "fr"),
         Language(2, "????????", "English", "en"),
         Language(3, "????????", "اَلْعَرَبِيَّةُ‎", "ar"),
       ];
     }
    }
    

     

    Note that we’ll be creating our application to incorporate 3 languages, namely, French, English, and Arabic. The flag emojis are obtained from https://flagpedia.net/emoji, which has the flag emojis of each country or language.

    Everything is set in the application, so now we have to integrate it with internationalization. You can also refer to the official Flutter i18n documentation which provides a rich description of the functions available. As of February 2020, the Flutter localization package supported 77 languages.

    Using the Flutter localizations package

    In order to use this package, you have to add it to the pubspec.yaml file as a dependency. Copy-paste the below code to accomplish this:

    dependencies:
     flutter:
       sdk: flutter
     flutter_localizations:
       sdk: flutter
    

    After adding the dependency to our application, we then have to use it in the main.dart file by importing it as follows:

    import 'package:flutter_localizations/flutter_localizations.dart';

    Next, we have to specify localizationsDelegates and supportedLocales for MaterialApp

    supportedLocales: [
     Locale("en", "US"),
     Locale("fr", "FR"),
     Locale("ar", "SA"),
    ],
    localizationsDelegates: [
     DemoLocalization.delegate,
     GlobalMaterialLocalizations.delegate,
     GlobalWidgetsLocalizations.delegate,
     GlobalCupertinoLocalizations.delegate,
    ],
    

    Elements included in the localizationsDelegates list are the factories that provide collections of localized values. For instance, our application will contain a localized calendar which is produced by localizationsDelegates

    Now what we need to do is to create a callback resolution. Here, we will use the localeResolutionCallback which is responsible for computing the locale of the app’s Localizations object when the app starts and when the user changes the default locale for the device.

    localeResolutionCallback: (locale, supportedLocales) {
     for (var supportedLocale in supportedLocales) {
       if (supportedLocale.languageCode == locale.languageCode &&
           supportedLocale.countryCode == locale.countryCode) {
         return supportedLocale;
       }
     }
     return supportedLocales.first;
    }
    

    In the function provided here, the device’s default location is identified and if it is supported, the app will launch the content in that particular language. If the device’s locale is not supported, the app content is loaded with the first supported locale.

    Making it stateful

    If you now select a language from the dropdown and try to see the changes, it will not work as the MyApp class in the main.dart is still a StatelessWidget. We need to modify it as a StatefulWidget so that we can enable locale changes. You can simply do this by placing your cursor on the StatelessWidget, pressing Alt+Enter, and then clicking on “Convert to StatefulWidget”. This way, you don’t need to bother with the boilerplate code as this will be automatically created for you.

    class MyApp extends StatefulWidget {
     const MyApp({Key key}) : super(key: key);
     static void setLocale(BuildContext context, Locale newLocale) {
       _MyAppState state = context.findAncestorStateOfType<_MyAppState>();
       state.setLocale(newLocale);
     }
    
     @override
     _MyAppState createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
     Locale _locale;
     setLocale(Locale locale) {
       setState(() {
         _locale = locale;
       });
     }
    //rest of the code
    

    To make this easier and avoid this issue, let’s create some constants in another file called language_constants.dart in the localization directory.

    import 'package:flutter/material.dart';
    import 'package:shared_preferences/shared_preferences.dart';
    
    const String LAGUAGE_CODE = 'languageCode';
    
    //languages code
    const String ENGLISH = 'en';
    const String FRENCH = 'fr';
    const String ARABIC = 'ar';
    const String HINDI = 'hi';
    
    Future<Locale> setLocale(String languageCode) async {
     SharedPreferences _prefs = await SharedPreferences.getInstance();
     await _prefs.setString(LAGUAGE_CODE, languageCode);
     return _locale(languageCode);
    }
    
    Future<Locale> getLocale() async {
     SharedPreferences _prefs = await SharedPreferences.getInstance();
     String languageCode = _prefs.getString(LAGUAGE_CODE) ?? "en";
     return _locale(languageCode);
    }
    
    Locale _locale(String languageCode) {
     switch (languageCode) {
       case ENGLISH:
         return Locale(ENGLISH, 'US');
       case FRENCH:
         return Locale(FRENCH, "FR");
       case ARABIC:
         return Locale(ARABIC, "SA");
       default:
         return Locale(ENGLISH, 'US');
     }
    }
    

    Here, the app will depend on a source file called intl/l10n.dart and it will define all the strings in the application that are localizable. This is a two-step process.

    import 'package:flutter/material.dart';
    import 'package:intl/intl.dart';
    import 'intl/messages_all.dart';
    
    class S {
     S();
    
     static S current;
    
     static const AppLocalizationDelegate delegate =
       AppLocalizationDelegate();
    
     static Future<S> load(Locale locale) {
       final name = (locale.countryCode?.isEmpty ?? false) ? locale.languageCode : locale.toString();
       final localeName = Intl.canonicalizedLocale(name);
       return initializeMessages(localeName).then((_) {
         Intl.defaultLocale = localeName;
         S.current = S();
        
         return S.current;
       });
     }
    
     static S of(BuildContext context) {
       return Localizations.of<S>(context, S);
     }
    
     /// `Home Page`
     String get homePageAppBarTitle {
       return Intl.message(
         'Home Page',
         name: 'homePageAppBarTitle',
         desc: '',
         args: [],
       );
     }
    
     /// `Personal Information`
     String get homePageMainFormTitle {
       return Intl.message(
         'Personal Information',
         name: 'homePageMainFormTitle',
         desc: '',
         args: [],
       );
     }
    
     /// `Name`
     String get formFieldName {
       return Intl.message(
         'Name',
         name: 'formFieldName',
         desc: '',
         args: [],
       );
     }
    
     /// `Enter your name`
     String get formFieldNameHint {
       return Intl.message(
         'Enter your name',
         name: 'formFieldNameHint',
         desc: '',
         args: [],
       );
     }
    
     /// `Email`
     String get formFieldEmail {
       return Intl.message(
         'Email',
         name: 'formFieldEmail',
         desc: '',
         args: [],
       );
     }
    
     /// `Enter your email`
     String get formFieldEmailHint {
       return Intl.message(
         'Enter your email',
         name: 'formFieldEmailHint',
         desc: '',
         args: [],
       );
     }
    
     /// `Date of Birth`
     String get formFieldDOB {
       return Intl.message(
         'Date of Birth',
         name: 'formFieldDOB',
         desc: '',
         args: [],
       );
     }
    
     /// `Required Field`
     String get formFieldRequired {
       return Intl.message(
         'Required Field',
         name: 'formFieldRequired',
         desc: '',
         args: [],
       );
     }
    
     /// `Submit Info`
     String get formFieldSubmitInfo {
       return Intl.message(
         'Submit Info',
         name: 'formFieldSubmitInfo',
         desc: '',
         args: [],
       );
     }
    
     /// `About Us`
     String get aboutUsPageAppBarTitle {
       return Intl.message(
         'About Us',
         name: 'aboutUsPageAppBarTitle',
         desc: '',
         args: [],
       );
     }
    
     /// `Settings`
     String get settingsPageAppBarTitle {
       return Intl.message(
         'Settings',
         name: 'settingsPageAppBarTitle',
         desc: '',
         args: [],
       );
     }
    
     /// `Change Language`
     String get formFieldChangeLanguage {
       return Intl.message(
         'Change Language',
         name: 'formFieldChangeLanguage',
         desc: '',
         args: [],
       );
     }
    
     /// `Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s.`
     String get formFieldAbout {
       return Intl.message(
         'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s.',
         name: 'formFieldAbout',
         desc: '',
         args: [],
       );
     }
    }
    
    class AppLocalizationDelegate extends LocalizationsDelegate<S> {
     const AppLocalizationDelegate();
    
     List<Locale> get supportedLocales {
       return const <Locale>[
         Locale.fromSubtags(languageCode: 'en', countryCode: 'US'),
         Locale.fromSubtags(languageCode: 'ar', countryCode: 'SA'),
         Locale.fromSubtags(languageCode: 'fr', countryCode: 'FR'),
       ];
     }
    
     @override
     bool isSupported(Locale locale) => _isSupported(locale);
     @override
     Future<S> load(Locale locale) => S.load(locale);
     @override
     bool shouldReload(AppLocalizationDelegate old) => false;
    
     bool _isSupported(Locale locale) {
       if (locale != null) {
         for (var supportedLocale in supportedLocales) {
           if (supportedLocale.languageCode == locale.languageCode) {
             return true;
           }
         }
       }
       return false;
     }
    }
    

    Extracting to ARB files

    First, you have to create the intl/l10n.arb from lib/main.dart using the following command:

    flutter pub run intl_translation:extract_to_arb --output-dir=lib/intl lib/main.dart

    What this command does is produce a file as intl_messages.arb containing all the messages from the program. It is an ARB format map where we have one entry for each and every Intl.message() function that is defined in the main.dart file. This ARB format file can be used as input for translation tools such as the Google Translator Toolkit. This file is also a template for the translations used. For example, intl_en_US.arb, intl_ar_SA.arb, and intl_fr_FR.arb, which represent the English, Arabic, and French translations respectively.

    Secondly, generate the intl_messages_<locale>.dart for all intl_<locale>.arb files and the intl_messages_all.dart file, which will import all the messages.

     flutter pub run intl_translation:generate_from_arb \
      --output-dir=lib/l10n --no-use-deferred-loading \
      lib/main.dart lib/l10n/intl_*.arb

    In the above command the file names have been wildcarded. However, as Windows doesn’t support wildcarding, you will have to list the .arb files that were previously created by the intl_translation:extract_to_arb command and your command will change as below:

    flutter pub run intl_translation:generate_from_arb \
     --output-dir=lib/l10n --no-use-deferred-loading \
     lib/main.dart \
     lib/l10n/intl_en_US.arb lib/l10n/intl_fr_FR.arb lib/l10n/intl_messages.arb
    

    Now, let’s write the translation files.

    intl_en_US.arb

    {
     "@@locale": "en_US",
     "homePageAppBarTitle": "Home Page",
     "homePageMainFormTitle": "Personal Information",
     "formFieldName": "Name",
     "formFieldNameHint": "Enter your name",
     "formFieldEmail": "Email",
     "formFieldEmailHint": "Enter your email",
     "formFieldDOB": "Date of Birth",
     "formFieldRequired": "Required Field",
     "formFieldSubmitInfo": "Submit Info",
     "aboutUsPageAppBarTitle": "About Us",
     "settingsPageAppBarTitle": "Settings",
     "formFieldChangeLanguage": "Change Language",
     "formFieldAbout": "Lorem Ipsum is simply a dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s."
    }
    

    intl_ar_SA.arb

    {
     "@@locale": "ar_SA",
     "homePageAppBarTitle": "الصفحة الرئيسية",
     "homePageMainFormTitle": "معلومات شخصية",
     "formFieldName": "اسم",
     "formFieldNameHint": "أدخل أسمك",
     "formFieldEmail": "البريد الإلكتروني",
     "formFieldEmailHint": "أدخل بريدك الإلكتروني",
     "formFieldDOB": "تاريخ الولادة",
     "formFieldRequired": "يتطلب حقلا",
     "formFieldSubmitInfo": "إرسال المعلومات",
     "aboutUsPageAppBarTitle": "معلومات عنا",
     "settingsPageAppBarTitle": "إعدادات",
     "formFieldChangeLanguage": "تغيير اللغة",
     "formFieldAbout": "لوريم إيبسوم هو ببساطة نص شكلي يستخدم في صناعة الطباعة والتنضيد. كان Lorem Ipsum هو النص الوهمي القياسي في الصناعة منذ القرن الخامس عشر الميلادي."
    }
    

    intl_fr_FR.arb

    {
     "@@locale": "fr_FR",
     "homePageAppBarTitle": "Page d'accueil",
     "homePageMainFormTitle": "Informations personnelles",
     "formFieldName": "Nom",
     "formFieldNameHint": "Entrez votre nom",
     "formFieldEmail": "Email",
     "formFieldEmailHint": "Entrer votre Email",
     "formFieldDOB": "Date de naissance",
     "formFieldRequired": "Champs requis",
     "formFieldSubmitInfo": "Soumettre des informations",
     "aboutUsPageAppBarTitle": "À propos de nous",
     "settingsPageAppBarTitle": "Réglages",
     "formFieldChangeLanguage": "Changer de langue",
     "formFieldAbout": "Lorem Ipsum est simplement un texte factice de l'industrie de l'impression et de la composition. Lorem Ipsum est le texte factice standard de l'industrie depuis les années 1500."
    }
    

    Generating the Dart message files from the ARB files

    We have to set our messages to be ready to use. The intl package gives a command that will generate Dart code files from the ARB files as shown here:

    flutter pub run intl_translation:generate_from_arb lib/src/lang/l10n.dart lib/l10n/*.arb  --output-dir=lib/intl

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

    • messages_all.dart
    • messages_ar_SA.dart
    • messages_en_US.dart
    • messages_fr_FR.dart

    As we have already created the ARB files correctly, we don’t need to be bothered with what these files accomplish. The main reason is that the intl package is responsible for those. If you really want to know, these files assist in loading the localized messages. They give them to the app we created as a Dart code and this is what the messages_all.dart file look like:

    import 'dart:async';
     
    import 'package:intl/intl.dart';
    import 'package:intl/message_lookup_by_library.dart';
    import 'package:intl/src/intl_helpers.dart';
     
    import 'messages_ar_SA.dart' as messages_ar_sa;
    import 'messages_en_US.dart' as messages_en_us;
    import 'messages_fr_FR.dart' as messages_fr_fr;
     
    typedef Future<dynamic> LibraryLoader();
    Map<String, LibraryLoader> _deferredLibraries = {
     'ar_SA': () => new Future.value(null),
     'en_US': () => new Future.value(null),
     'fr_FR': () => new Future.value(null),
    };
     
    MessageLookupByLibrary _findExact(String localeName) {
     switch (localeName) {
       case 'ar_SA':
         return messages_ar_sa.messages;
       case 'en_US':
         return messages_en_us.messages;
       case 'fr_FR':
         return messages_fr_fr.messages;
       default:
         return null;
     }
    }
     
    /// User programs should call this before using [localeName] for messages.
    Future<bool> initializeMessages(String localeName) async {
     var availableLocale = Intl.verifiedLocale(
       localeName,
       (locale) => _deferredLibraries[locale] != null,
       onFailure: (_) => null);
     if (availableLocale == null) {
       return new Future.value(false);
     }
     var lib = _deferredLibraries[availableLocale];
     await (lib == null ? new Future.value(false) : lib());
     initializeInternalMessageLookup(() => new CompositeMessageLookup());
     messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
     return new Future.value(true);
    }
     
    bool _messagesExistFor(String locale) {
     try {
       return _findExact(locale) != null;
     } catch (e) {
       return false;
     }
    }
     
    MessageLookupByLibrary _findGeneratedMessagesFor(String locale) {
     var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor,
         onFailure: (_) => null);
     if (actualLocale == null) return null;
     return _findExact(actualLocale);
    }
    

    Let’s see some of the screenshots of the app.

    French:

    Arabic:

     

    DateTime, Numbers, and Bidirectional text 

    Some other important aspects when it comes to Flutter i18n is how dates, numbers, genders, and currency are represented. This content and its format can vary depending on the locale being used in the application. This section will describe how to cater to those target user requirements.

    This section will also contain content from the package intl, so make sure you add the dependency for that package and import it before using it.

    Add dependency
    
    dependencies:
      intl: ^0.16.0
    
    Import package
    
    import 'package:intl/intl.dart' as intl;
    

    DateTime

    In what we have already created, the date is provided with the delegate itself. This is how you will see the calendar:

    Flutter i18n

    Flutter i18n

    However, if you want to format the date differently, you will have to use the Dart package intl. 

    DateFormat is for formatting and parsing dates in a locale-sensitive manner. Here, the DateFormat class is used to format dates according to different locales. Not only does it allow the user to choose various standard DateTime formats, but we can also customize the patterns.

    The default en_US locale does not need any initialization to format the date. You can do it using the following code:

    print(new DateFormat.yMMMd().format(new DateTime.now()));

    However, to format the date using other locales, you first have to initialize and then run your code.

    Whatever library you import, you will have to use the initializeDateFormatting method. Here’s how to format the date in French:

    import 'package:intl/date_symbol_data_local.dart';
    initializeDateFormatting("fr_FR", null).then((_) => runMyCode());

    Numbers

    You have to create a NumberFormat instance to format a number according to a specific locale. Note that the locale parameter is optional and if it is not included, the current locale will be automatically used in Flutter i18n.

    var f = NumberFormat('###.0#', 'en_US');
    print(f.format(12.345));

    This will output 12.34.

    There are certain limitations in this instance, such as currency format only printing the name of the currency as symbols are not supported. 

    Bidirectional Text

    Where some countries have unique DateTime formats or numbering formats, others have a unique text writing direction. Countries where Arabic is prevalent, such as Saudi Arabia, present their content from right to left, going against the more conventional way of writing from left to right. These peculiarities also need to be handled before the application is received by the end user. So, let’s see how we can change the direction of content depending on the locale in Flutter. 

    Fortunately, in the Flutter localizations package, the direction of the content is automatically taken care of according to the language. For example, if we configure the application for Arabic, the text’s reading direction automatically changes from left to right to right to left. 

    You can see this change in our application too, when we added the following in creating the stateful widget:

    class _MyAppState extends State<MyApp> {
     Locale _locale;
     setLocale(Locale locale) {
       setState(() {
         _locale = locale;
       });
     }
    //rest of the code
    

    Flutter i18n

    Nonetheless, if you want to do this without the delegations, then you have to use the Dart intl package. Use the BidiFormatter which provides various utilities for working with bidirectional text. Here, we have to wrap the relevant string with Unicode directional indicator characters and the direction is stated by using RTL or LTR, for example:

    BidiFormatter.RTL().wrapWithUnicode('xyz');
    BidiFormatter.RTL().wrapWithSpan('xyz');

    Handling interpolation and plurals

    The intl package can also be used to insert plurals into your application, like so:

    String showUsersCount(int quantity) {
     return Intl.plural(
       quantity,
       zero: 'You have no friends',
       one: '$quantity friend',
       other: '$quantity friends',
       name: "friend",
       args: [quantity],
       examples: const {'quantity': 5},
       desc: "Number of contacts",
     );
     }

    Now all you have to do is to pass your value in the defined showFriendsCount() method and it will return a string of text to show your output.

    Text formatting in ARB files

    It might be quite complex to disable formatted text from the ARB files in your Flutter application. However, Jonas, one of our readers, has suggested an interesting approach. Basically, you should import a Markdown plugin and render text using it. So, first of all, import the flutter_markdown plugin. Next, use the Markdown widget when you need to display a text with inline styling:

    Markdown(
      shrinkWrap: true,
      data: AppLocalizations.of(context)!.markDownDemo,
    ),

    shrinkWrap is not required but often it’s necessary when you replace RichText with Markdown widget.

    Next, inside your ARB file provide inline styling in Markdown format. For example, to make text bold:

    "markdownDemo": "This line has a **bold** word"

    Also, you can provide inline links:

    "markdownDemo": "Raw link: http://example.com \\\nText link: [Sample link](http://example.com)"

    Please note that in this case you have to use \\\n for line breaks: ordinary \n won’t work for Markdown widgets.

    You can also use MarkdownBody() instead of Markdown() to avoid default markdown-padding and scrolling. However, if you would like to display links with MarkdownBody(), you’ll need to handle them properly:

    MarkdownBody(
      onTapLink: (text, href, title) {
        // handle href-url
      },
      data: AppLocalizations.of(context)!.markDownDemo,
    ),

    Basically, that’s it. However, please note that Markdown widgets are slower than generic Text and RichText therefore don’t rely on them heavily. If you know a better solution, don’t hesitate to share it with us (simply drop a line to our support team)!

    Let Lokalise do the localizing!

    As you can see, managing translation files can be quite tedious, especially for larger applications. Meet Lokalise, the translation management system that takes care of all your Flutter internationalization needs and provides full support for ARB files. With features like:

    • Easy integration with various other services (GitHub, Figma, Asana, and many others)
    • Collaborative translations
    • Quality assurance tools for translations
    • Easy management of your translations through a central dashboard
    • Plus, loads of others

    Lokalise will make your life a whole lot easier by letting you expand your Flutter app to all the locales you’ll ever plan to reach.

    Start with Lokalise in just a few steps:

    • Sign up for a free trial (no credit card information required).
    • Log in to your account.
    • Create a new project under any name you like.
    • Upload your translation files and edit them as required.

    That’s all it takes!

    Conclusion

    This article has covered almost everything you need to know about Flutter i18n and l10n on a flutter application. I hope I was able to shed some light on the matter. But, if you have any questions or suggestions, please don’t hesitate to post a comment and let us know. You may also be interested in reading our article on android localization. Thank you and happy coding, everyone!

    Related posts

    Learn something new every two weeks

    Get the latest in localization delivered straight to your inbox.

    Related articles
    Localization made easy. Why wait?
    The preferred localization tool of 2000+ companies