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 here. I 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.
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:
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
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!