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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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).

Android Studio's preferences dialog

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
  sdk: flutter

You’ve just added two packages to your project:

  1. 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.
  2. 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 ToolsFlutter IntlInitialize for the Project.

Path to Flutter Intl on Android Studio's menu bar

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.

  main_locale: en_US
  enabled: true

Click Pub get in the Flutter commands bar at the top of your screen.

Return to ToolsFlutter Intl on the IDE’s menu bar, but this time, select Remove Locale. Choose en and click OK.

Flutter Intl's Remove Existing Locale dialog

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:

  1. generated: Holds the generated classes. You won’t need to touch them.
  2. 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
  // 2
supportedLocales: S.delegate.supportedLocales,

Here’s what you did:

  1. Remember the intermediate class? The delegate responsible for setting up instances of S? The plugin generated it for you, and you use it here.
  2. 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 ToolsFlutter IntlAdd Locale. Type in pt_BR and click OK.

Flutter Intl's Add New Locale dialog

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:


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.

Initial version of the sample app

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.

Note: You shouldn’t reuse entries, which is why you named them after the place they appear in your app. The same string can have different translations depending on its context.

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.

Initial version of the sample app

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;

void didChangeDependencies() {
  // 3
  final newLocale = Localizations.localeOf(context);
  if (newLocale != _userLocale) {
    _userLocale = newLocale;
    // 4
    _selectedDrinkSuggestion = null;
    _drinkSuggestions = [
        // 5
        name: S.of(context).firstSuggestedDrinkName,
        caffeineAmount: 145,
        servingSize: 8,
        name: S.of(context).secondSuggestedDrinkName,
        caffeineAmount: 77,
        servingSize: 1.5,
        name: S.of(context).thirdSuggestedDrinkName,
        caffeineAmount: 154,
        servingSize: 16,

There’s a lot going on in there:

  1. The property can’t be final anymore. Since you have strings that need to be localized in your list of Drinks, you now need to initialize it inside didChangeDependencies() before the context will be available.
  2. 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.
  3. You’re getting the current Locale.
  4. Here, you reset the input fields.
  5. You defined firstSuggestedDrinkName inside the .arb files and, like magic, it became a property of S. S.of(context) is short for Localizations.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:

  1. 'Death by Caffeine Calculator' becomes S.of(context).formPageAppBarTitle
  2. 'Body Weight' becomes S.of(context).formPageWeightInputLabel
  3. 'pounds' becomes S.of(context).formPageWeightInputSuffix
  4. 'Choose a drink' becomes S.of(context).formPageRadioListLabel
  5. 'Other' becomes S.of(context).formPageCustomDrinkRadioTitle
  6. 'Serving Size' becomes S.of(context).formPageCustomDrinkServingSizeInputLabel
  7. 'fl. oz' becomes S.of(context).formPageCustomDrinkServingSizeInputSuffix
  8. 'Caffeine' becomes S.of(context).formPageCustomDrinkCaffeineAmountInputLabel
  9. 'mg' becomes S.of(context).formPageCustomDrinkCaffeineAmountInputSuffix
  10. ‌'CALCULATE' becomes S.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. :]