Software localization
The Ultimate Guide to Flutter Localization
Flutter, Google’s multi-platform app framework, has not only garnered popularity in the mobile app development arena but has seamlessly branched out to web, Linux, macOS, and Windows. Beyond that, Flutter is blazing fast and a joy to work with.
When it comes to Flutter app internationalization (i18n), the Flutter team has crafted a solid built-in solution. In this tutorial, we’ll set up and configure Flutter’s i18n libraries, use them to load and display translations and work through date/time formatting, among other localization goodies.
🤿 Go deeper » Flutter’s native localization package is built on the first-party Dart intl package.
Phrase Strings
Take your web or mobile app global without any hassle
Adapt your software, website, or video game for global audiences with the leanest and most realiable software localization platform.
Our demo app
To keep things grounded and fun, we’ll build a small demo app and localize it: Heroes of Computer Science presents a selection of notable figures in the relatively short history of computing.
Versions used
We’re using the following language, framework, and package versions in this article:
- Dart 3.1.1
- Flutter 3.13.3
- DevTools 2.25.0
- flutter_localizations (version seems tied to Flutter) — provides localizations to common widgets, like Material or Cupertino widgets.
- intl 0.18.0 — the backbone of the localization system; allows us to create and use our own localizations; used for formatting dates and numbers.
Now let’s look at the code for our starter app, which is quite straightforward.
import 'package:flutter/material.dart';
import 'screens/hero_list.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Heroes of Computer Science',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HeroList(title: 'Heroes of Computer Science'),
);
}
}
Code language: Dart (dart)
Our root widget is a bread-and-butter MaterialApp
, with a HeroList
at its home
route.
import 'package:flutter/material.dart';
import 'package:flutter_i18n_2021/screens/settings.dart';
import 'package:flutter_i18n_2021/widgets/hero_card.dart';
class HeroList extends StatelessWidget {
final String title;
HeroList({this.title = ''});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.settings),
tooltip: 'Open settings',
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
},
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text('6 Heroes'),
),
Expanded(
child: ListView(
children: <Widget>[
HeroCard(
name: 'Grace Hopper',
born: '9 December 1906',
bio: 'Devised theory of machine...',
),
HeroCard(
name: 'Alan Turing',
born: '23 June 1912',
bio: 'Father of theoretical computer...',
),
// ...
],
),
),
],
),
),
);
}
}
Code language: Dart (dart)
HeroList
mainly houses a ListView
of parameterized HeroCard
s.
import 'package:flutter/material.dart';
class HeroCard extends StatelessWidget {
final String name;
final String born;
final String bio;
final String imagePath;
final String placeholderImagePath = 'assets/images/placeholder.jpg';
const HeroCard({
Key key,
this.name = '',
this.born = '',
this.bio = '',
this.imagePath,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Card(
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: Image.asset(
imagePath ?? placeholderImagePath,
width: 100,
height: 100,
),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
name,
style: theme.textTheme.headline6,
),
),
Padding(
padding: const EdgeInsets.only(top: 2, bottom: 4),
child: Text(
born.isEmpty ? '' : 'Born $born',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w300,
),
),
),
Text(
bio,
style: TextStyle(fontSize: 14),
),
],
),
),
],
),
),
);
}
}
Code language: JavaScript (javascript)
HeroCard
displays the given image and string params in a nice Material Card
widget and pretties everything up. Alright, let’s get to localizing this puppy!
🔗 Resource » You can get the code of the app up to this point from the start branch of our GitHub repo. The main branch also contains the fully localized app.
Installation and setup
We can install our packages by adding a few lines to pubspec.yaml
.
version: 1.0.0+1
environment:
sdk: '>=3.1.1 <4.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
# Add the flutter_localizations package
flutter_localizations:
sdk: flutter
# Add the intl package
intl: ^0.18.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
generate: true # Add this line
uses-material-design: true
Code language: YAML (yaml)
After adding the highlighted lines above we can run flutter pub get
from the command line to pull in our packages. The generate: true
line is necessary for the automatic code generation the localization packages provide for us. We’ll go more into code generation business shortly. For now, do include the line. It can save you quite some time.
Localization configuration
With our packages installed, let’s add a l10n.yaml
file to the root of our project. This file configures where our translation files will sit and the names of auto-generated dart files.
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
Code language: YAML (yaml)
🔗 Resource » The official Internationalization User Guide covers many more options that can go in l10n.yaml
to control the Flutter i18n code generator.
Adding translation files
Flutter localization uses ARB (Application Resource Bundle) files to house its translations by default. These are simple files written in JSON syntax. At the very least, we need a template file that corresponds to our default locale (English in our case). We specified that our template file will be lib/l10n/app_en.arb
in our above configuration. So let’s create this housing directory and add our template translations file to it.
{
"appTitle": "Heroes of Computer Science"
}
Code language: JSON / JSON with Comments (json)
Of course, all these shenanigans wouldn’t make much sense if we couldn’t provide translations for other locales. We’ll add an Arabic translations file here. Feel free to add any language you like. We’ll touch on right-to-left (RTL) layouts a bit later, so if you’re interested in that you might want to stick to Arabic or another RTL language.
{
"appTitle": "أبطال علوم الكمبيوتر"
}
Code language: JSON / JSON with Comments (json)
We can add as many locale translations as we want. We just need to make sure that our files conform to our configured naming convention: lib/l10n/app_<locale>.arb
Configuring our app
Let’s start telling our app about our anxious interest in i18n. We need to configure our main.dart
file to use the Flutter localization packages.
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'screens/hero_list.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Heroes of Computer Science',
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
// 'en' is the language code. We could optionally provide a
// country code as the second param, e.g.
// Locale('en', 'US'). If we do that, we may want to
// provide an additional app_en_US.arb file for
// region-specific translations.
const Locale('en', ''),
const Locale('ar', ''),
],
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HeroList(title: 'Heroes of Computer Science'),
);
}
}
Code language: JavaScript (javascript)
After importing flutter_localizations.dart
, we add the localizationsDelegates
and supportedLocales
props to the MaterialApp
constructor. localizationsDelegates
provide localizations to our app. The ones included above provide localizations for Flutter widgets, Material, and Cupertino, which have already been localized by the Flutter team.
For example, suppose we had a MaterialApp
and called the showDatePicker() function somewhere within it. Supposing also that our operating system language is set to Arabic, we would see something like the following.
Note that we didn’t have to translate anything ourselves to get this. The date picker widget has already been localized by the Flutter team. We just need to wire up the correct delegates in our app constructor, as we did above. Big hats off to the Flutter team for this: what a time-saver!
🗒️ Note » At the time of writing, flutter_localizations
supports 78 languages.
🔗 Resource » The official Flutter documentation does a good job of explaining how the different parts, like delegates and the Localizations
class, work together for their i18n/l10n.
The supportedLocales
prop we provided to the MaterialApp
constructor lists the languages our app supports. Flutter will only rebuild widgets in response to a locale change if the locale is in the supportedLocales
list. We’ll get back to supportedLocales
in a moment when we discuss locale resolution. Right now, let’s generate some code!
Automatic code generation
In order to use the translations in the ARB files in our Flutter app, we need to generate some Dart files that we import whenever we need the translations. To generate these files, just make sure you’ve followed the installation and setup steps up to this point and run the app. That’s right, just run the app. The code will automatically get generated, and if all went well, you should see the following files in your project directory:
.dart_tool/flutter_gen/gen_l10n/app_localizations.dart
.dart_tool/flutter_gen/gen_l10n/app_localizations_en.dart
.dart_tool/flutter_gen/gen_l10n/app_localizations_ar.dart
🗒️ Note » If these files weren’t generated, make sure your Flutter app has no compilation errors and check your debug console when you run the app.
Using our AppLocalizations
Let’s make use of the newly generated code files to localize our app title.
import 'package:flutter/material.dart';
import 'package:flutter_i18n_2021/screens/settings.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'screens/hero_list.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateTitle: (context) {
return AppLocalizations.of(context).appTitle;
},
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''),
const Locale('ar', ''),
],
theme: ThemeData(
primarySwatch: Colors.blue,
),
// remove home: HeroList(...)
initialRoute: '/',
routes: {
'/': (context) => HeroList(title: AppLocalizations.of(context).appTitle),
'/settings': (context) => Settings(),
},
);
}
}
Code language: Dart (dart)
We import app_localizations.dart
and add the auto-generated AppLocalizations.delegate
to our delegate list. This provides us with the AppLocalizations
widget, which we use to translate the app title and the HeroList
title. The auto-generated appTitle
property will contain the translation that matches the active locale, pulled from our app_<locale>.arb
file.
✋ Heads up » Due to loading order, our translations won’t be ready when we’re constructing our MaterialApp
. So we use the onGenerateTitle
and routes
props, and their builder (context) {}
functions to make sure that our translations are ready when we set our title strings.
Now, if we set our operating system language to Arabic and run our app, lo and behold!
Our title is now in Arabic. Moreover, notice how Flutter has laid out many of its widgets in a right-to-left direction automatically for us. Since Arabic is a right-to-left language, this saves us a ton of time! We’ll have to fix that padding to the left of the image in the HeroCard
s, and we’ll do that when we tackle directionality a bit later.
🤿 Go deeper » The eagle-eyed reader may have noticed that AppLocalizations.of(context)
looks a lot like calling an InheritedWidget. That’s because localization objects work a lot like InheritedWidget
s.
That’s it for setup. We have the foundation for localizing our app now. One question that you might have at this point is, “how does Flutter decide what locale to use?” Let’s talk about that.
Locale resolution
The locales we provided to MaterialApp(supportedLocales: [...])
are the only ones Flutter will use to determine the active locale when the app runs. To do this, Flutter uses three properties of a locale:
- The language code, e.g.
'en'
for English - The country code (optional), e.g. the
US
part inen_US
- The script code (optional) —the letter set used, e.g. traditional (
Hant
) or simplified Chinese (Hans
)
By default, Flutter will read the user’s preferred system locales and:
- Try to match the
languageCode
,scriptCode
, andcountryCode
with one insupportedLocales
. If that fails, - Try to match the
languageCode
andscriptCode
with one insupportedLocales
. If that fails, - Try to match the
languageCode
andcountryCode
with one insupportedLocales
. If that fails, - Try to match the
languageCode
with one insupportedLocales
. If that fails, - Try to match the
countryCode
with one insupportedLocales
only when all preferred locales fail to match. If that fails, - Return the first element of
supportedLocales
as a fallback.
So in our app, if the user’s iOS language is set to ar_SA
, they would see our ar
localizations (4. above) . If the user’s iOS language is set to fr
(French), they would see our en
localizations (6. above). On Android, a user can have a list of preferred locales, not just one. This is covered by Flutter in the above resolution algorithm.
🔗 Resource » The above algorithm is a paraphrasing of the official documentation of the supportedLocales property.
✋ Heads up » If your app supports a locale with a country code, like fr_CA
(Canadian French), you should provide a fallback without the country code, like fr
.
Updating the iOS project
The official Flutter documentation mentions the need to update the Info.plist
directly in the iOS app bundle, adding our supported locales to it. If Info.plist
isn’t updated our iOS app might not work as expected. To make the update we just need to open ios/Runner/Info.plist
in any text editor and make sure the following entries are in there.
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ar</string>
</array>
Code language: plaintext (plaintext)
Getting the active locale
We sometimes need to know what the runtime locale is in our code. We can do this with the following snippet.
Locale activeLocale = Localizations.localeOf(context);
// Assuming our active locale is fr_CA...
debugPrint(activeLocale.languageCode);
// => fr
debugPrint(activeLocale.countryCode);
// => CA
Code language: Dart (dart)
✋ Heads up » Notice that we’re using Localizations
, a widget built into Flutter, and not the auto-generated AppLocalizations
.
Basic translation messages
We already covered basic translation messages when we added our appTitle
message. However, let’s quickly go over the workflow for adding messages. We’ll translate the tooltip of our HeroList
‘s app bar icon button next. Might as well make some use of it since we’re not really building a settings screen 😅.
// ...
class HeroList extends StatelessWidget {
final String title;
HeroList({this.title = ''});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.settings),
tooltip: 'Open settings',
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
},
)
],
),
body: ...
);
}
Code language: Dart (dart)
Let’s get that tooltip localized, shall we? First, we’ll add the relevant entries to our ARB files.
// English
{
"appTitle": "Heroes of Computer Science",
"openSettings": "Open Settings"
}
// Arabic
{
"appTitle": "أبطال علوم الكمبيوتر",
"openSettings": "إفتح الإعدادات"
}
Code language: JSON / JSON with Comments (json)
Next, let’s reload our app to regenerate our code files. This step is really important and forgetting it can lead to undue frustration. Note that this is a full app restart (), not a hot reload.
Now we can update our code to use our new localized message.
// ...
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class HeroList extends StatelessWidget {
final String title;
HeroList({this.title = ''});
@override
Widget build(BuildContext context) {
var t = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.settings),
tooltip: t.openSettings,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
},
)
],
),
body: ...
);
}
}
Code language: Dart (dart)
With this code in place, when we reload our app we should see the localized value of our tooltip in the Widget Inspector.
✋ Heads up » You may often get error highlighting in your IDE after you add new translation messages. If you’ve reloaded your app, the error may be incorrect (you might be just fine). If you get a message saying that there are build errors, you can try to run the app anyway. As long as the app builds and runs, and you see your new translations, then all is probably well. To make the error go away in the IDE, trying shutting your app down entirely and starting it up again.
Interpolation in messages
We’re on our way to translating our app. But what about interpolating dynamic runtime values in our translation messages? For example, Steve Wozniak’s bio contains the product names Apple I and Apple II.
// ...
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
body: HeroCard(
name: 'Steve Wozniak',
born: '11 August 1950',
bio: 'Designed & developed the Apple I & '
'Apple II microcomputers.',
imagePath: 'assets/images/steve_wozniak.jpg',
),
// ...
);
}
}
Code language: Dart (dart)
When we localize this message, we might want to keep Apple I and Apple II in their original English regardless of the active locale. We can use placeholders in our translation files to accomplish this.
// English
{
// ...
"wozniakBio": "Developed the {appleOne} & {appleTwo} microcomputers.",
"@wozniakBio": {
"placeholders": {
"appleOne": {},
"appleTwo": {}
}
},
// ...
}
// Arabic
{
// ...
"wozniakBio": "طور جهازي كمبيوتر {appleOne} و {appleTwo}",
// ...
}
Code language: JSON / JSON with Comments (json)
We use the {placeholderName}
syntax to set the placeholders for our dynamic values, and we can have as many placeholders as we want in a message.
You’ve probably also noticed the @wozniakBio
key in our English translations above. This entry is a companion to the wozniakBio
message in the same file. Companion entries are optional for basic messages but required for messages with placeholders. In fact, we use companion entries to define message placeholders (among other things).
🗒️ Note » The companion entry for a message with key foo
must have a key of @foo
. We only need companion entries in our default/template translation file (English in our case).
"@wozniakBio": { "placeholders": { "appleOne": {}, "appleTwo": {} } }
Code language: JavaScript (javascript)
✋ Heads up » Placeholder names must be valid Dart method parameter names.
We could use the placeholders
object to specify the type of each value, and even provide examples as documentation if we want. We can also leave the definition as an empty {}
.
"@wozniakBio": {
"placeholders": {
"appleOne": {
// Explicit type
"type": "String",
// A little doc
"example": "Apple I"
},
// It's perfectly ok to just specifiy the name
"appleTwo": {}
}
}
Code language: PHP (php)
The type
is used in the method Flutter will generate on AppLocalizations
for our wozniakBio
message.
// ...
abstract class AppLocalizations {
// ...
// This method is implemented in app_localizations_en.dart
// and app_localizations_ar.dart.
// Explicit type for appleOne parameter. Implicit appleTwo
// parameter.
String wozniakBio(String appleOne, Object appleTwo);
// ...
}
Code language: JavaScript (javascript)
🗒️ Note » It’s perfectly OK for most cases to use empty {}
for placeholder definitions. An empty definition will cause the parameter to be of type Object
. Under the hood Flutter will just use the theParameter.toString()
value of the given parameter, so an Object
param will work just fine. An implicit Object
placeholder also keeps our messages flexible to take any type, since all Dart types derive from Object and have a toString()
.
OK, after rerunning the app to update AppLocalizations
, we can localize the Woz’s bio message with the new method.
// ...
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
var t = AppLocalizations.of(context);
return Scaffold(
// ...
body: HeroCard(
name: 'Steve Wozniak',
born: '11 August 1950',
bio: t.wozniakBio('Apple I', 'Apple II'),
imagePath: 'assets/images/steve_wozniak.jpg',
),
// ...
);
}
}
Code language: Dart (dart)
With that in place, we know we can never be sued by any fruit-flavored companies for misrepresenting their products in any language.
Plurals
We often need to handle dynamic plurals in our localization. “You have received one message” or “You have received 3 messages”, for example.
It’s important to note that different languages handle plurals differently. For example, English has two plural forms: one and other (other == zero and >1). Arabic has six plural forms. This can be a bit of a headache when localizing using libraries that don’t support complex plural rules. Luckily, Flutter’s first-party i18n solution handles complex plurals out of the box, so it has us covered. Let’s use it to localize the hero counter in our app.
// ...
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
body: Padding(
// ...
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
// Localized string should replace '6 Heroes'
child: Text('6 Heroes'),
),
// ...
],
),
),
);
}
}
// ...
Code language: Dart (dart)
First, let’s add the message to our template English ARB file.
{
// ...
"heroCount": "{count,plural, =0{No heroes yet} =1{1 hero} other{{count} heroes}}",
"@heroCount": {
"placeholders": {
"count": {}
}
},
// ...
}
Code language: JSON / JSON with Comments (json)
We specify a count
placeholder in our message, and use it with the special {count,plural,...}
syntax to define the different plural forms.
✋ Heads up » The count
param will always be an int
type. If you specify another type for count
, Flutter will ignore it and use int
anyway.
🗒️ Note » You can add placeholders other than count
to a plural message; they’re specified as usual (see Interpolation above).
Flutter supports the following plural forms.
- zero ➞
=0{No heroes}
- one ➞
=1{One hero}
- two ➞
=2(Two heroes}
- few ➞
few{The {count} heroes}
- many ➞
many{{count} heroes}
- other ➞
other{{count} heroes}
few
, many
, and other
have different meanings depending on the active language. The only required form in any language is the other
form.
🗒️ Note » We didn’t need to use the zero =0
form in our English message above. If we had omitted it, Flutter would have used our other
form instead.
Alright, let’s add our Arabic message. As we mentioned earlier, Arabic has six plural forms.
{
// ...
"heroCount": "{count,plural, =0{لا توجد أبطال بعد} =1{بطل واحد} =2{بطلان} few{{count} أبطال} many{{count} بطل} other{{count} بطل}}",
// ...
}
Code language: JSON / JSON with Comments (json)
Now let’s wire it all up and use our new message in our HeroList
widget.
// ...
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
var t = AppLocalizations.of(context);
return Scaffold(
// ...
body: Padding(
// ...
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(t.heroCount(6)),
),
// ...
],
),
),
);
}
}
// ...
Code language: Dart (dart)
Of course, in a production app the count parameter passed to t.heroCount()
would be dynamic. Flutter chooses the correct plural form from our message depending on the active locale.
Number formatting
We can format numbers in our localized messages using our friend the placeholders
object in the companion entries of our template ARB file (English in our case). There’s no great place to put number formatting in our little demo app, so we’ll just pretend we have an e-commerce app to demonstrate.
// app_en.arb in example app that has a shopping cart
{
"itemTotal": "Your total is: {value}",
"@itemTotal": {
"placeholders": {
"value": {
"type": "double",
"format": "currency"
}
}
}
}
Code language: JSON / JSON with Comments (json)
Notice that we specified an explicit type
and format
to control how the number will be displayed. As per usual, we can translate our message in our other locale files.
// app_ar.arb in example app that has a shopping cart
{ "itemTotal": "إجمالي: {value}" }
Code language: JSON / JSON with Comments (json)
After we reload our app, we can use our message as usual.
// In a Widget
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// In some widget builder with a context
var t = AppLocalizations.of(context);
var message = t.itemTotal(56.12);
// => "Your total is USD56.12" when current locale is English
// => "إجمالي: EGP56.12" when current locale is Arabic
Code language: Dart (dart)
✋ Heads up » You can’t override number format
s per locale. The format you specify in your template locale (English in our case) will be used across locales regardless of any format
override you specify in your other locale files.
Remember that underneath the hood Flutter is using the Dart intl library for most of its i18n work. The currency format we used above is one of several formats built into the intl number formatter. Other formats include decimals, percentages, and more.
🔗 Resource » Check out the official user guide for all the available formats.`
However, we don’t have to rely on Flutter to pass our numbers to intl. We can use intl directly to gain more control over our number formatting.
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart';
// In some Widget builder with a context
var currentLocale = AppLocalizations.of(context).localeName;
var compact = NumberFormat.compact(locale: currentLocale).format(6000000);
// => "6M" when current locale is US English
// => "٦ مليون" when current locale is Egyptian Arabic
var simpleCurrency = NumberFormat.simpleCurrency(locale: currentLocale).format(14.24);
// => "$14.24" when current locale is US English
// => "ج.م. ١٤٫٢٤" when current locale is Egyptian Arabic
Code language: Dart (dart)
🔗 Resource » You don’t need to use the predefined formats like compact
and simpleCurrency
. The intl NumberFormat
constructor gives you granular control over your number formats. Read all about it on the official documentation.
✋ Heads up » The only way I could get Eastern Arabic numerals (١،٢،٣…) rendering for Arabic is by setting the locale
param to "ar_EG"
(Egyptian Arabic). Neither "ar"
or any "ar_XX"
variant other than Egyptian worked for me.
✋ Heads up » Formats didn’t work with the plural count
variable for me. It seems that Flutter is overriding the format when it processes plurals. If you are able get formats in your plurals, please let us know how you did it in the comments below.
Date Formatting
Our heroes currently have hard-coded birth dates that aren’t localized, which isn’t too cool.
Recall that we’re rendering each of our heroes with a HeroCard
widget.
import 'package:flutter/material.dart';
class HeroCard extends StatelessWidget {
final String name;
final String born;
final String bio;
final String imagePath;
// ...
const HeroCard({
Key key,
this.name = '',
this.born = '',
this.bio = '',
this.imagePath,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// ...
return Card(
child: Padding(
// ...
Padding(
padding: const EdgeInsets.only(top: 2, bottom: 4),
child: Text(
born.isEmpty ? '' : 'Born $born',
// ...
),
),
// ...
),
);
}
}
Code language: Dart (dart)
To format the born
date for each locale our app supports, we first add some new localized messages with interpolated date values.
// English
{
// ...
"heroBorn": "Born {date}",
"@heroBorn": {
"placeholders": {
"date": {
"type": "DateTime",
"format": "yMMMd"
}
}
},
// ...
}
// Arabic
{
// ...
"heroBorn": "تاريخ الميلاد {date}",
// ...
}
Code language: JSON / JSON with Comments (json)
When we define our date
placeholder in our template localization file, we need to give it the DateTime
type. We can then use a format
to specify how we want to display the date. The format yMMMd
we defined above stands for “year, abbreviated month, day”, which in US English would render to something like “Dec 9, 1906”.
🔗 Resource » In fact, underneath the hood Flutter is just using intl’s DateFormat
class, generating code like DateFormat.yMMMd(localeName).format(date)
. The yMMMd
named constructor is a handy shortcut called a “skeleton”, and there are quite a few we can use. Check them out in the official DateFormat documentation.
🗒️ Note » We didn’t have to call our placeholder variable date
. We could have given it any name, as long as it was a valid Dart function parameter name.
Alright, let’s wire this up in our widget to get our new messages displayed.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class HeroCard extends StatelessWidget {
final String name;
final String born;
final String bio;
final String imagePath;
// ...
final DateTime bornDateTime;
HeroCard({
Key key,
this.name = '',
this.born = '',
this.bio = '',
this.imagePath,
}) : bornDateTime = DateFormat('d MMMM yyyy').parse(born),
super(key: key);
@override
Widget build(BuildContext context) {
// ...
var t = AppLocalizations.of(context);
return Card(
child: Padding(
// ...
Padding(
padding: const EdgeInsets.only(top: 2, bottom: 4),
child: Text(
born.isEmpty ? '' : t.heroBorn(bornDateTime),
// ...
),
),
// ...
),
);
}
}
Code language: Dart (dart)
We’re handed a hero’s birthdate as a String
, so we need to parse it to a DateTime
first. We use intl’s DateFormat
class to do this in our widget constructor.
In the build
method, we simply pass the parsed DateTime
to our t.heroBorn()
localized message. This gives us nicely localized dates.
What if we don’t want to use any of the predefined skeletons, and to fully customize our date formats? Well, much like number formatting (see above), we would need to use the intl.DateFormat
class directly. Suppose we wanted to display our hero birthdates in a format like “1912-06-23”. We would do something like the following.
import 'package:intl/intl.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// In widget builder with context
var t = AppLocalizations.of(context);
var bornDateTime = DateTime(1912, 6, 23);
var formattedBorn = DateFormat('yyyy-MM-dd', t.localeName).format(bornDateTime);
var message = t.heroBorn(formattedBorn);
// => "1912-06-23" in US English
// => "١٩١٢-٠٦-٢٣" in Egyptian Arabic
Code language: JavaScript (javascript)
Our localized heroBorn
messages in our ARB files would then just take regular Object
or String
params, since we’ve already done the formatting for them.
Directionality: left-to-right and right-to-left
While English is a left-to-right (LTR) language, Arabic goes the other way and is laid out right-to-left (RTL). This is currently causing a problem for us when our app is used on a device with Arabic as the system language.
The image and the text in each card are flush because we’re using EdgeInsets.only(right)
to define the padding around our image.
import 'package:intl/intl.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// In widget builder with context
var t = AppLocalizations.of(context);
var bornDateTime = DateTime(1912, 6, 23);
var formattedBorn = DateFormat('yyyy-MM-dd', t.localeName).format(bornDateTime);
var message = t.heroBorn(formattedBorn);
// => "1912-06-23" in US English
// => "١٩١٢-٠٦-٢٣" in Egyptian Arabic
Code language: Dart (dart)
This works in LTR languages, where we want a right margin between the image and the text. In RTL languages, however, we want the margin on the left.
An easy remedy here is to use EdgeInsetsDirectional
instead of EdgeInsets
.
//...
class HeroCard extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
// ...
return Card(
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsetsDirectional.only(end: 8.0),
child: ClipRRect(
// Portrait image...
),
),
Expanded(
// Text widgets...
),
],
),
),
);
}
}
Code language: PHP (php)
Notice that we use end
instead of right
to set the padding between the image and the text. EdgeInsetsDirectional
is one of a few layout Flutter widgets that are locale-direction-aware. These widgets take start
and end
parameters instead of left
and right
. And the cool thing is that these directional widgets will do the correct thing automatically for the active locale:
start
==left
for LTR languagesstart
==right
for RTL languagesend
==right
for LTR languagesend
==left
for RTL languages
With this small tweak to the code, our layout issue is resolved.
🔗 Resource » At the time of writing, the official Flutter documentation lists the following directional widgets:
- EdgeInsetsDirectional
- AlignmentDirectional
- BorderDirectional
- BorderRadiusDirectional
- PositionedDirectional
- AnimatedPositionedDirectional
With all that in place, our final app looks all globalized-like.
🔗 Resource » Get the complete code for our demo app from our GitHub repo.
Adding localized assets
Localizing images in Flutter involves using different sets of images for different locales or languages in your app. This is commonly done to display images with text or content that matches the user’s preferred language.
Let’s add a flag, which displays differently depending on the user’s region.
We’ll start by adding the images, one for each locale we plan to support. We should organize these images in a folder structure based on locales, for example:
└── assets/
└── images/
├── eg/
│ └── flag.jpg
├── us/
│ └── flag.jpg
└── flag.jpg
Code language: plaintext (plaintext)
For this tutorial we will add flag images for Egypt and the US. The file directly in the images folder (with no locale) is a fallback image for other regions.
Next, let’s register the files in pubspec.yml
file.
# pubspec.yml
# ...
flutter:
# ...
assets:
# ...
- assets/images/in/flag.jpg
- assets/images/us/flag.jpg
- assets/images/flag.jpg
# ...
Code language: YAML (yaml)
Now we can load the images in our hero_list.dart
file:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../widgets/hero_card.dart';
class HeroList extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ...
String getImagePath(String imageName) {
String basePath = 'assets/images/';
// When country code isn't supported, we
// display fallback image.
if (locale.countryCode?.isEmpty == true) {
return basePath + 'flag.jpg';
}
String localePath = '${locale.countryCode!.toLowerCase()}/';
return basePath + localePath + imageName;
}
return Scaffold(
// ...
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// ...
Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: Image.asset(
getImagePath('flag.jpg'),
width: 40,
height: 40,
),
),
],
),
),
);
}
}
Code language: Dart (dart)
Now in the main.dart
file make the following changes
...
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
...
supportedLocales: [
const Locale('ar', ''),
const Locale('en', ''),
// Add the supported region codes
const Locale('ar', 'EG'),
const Locale('en', 'US'),
],
...
}
}
Code language: Dart (dart)
Now the app shows the appropriate asset for the user’s locale and shows a fallback if their locale is not supported.
Changing language in app
Sometimes a user will want to have their OS in one language, and a specific app in another. To accommodate that, let’s add a language selector in our app. It should work for all platforms supported by Flutter.
In the main.dart
file, we’ll make the following changes:
void main() {
runApp(const MyApp());
}
// We need to make MyApp Stateful because
// it needs to react when the Locale changes.
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
static void setLocale(BuildContext context, Locale newLocale) {
_MyAppState? state = context.findAncestorStateOfType<_MyAppState>();
state?.setLocale(newLocale);
}
}
class _MyAppState extends State<MyApp> {
Locale? _locale;
setLocale(Locale locale) {
setState(() {
_locale = locale;
});
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
// ...
locale: _locale,
initialRoute: '/',
routes: {
'/': (context) {
return HeroList(title: AppLocalizations.of(context)!.appTitle);
},
},
);
}
}
Code language: Dart (dart)
Now, in the hero_list.dart
file, let’s make the dropdown menu and call the setLocale()
method we defined in the above.
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../widgets/hero_card.dart';
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
var t = AppLocalizations.of(context)!;
final Locale locale = Localizations.localeOf(context);
// Dropdown options
var items = [
'en',
'ar',
];
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: <Widget>[
DropdownButton(
// Down Arrow Icon
icon: const Icon(Icons.settings, color: Colors.white,),
items: items.map((String items) {
return DropdownMenuItem(
value: items,
child: Text(items),
);
}).toList(),
onChanged: (String? newValue) {
MyApp.setLocale(context, Locale(newValue));
},
),
],
),
// ...
);
}
}
Code language: Dart (dart)
With that, the user can select the app language independently from the system language.
🗒️ Note » Before selecting a locale, it will be set to the user’s system default locale.
🗒️ Note » Before the user manually selects a locale in-app, the system locale is used as a default. However, the user’s manually selected in-app locale won’t persist when the app is restarted. To persist the user’s in-app locale preference, you need to save and retrieve the locale using something like the Shared Preferences plugin.
The final project is available on GitHub.
Flutter localization made simpler
We hope you enjoyed our Flutter localization tutorial and picked up some handy tricks. Now, if you’re ready to take your localization game up a notch, Phrase Strings is your go-to solution. A localization wizard for your Flutter apps, Phrase Strings comes with ARB file support, and it’s super dev-friendly with its easy-to-use API and CLI. It also features a slick strings editor that makes life a breeze for translations.
What’s more, it syncs seamlessly with GitHub, GitLab, and Bitbucket, and even offers over-the-air translations for mobile apps to handle the localization heavy lifting, allowing you to stay focused on the code you love so much. Check out all Phrase features for developers and see for yourself how they can help you take your apps global more quickly.