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?
Adjusting the Results Page
You’re halfway there. Now, it’s time to give lib/pages/results_page.dart the same treatment. Open the file and add this import at the top of the imports block:
import 'package:buzzkill/generated/l10n.dart';
Now, move on to build()
and make these replacements:
-
'Dosages'
becomesS.of(context).resultsPageAppBarTitle
; -
'Lethal Dosage'
becomesS.of(context).resultsPageLethalDosageTitle
; -
'Daily Safe Maximum'
becomesS.of(context).resultsPageSafeDosageTitle
; -
'*Based on ${drink.servingSize} fl. oz serving.'
becomesS.of(context).resultsPageFirstDisclaimer(drink.servingSize)
;
You skipped a few strings in the build()
method, so double back and make sure to update them:
-
- Remove:
lethalDosage == 1 ? 'One serving.' : '${lethalDosage.toStringAsFixed(1)} ' 'servings in your system at one time.'
- Add:
S.of(context).resultsPageLethalDosageMessage( lethalDosage, lethalDosage.toStringAsFixed(1), )
- Remove:
-
- Remove:
safeDosage == 1 ? 'One serving per day.' : '${safeDosage.toStringAsFixed(1)} ' 'servings per day.'
- Add:
S.of(context).resultsPageSafeDosageMessage( safeDosage, safeDosage.toStringAsFixed(1), )
- Remove:
-
- Remove:
'*Applies to age 18 and over. This calculator ' 'does not replace professional medical advice.'
- Add:
S.of(context).resultsPageSecondDisclaimer
- Remove:
lethalDosage == 1
? 'One serving.'
: '${lethalDosage.toStringAsFixed(1)} '
'servings in your system at one time.'
S.of(context).resultsPageLethalDosageMessage(
lethalDosage,
lethalDosage.toStringAsFixed(1),
)
safeDosage == 1
? 'One serving per day.'
: '${safeDosage.toStringAsFixed(1)} '
'servings per day.'
S.of(context).resultsPageSafeDosageMessage(
safeDosage,
safeDosage.toStringAsFixed(1),
)
'*Applies to age 18 and over. This calculator '
'does not replace professional medical advice.'
S.of(context).resultsPageSecondDisclaimer
Notice that some of these aren’t properties of S
, but functions, because they need arguments like a plurals value or filler text for a placeholder.
Finally, build and run the app. Mess with the device’s language configuration and watch the app respond accordingly. The .gif below demonstrates how to do that on Android (on the left) and iOS (on the right), but the steps may vary on your OS version.
Congratulations, you’ve translated your first app!
Going Beyond Translation
Not to be a total buzzkill (ha!), but you’re just getting started with localization. Simply put: The product must feel local, and that goes beyond just the language. Different regions use different formats for everything from the time of day to how they write out phone numbers.
They say a picture is worth a thousand words. This one shows some things that localization covers:
Can you guess which aspects of localization apply to Buzz Kill?
- Measurement Units: Brazilians — and most of the rest of the world — don’t use the imperial system. Translating “pounds” and “fl. oz” into Portuguese doesn’t mean that people will understand them.
- Number Formatting: Unlike the United States, Brazil uses a comma as the decimal separator and a dot as the thousand separator.
- Culture: Drip coffees and espressos are pretty common in Brazil as well as in the United States, but lattes aren’t!
- Text Direction: In some languages, writing goes from the right to the left (RTL). Localization for these languages goes beyond just text direction. For example, if your left and right paddings have different values, you want to switch them for an RTL locale. Buzz Kill doesn’t support any RTL languages at the moment, but you’ll see how easy it is to be ready to support them right out of the box.
Next, you’ll put these localization features in place.
Making the App Feel Local
You’ll start by changing the text. Roll up your sleeves, open lib/l10n/intl_pt_BR.arb and replace:
-
"thirdSuggestedDrinkName": "Latte (Caneca)"
with"thirdSuggestedDrinkName": "Pingado (Copo Americano)"
; -
"formPageWeightInputSuffix": "libras"
with"formPageWeightInputSuffix": "quilos"
; -
"formPageCustomDrinkServingSizeInputSuffix": "fl. oz"
with"formPageCustomDrinkServingSizeInputSuffix": "ml"
; -
"resultsPageFirstDisclaimer": "*Baseado em uma porção de {servingSize} fl. oz."
with"resultsPageFirstDisclaimer": "*Baseado em uma porção de {servingSize} ml."
;
Save the file with Command-S on macOS or Control-S on Linux or Windows.
First, you’ve changed the third suggestion name from Latte to a well-known caffeinated friend of Brazilians, the pingado. Then you changed the weight measure name from pounds to kilograms, and the liquid volume measure name from fluid ounces to milliliters.
As you might have guessed, changing measure unit names isn’t enough. You need some math to convert them.
Converting Measures
Create a new file by right-clicking the lib folder and choosing New ▸ Dart File. Name it measurement_conversion and enter the following code:
import 'package:flutter/widgets.dart';
bool _shouldUseImperialSystem(Locale locale) {
final countryCode = locale.countryCode;
return countryCode == 'US';
}
// 1
extension IntMeasurementConversion on int {
int get _roundedPoundFromKg => (this * 2.20462).round();
double get _flOzFromMl => this * 0.033814;
// 2
int toPoundsIfNotAlready(Locale locale) {
if (_shouldUseImperialSystem(locale)) {
return this;
}
return _roundedPoundFromKg;
}
double toFlOzIfNotAlready(Locale locale) {
if (_shouldUseImperialSystem(locale)) {
return toDouble();
}
return _flOzFromMl;
}
}
extension DoubleMeasurementConversion on double {
int get _roundedMlFromFlOz => (this * 29.5735).round();
// 3
double toMillilitersIfShouldUseMetricSystem(Locale locale) {
if (_shouldUseImperialSystem(locale)) {
return this;
}
return _roundedMlFromFlOz.toDouble();
}
}
Going over it step-by-step:
- You use Dart extension methods to add utilities to
int
anddouble
. - The functions in this extension convert a number to its imperial system counterpart if the user entered it using the metric system.
- Finally, you’re converting the number to the metric system if the user isn’t using the imperial system.
Now, you need to use the functionalities you’ve just added.
Implementing the Measurement Conversion
Go to lib/pages/form_page.dart and add an import to the previously created file at the top:
import 'package:buzzkill/measurement_conversion.dart';
Inside _pushResultsPage()
, replace the weight
and drink
variable declarations with:
final weight =
_weightTextController.intValue
.toPoundsIfNotAlready(
_userLocale,
);
final drink = _selectedDrinkSuggestion ??
Drink(
caffeineAmount: _caffeineTextController.intValue,
servingSize: _servingSizeTextController.intValue
.toFlOzIfNotAlready(
_userLocale,
),
);
If the user entered the weight in kilograms and the serving size in milliliters, this code converts them to their imperial system alternatives because that’s how ResultsPage
and Drink
expect them.
Now, it’s time to make the numbers look like your Brazilian users expect them to.
Formatting Numbers
Go to lib/pages/results_page.dart and, at the top of the import block, add these two imports:
import 'package:buzzkill/measurement_conversion.dart';
import 'package:intl/intl.dart';
At the beginning of the build()
method, add this line below the declaration of lethalDosage
:
final numberFormat = NumberFormat('#.#');
NumberFormat
comes from Intl. It handles the decimal/thousand separators localization.
Still in build()
, make these substitutions:
-
lethalDosage.toStringAsFixed(1)
becomesnumberFormat.format(lethalDosage)
; -
safeDosage.toStringAsFixed(1)
becomesnumberFormat.format(safeDosage)
; -
S.of(context).resultsPageFirstDisclaimer(drink.servingSize)
becomes:S.of(context).resultsPageFirstDisclaimer( numberFormat.format( drink.servingSize.toMillilitersIfShouldUseMetricSystem( Localizations.localeOf(context), ), ), )
S.of(context).resultsPageFirstDisclaimer(
numberFormat.format(
drink.servingSize.toMillilitersIfShouldUseMetricSystem(
Localizations.localeOf(context),
),
),
)
In the first two, you started using the numberFormat
to format your numbers. The last substitution also uses one of the lib/measurement_conversion.dart functions to display the serving size converted back to milliliters, in case the user is not in the U.S.