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.

4.9 (22) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

  1. 'Dosages' becomes S.of(context).resultsPageAppBarTitle;
  2. 'Lethal Dosage' becomes S.of(context).resultsPageLethalDosageTitle;
  3. 'Daily Safe Maximum' becomes S.of(context).resultsPageSafeDosageTitle;
  4. '*Based on ${drink.servingSize} fl. oz serving.' becomes S.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:
    safeDosage == 1
        ? 'One serving per day.'
        : '${safeDosage.toStringAsFixed(1)} '
          'servings per day.'
    
  • Add:
    S.of(context).resultsPageSafeDosageMessage(
        safeDosage,
        safeDosage.toStringAsFixed(1),
    )
    
  • Remove:
    '*Applies to age 18 and over. This calculator '
    'does not replace professional medical advice.'
    
  • Add:
    S.of(context).resultsPageSecondDisclaimer
    
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.

Translated version of the sample app with instructions for changing the language settings

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:

Many different aspects of localization

Can you guess which aspects of localization apply to Buzz Kill?

  1. 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.
  2. Number Formatting: Unlike the United States, Brazil uses a comma as the decimal separator and a dot as the thousand separator.
  3. Culture: Drip coffees and espressos are pretty common in Brazil as well as in the United States, but lattes aren’t!
  4. 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:

  1. "thirdSuggestedDrinkName": "Latte (Caneca)" with "thirdSuggestedDrinkName": "Pingado (Copo Americano)";
  2. "formPageWeightInputSuffix": "libras" with "formPageWeightInputSuffix": "quilos";
  3. "formPageCustomDrinkServingSizeInputSuffix": "fl. oz" with "formPageCustomDrinkServingSizeInputSuffix": "ml";
  4. "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 NewDart 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:

  1. You use Dart extension methods to add utilities to int and double.
  2. The functions in this extension convert a number to its imperial system counterpart if the user entered it using the metric system.
  3. 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:

  1. lethalDosage.toStringAsFixed(1) becomes numberFormat.format(lethalDosage);
  2. safeDosage.toStringAsFixed(1) becomes numberFormat.format(safeDosage);
  3. 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.

Dog trying to reach a ball