Internationalizing and Localizing Your Flutter App
Learn how to use the flutter_localization and Intl packages to easily localize and internationalize your app, making it accessible to users in different locales. By Edson Bueno.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Internationalizing and Localizing Your Flutter App
30 mins
- Getting Started
- Differentiating Between Internationalization and Localization
- Internationalizing in Flutter
- Maintaining Translations
- Setting up Flutter Intl
- Generating Classes
- Configuring Your App
- Extracting the Strings
- Adding Brazilian Portuguese Translations
- Removing Hard-Coded Values
- Adjusting the Results Page
- Going Beyond Translation
- Making the App Feel Local
- Converting Measures
- Implementing the Measurement Conversion
- Formatting Numbers
- Customizing Drink Suggestions
- Preparing for RTL Languages
- Where to Go From Here?
Setting up Flutter Intl
Open Android Studio’s preferences by pressing Command-, (comma) on macOS or Control-Alt-S on Linux or Windows.
Select Plugins on the left-side panel (1) and Marketplace in the upper tab bar (2). Type intl in the search bar (3), then click Install for Localizely‘s Flutter Intl result (4).
With the plugin installed, restart your IDE.
Generating Classes
Open pubspec.yaml and replace # TODO: Add intl and flutter_localizations here.
with:
intl: 0.16.1
flutter_localizations:
sdk: flutter
You’ve just added two packages to your project:
- intl: Facilities for i18n and l10n such as message translation and date/number formatting and parsing. You’ll mostly deal with this package through the generated classes.
- flutter_localizations: Hang tight! You’ll learn more about this in just a second.
Select the starter folder in the project directory then, on the menu bar, select Tools ▸ Flutter Intl ▸ Initialize for the Project.
The command above added a flutter_intl
section to your pubspec.yaml. Add main_locale: en_US
to it, below and in the same indentation level of enabled: true
.
flutter_intl:
main_locale: en_US
enabled: true
Click Pub get in the Flutter commands bar at the top of your screen.
Return to Tools ▸ Flutter Intl on the IDE’s menu bar, but this time, select Remove Locale. Choose en and click OK.
Here, you changed your default locale from en to en_US, properly identifying the nuance. That’ll allow you to execute custom logic based on the country code later.
The plugin created two folders, including some files, inside lib:
- generated: Holds the generated classes. You won’t need to touch them.
- l10n: Home of your .arbs, the JSON syntax files that’ll hold your translations. The one for American English is already there for you.
Now that you’ve set up Flutter Intl, it’s time to configure your app to use it.
Configuring Your App
Go to lib/main.dart, and add these two imports below the others:
import 'package:buzzkill/generated/l10n.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
Now, inside MaterialApp
, remove // TODO: Specify localizationsDelegates and supportedLocales.
and add this instead:
localizationsDelegates: [
// 1
S.delegate,
// 2
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
Here’s what you did:
- Remember the intermediate class? The delegate responsible for setting up instances of
S
? The plugin generated it for you, and you use it here. - Not only does your app’s text need translations, but some Flutter widgets do as well. The delegates taking care of them came from the flutter_localizations package you added above.
Back in the menu bar, select Tools ▸ Flutter Intl ▸ Add Locale. Type in pt_BR and click OK.
The lib/l10n folder now contains a new file — intl_pt_BR.arb — and the Android version of your app is ready to support en-US and pt-BR.
Only the Android version? Not the iOS? Yep! That’s because one specific iOS configuration file needs your special care. The good news is that you don’t need Xcode to do it.
In your project structure, open ios/Runner/Info.plist and substitute <!-- TODO: Specify supported locales. -->
with:
<key>CFBundleLocalizations</key>
<array>
<string>en_US</string>
<string>pt_BR</string>
</array>
Stop the previous execution of the app, then build and run it again to make sure you haven’t introduced any errors. Don’t expect any visual changes.
Extracting the Strings
Replace what’s in lib/l10n/intl_en_US.arb with:
{
"@@locale": "en_US",
"formPageAppBarTitle": "Death by Caffeine Calculator",
"firstSuggestedDrinkName": "Drip Coffee (Cup)",
"secondSuggestedDrinkName": "Espresso (Shot)",
"thirdSuggestedDrinkName": "Latte (Mug)",
"formPageWeightInputLabel": "Body Weight",
"formPageWeightInputSuffix": "pounds",
"formPageRadioListLabel": "Choose a drink",
"formPageActionButtonTitle": "CALCULATE",
"formPageCustomDrinkRadioTitle": "Other",
"formPageCustomDrinkServingSizeInputLabel": "Serving Size",
"formPageCustomDrinkServingSizeInputSuffix": "fl. oz",
"formPageCustomDrinkCaffeineAmountInputLabel": "Caffeine",
"formPageCustomDrinkCaffeineAmountInputSuffix": "mg",
"resultsPageAppBarTitle": "Dosages",
"resultsPageLethalDosageTitle": "Lethal Dosage",
"resultsPageFirstDisclaimer": "*Based on {servingSize} fl. oz serving.",
"resultsPageLethalDosageMessage": "{quantity, plural, one{One serving.} other{{formattedNumber} servings in your system at one time.}}",
"resultsPageSafeDosageTitle": "Daily Safe Maximum",
"resultsPageSafeDosageMessage": "{quantity, plural, one{One serving per day.} other{{formattedNumber} servings per day.}}",
"resultsPageSecondDisclaimer": "*Applies to age 18 and over. This calculator does not replace professional medical advice."
}
These are the en-US entries for every visible line of text within Buzz Kill. Pay special attention to:
- @@locale: Identifies the locale of the file. If you don’t add it, Intl can infer it from the file name, but it’ll give you a warning.
-
resultsPageFirstDisclaimer:
{servingSize}
is a placeholder.S
dynamically replaces it with the value you specify when using the function it generates. -
resultsPageLethalDosageMessage and resultsPageSafeDosageMessage: These are plurals. Their values depend on a number specified when calling the function. Besides that, they also use a
{formattedNumber}
placeholder, like resultsPageFirstDisclaimer.
Don’t worry! These concepts are easier to grasp when you see them reflected on the Dart side.
Now that you have your .arb file set up for American English, it’s time to add the Brazilian Portuguese version to Buzz Kill.
Adding Brazilian Portuguese Translations
This time, inside lib/l10n/intl_pt_BR.arb, replace everything with:
{
"@@locale": "pt_BR",
"formPageAppBarTitle": "Calculadora de Morte por Cafeína",
"firstSuggestedDrinkName": "Café Coado (Xícara)",
"secondSuggestedDrinkName": "Espresso (Shot)",
"thirdSuggestedDrinkName": "Latte (Caneca)",
"formPageWeightInputLabel": "Peso Corporal",
"formPageWeightInputSuffix": "libras",
"formPageRadioListLabel": "Escolha uma bebida",
"formPageActionButtonTitle": "CALCULAR",
"formPageCustomDrinkRadioTitle": "Outra",
"formPageCustomDrinkServingSizeInputLabel": "Tamanho",
"formPageCustomDrinkServingSizeInputSuffix": "fl. oz",
"formPageCustomDrinkCaffeineAmountInputLabel": "Cafeína",
"formPageCustomDrinkCaffeineAmountInputSuffix": "mg",
"resultsPageAppBarTitle": "Dosagens",
"resultsPageLethalDosageTitle": "Dose Letal",
"resultsPageLethalDosageMessage": "{quantity, plural, one{Uma porção.} other{{formattedNumber} porções no seu sistema de uma vez.}}",
"resultsPageSafeDosageTitle": "Limite Seguro Diário",
"resultsPageSafeDosageMessage": "{quantity, plural, one{Uma porção por dia.} other{{formattedNumber} porções por dia.}}",
"resultsPageFirstDisclaimer": "*Baseado em uma porção de {servingSize} fl. oz.",
"resultsPageSecondDisclaimer": "*Se aplica a pessoas com 18 anos ou mais. Essa calculadora não substitui conselhos médicos profissionais."
}
There’s nothing new here. These are the Portuguese translations for the same lib/l10n/intl_en_US.arb entries.
Trigger the classes to re-generate by saving the file with Command-S on macOS or Control-S on Linux or Windows.
Build and run again. Everything should look the same as before, and now you’re ready to code for real.
You’ve accomplished a lot, but the app won’t reflect your changes until you remove the hard-coded values from your widgets.
Removing Hard-Coded Values
Your journey begins on lib/pages/form_page.dart, Buzz Kill’s home. Open the file and start by adding this line at the top of the imports block:
import 'package:buzzkill/generated/l10n.dart';
The UI presents the user with three suggested caffeinated drinks. In code, you control them with _drinkSuggestions
. Remove the current _drinkSuggestions
definition and add this instead:
// 1
List<Drink> _drinkSuggestions;
// 2
Locale _userLocale;
@override
void didChangeDependencies() {
// 3
final newLocale = Localizations.localeOf(context);
if (newLocale != _userLocale) {
_userLocale = newLocale;
// 4
_weightTextController.clear();
_servingSizeTextController.clear();
_caffeineTextController.clear();
_selectedDrinkSuggestion = null;
_drinkSuggestions = [
Drink(
// 5
name: S.of(context).firstSuggestedDrinkName,
caffeineAmount: 145,
servingSize: 8,
),
Drink(
name: S.of(context).secondSuggestedDrinkName,
caffeineAmount: 77,
servingSize: 1.5,
),
Drink(
name: S.of(context).thirdSuggestedDrinkName,
caffeineAmount: 154,
servingSize: 16,
),
];
}
super.didChangeDependencies();
}
There’s a lot going on in there:
- The property can’t be final anymore. Since you have strings that need to be localized in your list of
Drink
s, you now need to initialize it insidedidChangeDependencies()
before thecontext
will be available. -
Locale
contains information about (drum roll, please), the current locale! You use this property to track locale changes while the app is open. That way, you can reset your form fields and suggestions if a change occurs. - You’re getting the current
Locale
. - Here, you reset the input fields.
- You defined
firstSuggestedDrinkName
inside the .arb files and, like magic, it became a property ofS
.S.of(context)
is short forLocalizations.of<S>(context, S)
;
Now that you’ve translated the suggestions, it’s time to move on to build()
. From top to bottom, here’s a sequence of substitutions for you to make:
-
'Death by Caffeine Calculator'
becomesS.of(context).formPageAppBarTitle
-
'Body Weight'
becomesS.of(context).formPageWeightInputLabel
-
'pounds'
becomesS.of(context).formPageWeightInputSuffix
-
'Choose a drink'
becomesS.of(context).formPageRadioListLabel
-
'Other'
becomesS.of(context).formPageCustomDrinkRadioTitle
-
'Serving Size'
becomesS.of(context).formPageCustomDrinkServingSizeInputLabel
-
'fl. oz'
becomesS.of(context).formPageCustomDrinkServingSizeInputSuffix
-
'Caffeine'
becomesS.of(context).formPageCustomDrinkCaffeineAmountInputLabel
-
'mg'
becomesS.of(context).formPageCustomDrinkCaffeineAmountInputSuffix
-
'CALCULATE'
becomesS.of(context).formPageActionButtonTitle
Go through the build()
method and replace each instance of the above strings with their corresponding localized variants.
Wow, that was heavy-ish! If only the Buzz Kill’s developer had been smart enough to internationalize it from the start. :]