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, too. All in all, Flutter internationalization (or 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.
Flutter internationalization and localization
Prerequisites
In this article, we are going to see how to introduce Flutter i18n into an application. Before diving into the internationalization part, however, you will need to set up your working environment by following the official guide at docs.flutter.dev.
This tutorial also assumes that you have basic knowledge of Flutter.
Once you have installed everything, just create a new Flutter application by running:
flutter create i18n_demo
We also need content to work with, so let’s do some more preparations.
Source code can be found on GitHub.
Preparing the Flutter app
We are going to create most of the files inside the lib
folder so unless stated otherwise you should work within that folder.
So, let’s open lib
and tweak the main.dart
file in the following way:
import 'package:flutter/material.dart'; import 'package:i18n_demo/app/my_app.dart'; void main() { runApp(const MyApp()); }
Next, inside, create an app
folder with a my_app.dart
file:
import 'package:flutter/material.dart'; import 'package:i18n_demo/app/pages/my_home_page.dart'; class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter i18n', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(), ); } }
Finally, in the app
folder, create a new pages
directory with a my_home_page.dart
file:
import 'package:flutter/material.dart'; class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: const Text("Welcome!"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text("Press the button below"), Text( "Times pressed: $_counter", style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }
Here we are displaying a button that can be pressed and the counter will increment accordingly.
Now we are ready to proceed to the main part!
Internationalization libraries
So, the first step is adding two dependencies to our app — namely, the flutter_localizations and the intl libraries — that will do some heavy lifting for us.
Therefore, run these two commands:
flutter pub add flutter_localizations --sdk=flutter flutter pub add intl:any
After everything is installed, open the pubspec.yaml
file in the project root, find the flutter
section, and tweak it by adding the generate
setting:
# The following section is specific to Flutter packages. flutter: generate: true
Finally, in the project root, create a new file called l10n.yaml
with the following contents:
arb-dir: lib/l10n template-arb-file: intl_en.arb output-localization-file: app_localizations.dart
Here we are saying where our translation files will be located, what the template (main) file is, and where the compiled translations should be placed. There are many other options that you can tweak — learn about them in the official docs.
That’s it! Now let’s take care of the translation files.
Creating Flutter translation files
ARB files
Translations for Flutter apps are usually stored within ARB files. These are relatively simple key-value files that will then be compiled into special Dart files consumed by the app.
We’ll start by creating an l10n
directory within the lib
folder. Inside, I’m going to create two files: intl_en.arb
and intl_ru.arb
to store English and Russian translations. Of course, you can pick almost any other locale as currently Flutter supports more than a hundred languages.
Let’s provide the English text first:
{ "@@locale": "en", "appTitle": "Flutter i18n", "@appTitle": { "description": "Main application title" }, "welcome": "Welcome!" }
A few things to note here:
@@locale
specifies the language within this fileappTitle
andwelcome
are the translation keys that we are going to use within our source code. Their corresponding values are the actual translations.@appTitle
is the metadata for theappTitle
key. In the example above, it contains the key description. While it’s not mandatory to provide metadata, you’ll usually want to do it to explain the purpose of this key and provide additional context for your translators.
Now let’s tweak the intl_ru.yaml
file:
{ "@@locale": "ru", "appTitle": "Flutter i18n", "welcome": "Добро пожаловать!" }
In this file, we don’t normally provide any metadata.
Compiling Dart files from ARB
While you or your translators will work with the ARB files, Flutter requires translations to be provided in a different format. To be precise, we should convert the ARBs into regular Dart files, so let’s run the following command:
flutter gen-l10n
Make sure to rerun this command every time you update your translations.
You’ll notice that a new .dart_tool\flutter_gen\gen_l10n
folder will be created in the project root. It contains three files: app_localizations.dart
, app_localizations_en.dart
, and app_localizations_ru.dart
. The first file stores information about all the supported locales, as well as exposing some useful methods. The two other files simply store your translations.
Please note that you are not expected to modify these files manually as they will be recompiled every time the flutter gen-l10n
command is executed.
Performing simple translations
All right, so now that we have two translation keys it’s time to use them in the source code.
Translating the app title
First, I’d like to display the app title. Open the app/my_app.dart
file and import the app_localizations.dart
:
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
Next, we need to provide information about the supported locales and configure delegates, so make the following changes within the same file:
class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter i18n', localizationsDelegates: AppLocalizations.localizationsDelegates, // <--- add this supportedLocales: AppLocalizations.supportedLocales, // <--- add this theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(), ); } }
Finally, replace the title
with the onGenerateTitle
:
class MyApp extends StatelessWidget { // ... Widget build(BuildContext context) { return MaterialApp( onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, // <--- // ... ); } }
In this code sample, you can see how the translation keys are utilized in the code. Specifically, you reference the AppLocalizations
and then call the property named after your translation key (appTitle
in our case).
Displaying the welcoming message
Now let’s see another example. Open the app/pages/my_home_page.dart
file and add the following import:
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
Then, simply tweak your widget by adjusting its title
:
class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { // ... } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(AppLocalizations.of(context)!.welcome), // <--- add this ), body: Center( // ... ), // ... ); } }
Note that in this case we don’t need to use the onGenerateTitle
(in fact, it’s only needed for the global title).
That’s it! You can now run your Flutter app on a mobile phone or emulator to make sure that everything is running smoothly. If you change the system locale in the phone settings, the app will be translated accordingly.
Interpolation and placeholders in Flutter translations
Of course, that’s not it. At this point, let’s see how to interpolate custom values within your translations.
Inserting custom text
Let’s suppose that we would like to display copyright information and embed the company name in it. Tweak the intl_en.arb
file within the lib/l10n
directory:
{ "@@locale": "en", // ... "createdBy": "Tutorial by {company}", "@createdBy": { "description": "Copyright message", "placeholders": { "company": { "type": "String", "example": "Demo" } } } }
{company}
is a placeholder for which we can provide the value within the source code. Inside the metadata, we can also explain what the expected type of this value is and show an example.
Let’s adjust the intl_ru.arb
file:
{ "@@locale": "ru", // ... "createdBy": "Руководство от {company}" }
Please note that the placeholder’s name must stay the same, whereas the translation text can be modified as needed.
Now, remember to run the flutter gen-l10n
command.
Next, open the app/pages/my_home_page.dart
file and provide a new text widget:
class _MyHomePageState extends State<MyHomePage> { // ... Widget build(BuildContext context) { return Scaffold( // ... body: Center( child: Column( // ... children: <Widget>[ Text(AppLocalizations.of(context)!.createdBy('Lokalise')), // <--- add this const Text("Press the button below"), // ... ], ), ), // ... ); } }
As you can see, the approach is similar but in this case we have to provide the company name as an argument.
In fact, the createdBy
method looks like this:
String createdBy(String company) { return 'Tutorial by $company'; }
So, it’s nothing too complex, really.
Displaying the currently set locale
We can use interpolation to display the currently set locale as well.
First, let’s create a new current_locale_widget.dart
in the app/widgets
folder:
import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class CurrentLocaleWidget extends StatelessWidget { const CurrentLocaleWidget({super.key}); @override Widget build(BuildContext context) { final locale = Localizations.localeOf(context); return Center( child: Text( AppLocalizations.of(context)!.currentLocale(locale.toString()), style: Theme.of(context).textTheme.headlineMedium, )); } }
The currently set locale can be easily fetched by running the localeOf
method. Then, we simply display it using the currentLocale
method, which will be created in a moment.
Let’s import and display this widget inside the my_home_page.dart
:
// ... import 'package:i18n_demo/app/widgets/current_locale_widget.dart'; // <--- add this // ... class _MyHomePageState extends State<MyHomePage> { // ... Widget build(BuildContext context) { return Scaffold( // ... body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const CurrentLocaleWidget(), // <--- add this Text(AppLocalizations.of(context)!.createdBy('Lokalise')), // ... ], ), ), // ... ); } }
Finally, add English translation in the intl_en.arb
file:
{ // ... "currentLocale": "Current locale is {locale}" }
And the Russian one in the intl_ru.arb
file:
{ // ... "currentLocale": "Текущая локаль: {locale}" }
Great job!
More Flutter i18n features
Date and time localization
Let’s move on to localizing date and time in Flutter.
First, add a new Text
widget right before or after the const CurrentLocaleWidget()
in the my_home_page.dart
file:
// ... children: <Widget>[ const LanguageWidget(), Text(AppLocalizations.of(context)!.currentDate(DateTime.now())), // <--- add this Text(AppLocalizations.of(context)!.pressButton), // ... other text widgets here... ], // ...
So, we are simply displaying the current date and time. However, as you know, there can be many different formats to represent this information. Luckily, the DateFormat
class provided by the intl
package has our back, and we simply need to choose the format that works for us.
For instance, let’s display the current year, month, weekday, and day. Tweak the intl_en.arb
in the following way:
{ // ... "currentDate": "Today is {date}", "@currentDate": { "description": "Localized current date", "placeholders": { "date": { "type": "DateTime", "format": "yMMMMEEEEd" } } } }
yMMMMEEEEd
is the predefined format available in the DateFormat
class. Note that it must be provided in the metadata.
Inside the intl_ru.arb
file, we can omit the metadata to use the same format:
{ // ... "currentDate": "Сегодня {date}" }
That’s it!
Localizing numbers and currencies
The next step is localizing number and currency information.
Let’s add yet another Text
widget inside the my_home_page.dart
file:
// ... children: <Widget>[ const LanguageWidget(), Text(AppLocalizations.of(context)!.currentDate(DateTime.now())), Text(AppLocalizations.of(context)!.currencyDemo(1234567)), // <--- add this // ... other text widgets here... ], // ...
Flutter has a few predefined number formats for us, so you can pick any that suit you or find more information in the NumberFormat
class docs.
Tweak the intl_en.arb
file, like so:
{ // ... "currencyDemo": "Here's a demo price: {value}", "@currencyDemo": { "description": "A demo showing how to localize currency", "placeholders": { "value": { "type": "int", "format": "currency", "optionalParameters": { "decimalDigits": 2 } } } } }
In this example we are employing the currency
format, which uses the following pattern: <CURRENCY_NAME><FORMATTED_NUMBER>
. For the English locale the default currency is USD, whereas for Russian it’s RUB. We also ask to add two decimal digits.
Now, provide the translation in the intl_ru.arb
file:
{ // ... "currencyDemo": "Демонстрационная цена: {value}" }
Great — our currency information is properly localized!
Pluralization
Another important area is pluralization, which means adjusting the text based on a count. As you likely remember, we have a button that can be pressed to increment the counter. Let’s try to adjust the displayed text according to the counter’s value.
First, tweak the Text
widgets in the my_home_page.dart
file once again:
children: <Widget>[ const CurrentLocaleWidget(), // ... Text(AppLocalizations.of(context)!.pressButton), // <-- change this Text( AppLocalizations.of(context)!.buttonPressed(_counter), // <-- change this style: Theme.of(context).textTheme.headlineMedium, ), ],
Now open the intl_en.arb
file and add the following:
{ "pressButton": "Press the button below", "buttonPressed": "{count, plural, =0{Has not pressed yet} =1{Pressed 1 time} other{Pressed {count} times}}", "@buttonPressed": { "description": "Shows how many times the button has been pressed (pluralized)", "placeholders": { "count": { "type": "num", "format": "compact" } } } }
As you can see, for the buttonPressed
key we are using a special plural
expression that will display one of the texts based on the value of count
. Specifically, there are three possible cases: when the count equals zero, when it equals one, and when it has some other value.
By the way, plural
is not the only expression supported by Flutter. For example, there’s a select
expression that can come in really handy when working with gender information, as shown here:
"pronoun": "{gender, select, male{he} female{she} other{they}}", "@pronoun": { "description": "A gendered message", "placeholders": { "gender": { "type": "String" } } }
Finally, let’s add the translations within the intl_ru.arb
file:
{ // ... "pressButton": "Нажмите кнопку ниже", "buttonPressed": "{count, plural, =0{Не было нажатий} =1{Нажата 1 раз} few{Нажата {count} раза} other{Нажата {count} раз}}" }
Different languages have different pluralization rules, so in this case we have to provide not three but four options (these are called plural forms).
And that’s it! We have translated our app and now you can rerun it to make sure that everything works.
Switching the locale programmatically
While the app language can be changed by switching the system locale, this is not always convenient. Therefore, let’s see a simple way to add a locale switcher to the app.
Adding a locale provider
First, we’ll need to add a new dependency, called provider, that will greatly help us. Run the following command:
flutter pub add provider
Next, let’s create a locale_provider.dart
file inside the app/providers
directory:
import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class LocaleProvider extends ChangeNotifier { Locale _locale = const Locale("en"); Locale get locale => _locale; void setLocale(Locale locale) { if (!AppLocalizations.supportedLocales.contains(locale)) return; _locale = locale; notifyListeners(); } }
There are two main things to note here:
- Our default locale is set to
en
. - Inside the
setLocale
method, we check that the requested locale is actually supported, then switch it and notify the listeners of this.
The final thing to do is use this provider in the my_app.dart
file. Add these imports:
import 'package:i18n_demo/app/providers/locale_provider.dart'; import 'package:provider/provider.dart';
We then need to wrap the main part of the code in the ChangeNotifierProvider
, so your file will now have the following content:
class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) => ChangeNotifierProvider( // <--- add this create: (context) => LocaleProvider(), // <--- add this builder: (context, child) { // <--- add this final provider = Provider.of<LocaleProvider>(context); // <--- add this return MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: provider.locale, // <--- add this onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(), ); }); }
Also note that we are providing locale
here, and read the currently set locale from the provider.
Creating a locale switcher
So, our provider is now complete; therefore, create a locale_switcher_widget.dart
file in the app/widgets
folder. Add the following imports inside it:
import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:i18n_demo/app/providers/locale_provider.dart'; import 'package:provider/provider.dart';
Then code the widget itself:
class LocaleSwitcherWidget extends StatelessWidget { const LocaleSwitcherWidget({super.key}); @override Widget build(BuildContext context) { final provider = Provider.of<LocaleProvider>(context); final locale = provider.locale; return DropdownButtonHideUnderline( child: DropdownButton( value: locale, icon: Container(width: 12), items: AppLocalizations.supportedLocales.map( (nextLocale) { return DropdownMenuItem( value: nextLocale, onTap: () { final provider = Provider.of<LocaleProvider>(context, listen: false); provider.setLocale(nextLocale); }, child: Center( child: Text(nextLocale.toString()), ), ); }, ).toList(), onChanged: (_) {}, ), ); } }
What we are doing here is reading the currently set locale and then drawing a dropdown. Inside, we are displaying all the supported locales and adding an onTap
event that calls the setLocale()
method in our provider. Basically, that’s it.
Of course, we also have to display the widget itself, so import it in the my_home_page.dart
file:
import 'package:i18n_demo/app/widgets/locale_switcher_widget.dart';
Then simply add actions
to the appBar
:
class _MyHomePageState extends State<MyHomePage> { // ... Widget build(BuildContext context) { return Scaffold( appBar: AppBar( // ... actions: const [ // <--- add this LocaleSwitcherWidget(), SizedBox(width: 12), ], ), body: Center( // ... ), // ... ); } }
Awesome! Now reload the app and try to switch locale using the dropdown in the top menu.
Use Lokalise AI to translate into multiple locales
As you can see, managing translation files can be quite tedious, especially for larger applications. Moreover, you might need to add support for languages that you can’t really speak. In this case, you’ll need either to hire a professional or use AI. 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
- Translation quality assurance tools
- 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 ever plan to reach.
Uploading ARB translation files
To get started, sign up for a free trial (no credit card information required). In your projects dashboard click the New project button:
In the dialog, choose Software localization:
Next, give your project any name, choose English (or any other language that is the default one for your app) as the Base language, and choose one or more Target languages (the ones to translate into):
Let’s suppose, for instance, that I would now like to translate into French in addition to Russian.
Then hit Proceed.
On the next screen, click Upload files:
Choose the ARB files from your project (don’t upload Dart files!). In fact, you can just upload the intl_en.arb
, unless you want to adjust the Russian translations as well.
If you are using pluralization in your ARB files, be sure to check the Detect ICU plurals option (because the plural
is actually an ICU expression):
The main part of the screen will look like this:
What I also recommend doing is adding a special %LANG_ISO%
placeholder in the filename (if it has not been added for you already). Click on the filename and provide the following:
Adding this placeholder is beneficial because it will be replaced with the actual locale code once the file is exported back to the project (otherwise, all the translation files will have identical names).
Press Import files once you are ready.
Now, click Editor in the top menu and observe your translations:
You might notice that the buttonPressed
key does not contain the zero
plural form. Why is that? Well, because it’s actually not a standard form for English. By default, this language only has two forms: one
and other
. If you need additional forms, click on the languages dropdown in the top menu, find the desired locale, and choose Settings:
Then, simply enable the Custom plural forms switch and adjust the forms as needed:
Next, simply re-upload the translation file while enabling the Detect ICU plurals and Replace modified values options.
Of course, you can edit these texts manually, or you can take advantage of Lokalise AI to perform translations for you.
Using Lokalise AI
So, let’s employ Lokalise AI to translate into French. Choose all your translation keys by checking the box in the top left corner:
Select Create a task from the dialog and hit Proceed:
Pick the Automatic translation task type, give it a name, and optionally provide additional instructions for the AI:
Click Next.
Choose the Source language (English in my case) and one or more Target languages to translate into:
Press Create task and wait a little while the AI does the job for you. You’ll get a notification email once everything is done.
Now return to the editor and observe the result:
Note by the way that the AI has even properly detected the French plural forms and provided correct translations for both one
and other
. Still, we recommend double-checking all the translations generated by AI because they are not always 100% ideal.
Once everything is ready, you can download your translations back to the Flutter project.
Downloading translation files
Click Download in the top menu and choose Flutter (.arb) from the File format dropdown:
Be sure not to pick Flutter SDK because this feature relates to OTA, which I’ll briefly explain soon.
Now pick one or more languages to include in the download bundle. Since I have only amended the French translations, I’ll choose one language:
Then click Build and download:
You will get an archive containing your translation files. Extract it into the l10n
folder of the project and don’t forget to run the flutter gen-l10n
command again.
Now run the app and make sure that it has support for three languages. How cool is that?
Introducing the over-the-air flow
Actually, Lokalise can do much more than that. Specifically, we offer a very cool feature that is called over the air (OTA). In a nutshell, it enables you to deliver updated translations to mobile app users without the need to release a new version of the app itself. Sound like magic? Well, perhaps, but you can easily set it up in your Flutter project.
If you are interested in this feature, check out this tutorial that I’ve prepared for you. It covers all the necessary details that will help you take full advantage of OTA.
Conclusion
So, in this article we have learned how to translate Flutter apps into multiple languages, how to perform pluralization and localization, and how to programmatically set the locale. On top of that, we have utilized AI to translate our app into a new language. Great job!
You may also be interested in reading our article on Android localization.
Thank you for staying with me today and happy coding!