Internationalizing and Localizing a Flutter App | How to Develop an App with Flutter – Part 7
Learn how to develop your first app with Flutter. This time, we will make our app multilingual. I will show you how to go through the internationalization process and how to localize your app.
Are you interested in internationalizing and localizing your Flutter app? If yes, you’re in the right place. In this article, we will make our Smoge app multilingual. More precisely, we will explain how to easily localize and internationalize your Flutter app, making it accessible to users in different locales.
Want to build an app with Flutter? Follow our series “Flutter app development tutorial for beginners”. We’ve published the following posts so far:
- Introduction to your first Flutter app development
- Flutter Project Setup
- Creating a Home Screen
- Styling the Home Screen
- Networking and connecting to API
- Refining Widgets’ Layer with Provider
- Internationalizing and Localizing your Flutter App – you are reading this
What’s more, we’ve also prepared a Roadmap that can be useful in your Flutter development journey:
- Roadmap for Flutter developers in 2020 – feel invited to contribute!
This article consists of the following sections:
- What does internationalizing and localizing a Flutter app mean?
- Localization mechanism – Flutter vs native platforms
- Flutter app internationalization
- The tools that simplify Flutter app localization
- intl_utils
- Wrap up
Let’s define a few important words.
What does internationalizing and localizing a Flutter app mean?
- Internationalization (aka i18n) is a necessary setup and process for app localization. It is mostly done by developers.
- Localization (aka l10n) means adding support of multiple locales to the application. This work is mostly done by translators (which may not necessarily be developers).
- Locale (no fancy abbreviation like l4e 😉 ) is a descriptor of a specific region (political, cultural, or geographical). In Flutter (more precisely in Dart) it consists of 3 ingredients:
- language (mandatory), eg. English, Polish or Serbian.
- region (optional; in API it is called country code), eg. Australia, USA, or Hong Kong.
- script (optional), eg. Latin or Cyrillic.
Numbers in fancy abbreviations represent the number of omitted letters. Apart from i18n and l10n, you may also encounter a11y (accessibility) in related contexts.
Localization mechanism – Flutter vs native platforms
Flutter does not have its own localization mechanism similar to native platforms (iOS, Android). In short: in native projects, you can just define the key-value pairs for each supported locale, where key acts as a unique identifier (like settingsScreenName) used later in a source code and value is an actual translation (eg. Settings for English or Rendszer for Hungarian). Read more in the official documentation for Android and iOS respectively.
On the other hand, Flutter does not have such mechanisms available out of the box. Flutter CLI supports Dart’s intl package which can help in app localization. However, it is not so easy to use as built-in support on native platforms.
Flutter app internationalization
The most important requirement is to separate potentially translatable texts from the source code. In other words: translatable texts must not be hardcoded, so instead of code like this:
Text('Measuring points')
You have to use something like this:
Text(Strings.of(context).measuringPoints)
The exact form depends on the chosen internationalization technique. This one is from intl_utils (which will be described in the next paragraphs).
Many internationalization frameworks for Flutter including the built-in intl package use the ARB (Application Resource Bundle) file format based on JSON. More information about ARB can be found in the specification.
It is important to follow this approach since the beginning of the project. The later massive transformation of hardcoded text into translatable resources is almost always more error-prone and time-consuming. Sometimes, further changes require even code refactor. This may happen especially if texts contain placeholders and/or plurals.
In the next few paragraphs, I describe the common aspects related to internationalization. Not everything occurs in every app nor it’s a completely exhaustive list. However, it should cover most of the cases which can be encountered during app development.
Design
Let’s assume that the app displays an English text: Always allow Wi-Fi Roam Scans and it looks very well. So let’s translate it into Polish. Now, the text will be Zawsze szukaj Wi-Fi w roamingu, but it may look bad. Keep in mind that translated text may be shorter or longer than English (or another default language of your app).
All the text fields have to be configured to handle that. Depending on the context, you may need to increase the number of displayed lines or enable various overflow like an ellipsis (three dots at the end). Please note that translation may contain dîąćŗĩţĭċš so it may also grow vertically.
Placeholders
Let’s say that we need a text like Gmail application is running. Where Gmail represents a variable value evaluated somehow by the business logic. One may think that such cases may be easily implemented like this:
Text('$appName ${Strings.of(context).runningLabel}')
So we take a variable, a hardcoded space character, and translatable text referred by key. Well, nothing can be further from the truth!
That text translated to other languages may use a different order of the words, for example in Polish it will be Aplikacja Gmail jest uruchomiona.
So it is important to treat the entire text as one translatable unit with placeholders instead of manual crafting in the code. The mentioned sample text can be expressed like this in ARB format:
- English:
"runningLabel" : "{appName} is running"
- Polish:
"runningLabel" : "Aplikacja {appName} jest uruchomiona"
And it may be used like this:
Text(Strings.of(context).runningLabel(appName)
Plurals
Let’s consider the following text: 3 songs found, where 3 is a dynamic value evaluated in the app logic. It seems to be analogous to the previous example. However, it isn’t… In the case of a single song, we have 1 song(not songs!) found.
Languages other than English may even have more plural forms eg. in Polish we have: Znaleziono 1 piosenkę, Znaleziono 2 piosenki and Znaleziono 5 piosenek respectively. In some cases (eg. in Arabic) there are even 6 plural forms!
Plural definition in ARB format looks like this:
{num, plural, one{A song found} other{{num} songs found}}
Note that although plural quantity names (like one or other in this example) are standardized, the number ranges they cover depends on the language. For example, in Polish one is used only for 1 while in Croatian it applies to every number ending with 1 except 11. You can find exact quantity mappings in the official Unicode CLDR documentation. Also, note that the number does not necessarily need to be additionally a placeholder (we don’t use num
in one quantity).
Select
Similarly to numbers, we can parametrize texts by the argument of any type:
"weather": "{weather, select, sunny{The sun is shining} cloudy{There are clouds} rainy{Take an umbrella} other{No clue what the weather is}}"
The other case must be provided and it acts as a fallback for any undefined case passed from the code. Note that at the time of writing the documentation says that other cases may be omitted and an empty string is returned when there is no mapping found. See this issue for more information.
On the Dart source code side, the argument (weather in this case) is passed as an Object. Note that the cases should be unique. If there are duplicates the result depends on the used tool. The intl_utils for example takes the first one and raises a warning (not an error) so the building process does not fail.
Genders
Texts can also be parametrized by gender:
"invitation": "{sex, select, male{He invites you} female{She invites you} other{They invite you}}"
In fact, it is only a special case of the above-mentioned select feature. If (apart from other) there are only male and female cases then the text is treated as gender on the Flutter side (at least when using intl_utils to parse ARB).
Note that there are only 3 genders supported: male, female and other. In Dart source code, gender is passed by string, not by Object like in the aforementioned select feature, and it is the only difference in practice. Due to the fact gender is a select feature, the other case is also effectively mandatory. Even if your app logic supports only 2 genders, you have to provide 3 variants of gendered texts, also you can’t add more.
Let’s go to another aspect of internationalizing and localizing your Flutter app.
Numbers
In the source code we usually write numbers like this:
12345678901.2
However, on the UI they should appear formatted. The exact format depends on the context eg. we may want a compact format, 2, 3, or no decimal places, etc. Moreover, it depends on the locale as different languages use a variety of decimal and thousand separator combinations. What is more, a locale determines the usage of either long or a short scale.
The aforementioned number may be represented for example like this:
- 12,345,678,901.2 – US English, decimal.
- ١٢٬٣٤٥٬٦٧٨٬٩٠١٫٢ – Egypt Arabic decimal.
- 12 345 678 901,2 – Polish, decimal, note that there are non-breaking spaces.
- 12.3 billion – US English, compact long.
- 12,3 miliarda – Polish, compact long (note that Polish uses long scale).
There are also other kinds of formats like scientific or percent. You can find the complete list in the NumberFormat class documentation.
Currencies
Similarly to numbers, we can also use NumberFormat class to format currency values. Note that apart from the locale, it also depends on the currency, eg. Japan yen has no decimal digits while the Omani rial has 3 of them.
For example, the value 12,345,670,000 (in millionths of a currency unit as defined in MicroMoney class) may appear as:
- 12 345,67 Kč – Czech koruna, simple format.
- ١٢٬٣٤٥٫٦٧ EGP – Egyptian pound using Eastern Arabic numerals, regular format.
Note that (at the time of writing) there are 2 currencies in the world (Mauritanian ouguiya and Malagasy ariary) which units are divided into 5 subunits, not to 100 or 1000 like any other currency having subunits. However, in terms of programming (including Dart/Flutter), usually, only subunits which are powers of 10 are supported, so they are treated like having 100 subunits.
Warning: The floating-point types (like Dart’s double) should not be used for mathematics on currency types! It may lead to subtle errors eg. adding 0.1 (expressed as double) ten times gives 0.999… not 1.0.
The aforementionedMicroMoney class can only be used for number formatting. Flutter standard library does not contain a dedicated decimal or monetary type which can be used for mathematics. If you need to perform such calculations, you can do that on integral subunits (cents or millionths), or use some 3rd party library from the pub.
Dates and times
Similarly to numbers, the date formatting also depends on the locale. Unlike pure numbers, there are more kinds of differences between various data formats. For example, November 23th 2020 may be represented as among the others:
- 11/23/2020 – US English,
yMd
format (year Month day). - 23.11.2020 – Polish,
yMd
. - 23 listopada – Polish,
dd MMMM
. - 23 November – English,
dd MMMM
. - 23 listopad – Polish,
dd LLLL
(L stands for a standalone month). - 23 November – English,
dd LLLL
.
Apart from separators and word translations, we have to take into consideration:
- order in which day month and year is written,
- whether 12- or 24-hours clock is used,
- whether a month is written as a name, number, or a Roman numeral.
Some languages (eg. Polish but not English) also make a distinction whether the month is a standalone word or appears near the day.
Find more information about the available date and time formats in the DateFormat class documentation.
Units of measurement
Most of the world uses the metric system (SI) with units like (kilo)meters for length, grams for mass and Celsius degrees for temperature. However, a few countries (USA, Liberia, and Myanmar) use imperial system with units like miles for length, pounds for mass, and Fahrenheit degrees for temperature. Some countries like the United Kingdom may use both of them. It may depend on the context in which one should be used.
Note that unlike aforementioned dates, numbers, and currencies where raw value is always the same (eg. the source of both 1,50 zł and $1.50 is a 1.5) in case of different measurement systems also the value has to be calculated separately eg. 1.5 km is approximately 0.93 mi.
There is no support for units of measurement conversion nor formatting in the standard library but various 3rd party packages are available on pub.dev.
Right To Left (RTL)
This text is written in English using the Latin alphabet. It uses left to right text direction (LTR). However, some alphabets (or more precisely scripts) are written right to left (RTL).
Take a look at the examples:
English:
Hello
Arabic (look on the right side):
مرحبا
Note that not only is the text direction different (the first letter – مـ is on the right of the text) but also the layout direction (the entire block of text is on the right of the screen). In general, you should not think about the left and right sides but rather the start and end.
The aforementioned sides are usually used with margins, paddings, alignments, etc. In Flutter standard library, class names supporting RTL use a Directional suffix. For example, we have EdgeInsetsDirectional or BorderRadiusDirectional. For text direction, there is a TextAlign enum which – as the name suggests – determines the text alignment.
Information whether the start is on the physical left or right side, is primarily taken from the BuildContext with the help of Directionality class. In the case of text, apart from its alignment, we can also explicitly set its direction. Text displaying is a complicated topic. We can have eg. Arabic text (written normally right to left) with embedded English proper names (written left to right). Flutter standard library has a Bidi class with a set of methods that can help handle such cases.
Note that although Flutter supports directional properties, it does not mean that they should be blindly used everywhere in the code, even if your app supports RTL locales. For purely decorative elements (excluding those connected to direction like back arrows), you should use visual properties (those without Directional suffix).
Phone numbers
Phone numbers are also formatted differently depending on the country and other factors eg. whether it is dialed domestically or internationally. For example:
- (541) 754-3010 – domestic USA
- +1-202-555-0108 – international USA
- 123 456 789 – domestic Poland
There are some falsehoods programmers believe about phone numbers.
There is no library having capabilities like libphonenumber for native platforms. However, some unofficial ports exist.
Images
Sometimes you may want to localize assets. For example, if the image contains text or currency. There is no built-in mechanism for such use cases in Flutter. However, you can construct asset paths dynamically like this:
Image.asset('assets/images/${Localizations.localeOf(context)}/icon.png')
Legal issues
Depending on geographical region (not necessarily a country) the laws may be different. For example in the European Union, we have to follow GDPR, while in Brazil there is LGPD, CCPA applies in California (USA), etc. It may impact necessary consents obtained from users and/or displayed legal notices, disclaimers, terms of services, etc.
Some countries may require additional actions, for example when dealing with sensitive or medical data (eg. HIPAA in the USA), gambling or interacting with children (eg. COPPA in the USA). Sometimes, you may need to display different content in different locations because of licensing, copyright, price discrimination, etc. Eventually, you may be even enforced to geo-block some content in the app.
Implementation
To achieve working locale-dependent APIs, we need to specify which locales do we support and provide the localization delegates with actual implementation. Usually, we don’t need to implement the latter by hand as they can be provided by libraries. In the case of the Smoge app we need to extend the MaterialApp instantiation like this:
1 2 3 4 5 6 7 8 9 | MaterialApp( localizationsDelegates: [ Strings.delegate, ...GlobalMaterialLocalizations.delegates ], supportedLocales: Strings.delegate.supportedLocales, theme: AppThemeDataFactory.prepareThemeData(), home: NavigationContainer(), ); |
Note that GlobalMaterialLocalizations are provided by the official Material library. Not to be confused with DefaultMaterialLocalizations which are English-only. Strings class is generated by intl_utils which will be described in the next paragraphs.
Native resources
Some of the properties cannot be changed at the Flutter level and need to be done natively. This includes at least the app name – label shown near the icon on the home screen and in various places like the recent apps screen on Android.
For iOS, the label can be set by adding CFBundleDisplayName property to ios/Runner/Info.plist
file. If the app name itself has to be localized, its translations can be added to InfoPlist.strings files for the appropriate locale.
Note that if your app needs privileged access eg. to camera or device location (not to be confused with localization 😉 ), then you also need to provide the messages for each such permission. It can be done analogously to the app title.
Additionally, on iOS it is mandatory to list locales supported by the app (apart from doing the same at Flutter Level). This can be done by the CFBundleLocalizations array in the mentioned Info.plist file.
On Android, the label is set using the label application manifest attribute. In order to localize it use the string resource instead of a hardcoded value.
Moreover, some functionality of the Flutter app may be provided by plugins and implemented separately for Android and iOS. If there is some content that needs to be localized there you have to use some platform-specific technique.
See official documentation for Android and iOS respectively for more details about localizing native resources.
The tools that simplify Flutter app localization
There are many 3rd party tools available that can simplify Flutter app localization. Here are some popular ones at the time of writing this article. If you are reading this months or years after publication then the list may be out of date, new solutions will appear, some may be abandoned and forgotten.
List of Flutter localization tools (November 2020):
For our Smoge app, we have chosen the latest one – intl_utils. It is simple to use, supports the device’s locale changes (unlike eg. easy_localization – see an issue), integrates well with Localizely – a web service with GUI for localization, has Flutter pub CLI bindings and dedicated IDE plugins (for both Android Studio and VS Code).
intl_utils
Setup
The first step of integrating intl_utils is to enable it in pubspec.yaml:
1 2 3 4 5 | flutter_intl: enabled: true class_name: Strings localizely: project_id: 39cf3f3a-a154-4d3f-85b5-f57e71774f3e |
Note that apart from enabling we also configure 2 more optional settings. We set the generated delegate class name to Strings (without that it is by default called S). We also set a project id on Localizely so we can manage the translations using an online editor. You can find the complete list of options in the official documentation. Note that project ID can be safely committed to the repo and even published.
Additionally, we need to enable Material library translations by adding the following entries to the dependencies section:
1 2 3 4 | intl: 0.16.1 intl_translation: 0.17.10+1 flutter_localizations: sdk: flutter |
The last step of setup is to configure the static code analysis to exclude generated sources related to the localization. This can be done by adding the following entry to analyzer -> exclude list in analysis_options.yaml:
- 'lib/generated/**'
Localization
Translations are located in the lib/l10n
directory, in locale-specific ARB files named using intl_<locale>.arb
scheme. Where locale is eg. en (English) or es_MX (Spanish, Mexico).
Files contain JSONs like this:
1 2 3 4 5 6 7 8 9 | "@@locale": "en", "airQualityNorm": "norm", "@airQualityNorm": { "description": "Air quality norm percentage label" }, "percentage": "{value}%", "@percentage": { "description": "Pollution percentage norm scheme" }, more keys... |
At the beginning, we have a locale specifier (usually matching file name) and then actual translations. Each one can have an associated description which can help translators to find an actual context.
For example, the English word book may be translated into French as either livre (noun) or réserver (verb). Without a context, the translators may not be able to choose the correct translation, or even worse they can infer an incorrect one.
Note that if you provide region-specific translations (like Spanish, Mexico), you should also consider providing a generic region-agnostic file specifying only a language (es – Spanish in this case). Without that, if a user has set the Spanish language, but a country other than Mexico, the resolution mechanism will fall back to the default locale (by default English). It won’t try other Spanish variants as it can happen on Android.
Generating sources
Actual Dart source files can be generated out of ARB using either Flutter CLI (useful on CI) or by IDE plugins.
Another question is whether generated files should be committed to VCS or not. If they aren’t, new developers need to know how to generate them before they first build the project. Otherwise, they will get compilation errors.
Localizely
Intl_utils also integrates well with Localizely (it’s a company behind it). It provides a spreadsheet-like graphical online translations editor. Usually more convenient than raw JSONs in ARB files. Keep in mind, that professional translators may not be technical.
You need to generate an API token in the Localizely console in order to communicate with its API. That token is saved in your home directory, so there is no need to gitignore it.
There is also an OTA (Over-The-Air) translations update available. However, in the Smoge app, we don’t use it.
Note that Localizely in general is a commercial, paid service. At the time of writing It can be free of charge for open source projects or small closed source ones (up to 250 string keys). See pricing for more details. Disclaimer: we are not affiliated with Localizely.
Internationalizing and localizing a Flutter app – wrap up
Internationalization is not a trivial process. It involves not only texts themselves but also numbers, dates, sometimes images, or even functionalities of the app. There are tools like intl_utils or Localizely which can help. However, it is very important to think about localization from the very beginning. Internationalizing an already done app is much harder and error-prone.
You can find a complete example in the Smoge app repository on GitHub. Changes related to internationalization were added in pull request #10.
We hope that, whether you want to be a freelancer or work at a Flutter development company, our series will help you to become a Flutter developer and develop your first Flutter app.
The post is written in cooperation with Wrocław Java User Group.
About the author
Want to develop a mobile app with Flutter?
Receive the first working demo within 7 days from the project kick-off
Hey Karol,
I have read your every part of designing flutter app development, and it is so helpful. Thank you for sharing this great stuff with us!