A Guide To Flutter Localization
A Guide To Flutter Localization
A Guide To Flutter Localization
We untangle Flutter localization and internationalization so you can get back to the fun of Flutter
app dev.
1. Home
2. Blog
3. Developers
4. A Guide to Flutter Localization
In this article, we’ll show you how to use Flutter’s native localization package to
localize your mobile apps for Android and iOS. We won’t cover web or desktop
here, although it seems they should work in largely the same way.
🗒 Note » If you want us to write about Flutter Web and/or Desktop i18n, let us
know in the comments below.
Table of Contents
Demo App
o Versions Used
Installation & Setup
o Localization Configuration
o Adding Translation Files
o Configuring Our App
o Automatic Code Generation
o Using Our AppLocalizations
Locale Resolution
Updating the iOS Project
Getting the Active Locale
Basic Translation Messages
Interpolation in Messages
Plurals
Number Formatting
Date Formatting
Directionality: Left -to-Right and Right-to-Left
Up, Up and Away
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 2.12.3
Flutter 2.0.5
flutter_localizations (version seems tied to Flutter) — provides localizations to
common widgets, like Material or Cupertino widgets.
intl 0.17.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 pretty straightforward.
lib/main.dart
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'),
);
}
}
lib/screens/hero_list.dart
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 Hereos'),
),
Expanded(
child: ListView(
children: <Widget>[
HeroCard(
name: 'Grace Hopper',
born: '9 December 1906',
bio: 'Devised theory of machine-independent '
'programming languages.',
imagePath: 'assets/images/grace_hopper.jpg',
),
HeroCard(
name: 'Alan Turing',
born: '23 June 1912',
bio: 'Father of theoretical computer science & '
'artificial intelligence.',
imagePath: 'assets/images/alan_turing.jpg',
),
// ...
],
),
),
],
),
),
);
}
}
lib/widgets/hero_card.dart
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),
),
],
),
),
],
),
),
);
}
}
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 has the fully localized app.
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.17.0
# ...
# The following section is specific to Flutter.
flutter:
generate: true
uses-material-design: true
# ...
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 really
saves 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.
l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
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.
lib/l10n/app_en.arb
{
"appTitle": "Heroes of Computer Science"
}
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.
lib/l10n/app_ar.arb
{
"appTitle": ""أبطال علوم الكمبيوتر
}
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
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
// 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'),
);
}
}
.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.
Let’s make use of the newly generated code files to localize our app title.
lib/main.dart
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) {
return HeroList(title: AppLocalizations.of(context).appTitle);
},
'/settings': (context) => Settings(),
},
);
}
}
✋🏽 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 HeroCards, and we’ll do that when we tackle directionality a bit
later.
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:
By default, Flutter will read the user’s preferred system locales and:
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.
ios/Runner/Info.plist
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ar</string>
</array>
lib/screens/hero_list.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: ...
);
}
Let’s get that tooltip localized, shall we? First, we’ll add the relevant entries to our
ARB files.
lib/l10n/app_en.arb
{
"appTitle": "Heroes of Computer Science",
"openSettings": "Open Settings"
}
lib/l10n/app_ar.arb
{
"appTitle": ""أبطال علوم الكمبيوتر,
"openSettings": ""إفتح اإلعدادات
}
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.
lib/screens/hero_list.dart
// ...
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: ...
);
}
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.
lib/screens/hero_list.dart
// ...
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
HeroCard(
name: 'Steve Wozniak',
born: '11 August 1950',
bio: 'Designed & developed the Apple I & '
'Apple II microcomputers.',
imagePath: 'assets/images/steve_wozniak.jpg',
),
// ...
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.
lib/l10n/app_en.arb
{
// ...
"wozniakBio": "Developed the {appleOne} & {appleTwo} microcomputers.",
"@wozniakBio": {
"placeholders": {
"appleOne": {},
"appleTwo": {}
}
},
// ...
}
lib/l10n/app_ar.arb
{
// ...
"wozniakBio": "{ طور جهازي كمبيوترappleOne} { وappleTwo}",
// ...
}
🗒 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": {}
}
}
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": {}
}
}
.dart_tool/flutter_gen/gen_l10n/app_localizations.dart
// ...
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);
// ...
}
lib/screens/hero_list.dart
// ...
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
var t = AppLocalizations.of(context);
return Scaffold(
// ...
HeroCard(
name: 'Steve Wozniak',
born: '11 August 1950',
bio: t.wozniakBio('Apple I', 'Apple II'),
imagePath: 'assets/images/steve_wozniak.jpg',
),
// ...
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.
lib/screens/hero_list.dart
// ...
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
//...
body: Padding(
// ...
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
// Let's localize this dude
child: Text('6 Hereos'),
),
// ...
First, let’s add the message to our template English ARB file.
lib/l10n/app_en.arb
{
// ...
"heroCount": "{count,plural, =0{No heroes yet} =1{1 hero} other{{count} heroes}}",
"@heroCount": {
"placeholders": {
"count": {}
}
},
// ...
}
🗒 Note » You can add placeholders other than count to a plural message; they’re
specified as usual (see Interpolation above).
🗒 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.
lib/l10n/app_ar.arb
{
// ...
"heroCount": "{count,plural, =0{= }ال توجد أبطال بعد1{{ بطلcount}} =2{ }بطالنfew{{count} }أبطال
many{{count} }بطلother{{count} "}بطل,
// ...
}
Now let’s wire it all up and use our new message in our HeroList widget.
lib/screens/hero_list.dart
// ...
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)),
),
// ...
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.
// 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
✋🏽 Heads up » You can’t override number formats 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 curent locale is Egptian Arabic
var simpleCurrency =
NumberFormat.simpleCurrency(locale: currentLocale).format(14.24);
// => "$14.24" when current locale is US English
// => "£E ١٤،٢٤" when current locale is Egyptian Arabic
✋🏽 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.
We want this date localized to Arabic
lib/widgets/hero_card.dart
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',
// ...
To format the born date for each locale our app supports, we first add some new
localized messages with interpolated date values.
lib/l10n/app_en.arb
{
// ...
"heroBorn": "Born {date}",
"@heroBorn": {
"placeholders": {
"date": {
"type": "DateTime",
"format": "yMMMd"
}
}
},
// ...
}
lib/l10n/app_ar.arb
{
// ...
"heroBorn": "{ تاريخ الميالدdate}",
// ...
}
🗒 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.
lib/widgets/hero_card.dart
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 = new 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),
// ...
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 = new DateTime(1912, 6, 23);
var formattedBorn =
new DateFormat('yyyy-MM-dd', t.localeName).format(bornDateTime);
var message = t.heroBorn(formattedBorn);
// => "1912-06-23" in US English
// => "٢٣-٠٦-١٩١٢" in Egyptian Arabic
The image and the text in each card are flush because we’re
using EdgeInsets.only(right) to define the padding around our image.
lib/widgets/hero_card.dart
//...
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 EdgeInsets.only(right: 8.0),
child: ClipRRect(
// Portrait image ...
),
),
),
Expanded(
// Text widgets...
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.
lib/widgets/hero_card.dart
//...
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...
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:
With this small tweak to the code, our layout issue is resolved.
The layout now adapts to the active locale’s direction
🔗 Resource » At the time of writing, the official Flutter documentation lists the
following directional widgets:
EdgeInsetsDirectional
AlignmentDirectional
BorderDirectional
BorderRadiusDirectional
PositionedDirectional
AnimatedPositionedDirectional
And with all that in place, our final app looks all globalized-like.
🔗 Resource » Get the complete for our demo app from our GitHub repo.