跳转至

9 Internationalizing & Localizing

At its most basic form, internationalization is the process of removing hard-coded text from your codebase, like Text('Hello'), and replacing it with dynamic properties, like Text(l10n.homeScreenGreetings). The first reason to do that is to have a more organized codebase, and the second is to lay the groundwork for localizing your app.

Localizing means adding support for another language. Spot the distinction between internationalization and localization:

  • Internationalization is the engineering effort of making sure your app is translatable, even if you don’t plan to support more than one language at the moment — or at any moment.
  • Localization is taking advantage of an already internationalized codebase and feeding it the translations it needs to support another language.

But, of course, things can always be more complex. Internationalization and localization often go way beyond just text translation. Different regions write dates differently and can have different phone number formats, addresses, measurement units, currencies, etc. But that’s not for today.

In this chapter, you’ll learn how to:

  • Internationalize your app.
  • Best organize internationalized messages.
  • Localize your app to add support for another language.
  • Approach internationalization and localization in a multi-package codebase.

One quick explanation before you get your feet wet: Internationalization is also referred to as “i18n”. Why? It encompasses the first and last letters of “internationalization” — “i” and “n” — and then substitutes the number “18” for the 18 letters in between — “nternationalizatio”. It’s the same reason people also call localization “l10n”.

Throughout this chapter, you’ll work on the starter project from this chapter’s assetsfolder.

Getting Started

Use your IDE of choice to open the starter project. Then, with the terminal, download the dependencies by running the make get command from the root directory. Wait for the command to finish executing, then build and run your app. This is what you’ll see:

img

Note

If you’re having trouble running the app, it’s because you forgot to propagate the configurations you did in the first chapter’s starter project to the following chapters’ materials. If that’s the case, please revisit Chapter 1, “Setting up Your Environment”.

Your app is pretty much complete, except that the only language it supports at the moment is English. Your goal from here on will be to add Portuguese support to it. Why Portuguese? The most populous country that speaks Portuguese is Brazil, which has the third-most app downloads worldwide, and a population where only 5% speaks English. That makes it an excellent target for localization.

Note

You’ll only localize text that’s embedded in the codebase. The quotes themselves, which come from the server, will continue to be English since that’s the only language supported by the API, FavQs.com.

Generating Internationalization Files

The first thing to call out is that WonderWords is a multi-package project, where each screen in the app lives in an isolated package:

img

That means the internationalization process needs to happen individually for each one of these. To save you some time and repetitive work, most packages have already been taken care of for you. Your responsibility will be to handle profile_menu, which holds the code for the first screen on the Profile tab:

img

As of now, this screen’s code contains only hard-coded text; your job will be to replace it with dynamic values. Kick off this work by opening the profile_menu package’s pubspec.yaml:

img

Now, replace ## TODO: Add l10n dependencies. with:

flutter_localizations:
  sdk: flutter
intl: ^0.17.0

Done! Those are the two dependencies you need to add internationalization support to an app. Your IDE may warn you to re-fetch your dependencies since you’ve changed pubspec.yaml, but you don’t need to do it in this case.

Next, still inside the profile_menu package, expand the lib/src directory and create a new folder there named l10n — please note that the first letter is a lowercase “L” and not a capital “I”.

img

Then, create a new file under your new folder and name it messages_en.arb.

img

Add the following to the file you just created:

{
  "signInButtonLabel": "Sign In",
  "signedInUserGreeting": "Hi, {username}!",
  "@signedInUserGreeting": {
    "placeholders": {
      "username": {
        "type": "String"
      }
    }
  },
  "updateProfileTileLabel": "Update Profile",
  "darkModePreferencesHeaderTileLabel": "Dark Mode Preferences",
  "darkModePreferencesAlwaysDarkTileLabel": "Always Dark",
  "darkModePreferencesAlwaysLightTileLabel": "Always Light",
  "darkModePreferencesUseSystemSettingsTileLabel": "Use System Settings",
  "signOutButtonLabel": "Sign Out",
  "signUpOpeningText": "Don't have an account?",
  "signUpButtonLabel": "Sign up"
}

This messages_en.arb file is what you’ll use to maintain the English version of all the translatable text you have in this package. Notice the JSON format of the content you just inserted — arb files are simply JSONs with extra features.

Internationalization in Flutter works heavily based on code generation. Later, you’ll run a command that will cause a Flutter tool to parse this arb file and generate a Dart class for you based on it. This generated class is then what you’ll use to access these values from your Flutter code, like: Text(l10n.signInButtonLabel).

There are two things worth noticing in the JSON snippet above:

  1. Notice how you named your JSON properties. The keys describe where that message will be used and not the content of the message. For example, signInButtonLabel instead of just signIn. This is because you shouldn’t reuse messages. If the same text appears in two different places of the app or screen, you should have two different entries in your arb file. The first reason for this is that the same text can have different translations depending on where you’re using it. The second reason is you might want to change the value for one place without affecting the other.
  2. Look at the signedInUserGreeting and @signedInUserGreeting entries. This is how you define a message with a dynamic parameter you want to inject from your Flutter code, like Text(l10n.signedInUserGreeting(username))— where username is a runtime value you got from the server, for example. For context, this signedInUserGreeting is the message that appears at the top of the screen for a signed-in user:

img

Awesome. Now that you have your messages’ source in place — at least the English version — you need to put some configuration in place before being able to ask Flutter to generate the corresponding Dart code for you.

Create a new file named l10n.yaml under the profile_menu package’s root folder.

img

Add this content to your newly created file:

arb-dir: lib/src/l10n
template-arb-file: messages_en.arb
output-localization-file: profile_menu_localizations.dart
output-class: ProfileMenuLocalizations
nullable-getter: false
synthetic-package: false

This l10n.yaml configuration file is where you give Flutter the guidance it needs to generate the code you’ll interact with. Here’s a walkthrough of what each line in there is doing:

  • arb-dir: Tells Flutter where it can find your arb files — you only have one at the moment, but you’ll create another soon.
  • template-arb-file: Tells Flutter what your main arb file is.
  • output-localization-file: How you want it to name your generated Dart file. The default is app_localizations.dart, which is too generic for a project containing several packages.
  • output-class: Same as the above, but now for the name of the actual Dart class instead of the file.
  • nullable-getter: If this was set to true, you’d need to check for nullability whenever accessing a property from your Flutter code, like: l10n?.signedInUserGreeting.
  • synthetic-package: By default, Flutter generates your localization files under a hidden/synthetic package that’s only visible to the package you generated the files for. This doesn’t work for WonderWords’ multi-package structure; you need to be able to export your localization files so you can access them from your mainpackage to plug them into your MaterialApp.

Finally, open the terminal, and using the cd command, navigate to the profile_menupackage’s root folder. From there, run the flutter gen-l10n command. Ensure the latest command generated two new files for you under the l10n folder you created a few steps ago.

img

Note

The command above will output a message to your console saying Because l10n.yaml exists, [...]. Just ignore it.

Replacing Hard-Coded Text

You have everything you need to start internationalizing your app. Time for the main act.

Inside lib/src, open profile_menu_screen.dart.

img

Add the following import line to the top of the file:

import 'package:profile_menu/src/l10n/profile_menu_localizations.dart';

Now, replace all three instances of // TODO: Get a ProfileMenuLocalizations instance. in this file with:

final l10n = ProfileMenuLocalizations.of(context);

This ProfileMenuLocalizations is the class Flutter generated for you, mirroring the content you have in your messages_en.arb file. In fact, if you remember, the name ProfileMenuLocalizations was your choice inside l10n.yaml.

Notice you didn’t instantiate ProfileMenuLocalizations. Instead, you used the ProfileMenuLocalizations.of(context) call, which will get you an instance based on the device’s language. Since you only support English at the moment, this will always return an instance containing English messages.

Continuing on profile_menu_screen.dart, you’ll now replace all the hard-coded text in this file with dynamic properties from these l10n variables you created. Do this by replacing:

  1. 'Don\'t have an account?', with l10n.signUpOpeningText,.
  2. 'Sign up', with l10n.signUpButtonLabel,.
  3. 'Hi, $username!', with l10n.signedInUserGreeting(username),.
  4. label: 'Update Profile', with label: l10n.updateProfileTileLabel,.
  5. label: 'Sign In', with label: l10n.signInButtonLabel,.
  6. label: 'Sign Out', with label: l10n.signOutButtonLabel,. You’ll find this one twice in this file; replace both.

That’s all for this file! Now, you’ll do the same for dark_mode_preference_picker.dart.

img

Start by replacing // TODO: Get a ProfileMenuLocalizations instance. with:

final l10n = ProfileMenuLocalizations.of(context);

Then, replace:

  1. 'Dark Mode Preferences', with l10n.darkModePreferencesHeaderTileLabel,.
  2. 'Always Dark', with l10n.darkModePreferencesAlwaysDarkTileLabel,.
  3. 'Always Light', with l10n.darkModePreferencesAlwaysLightTileLabel,.
  4. 'Use System Settings', with l10n.darkModePreferencesUseSystemSettingsTileLabel,.

Done! Your codebase is now completely free of hard-coded text.

Pluging Localization Classes Into MaterialApp

Now, before running your project, you need to connect that ProfileMenuLocalizations class to the MaterialApp you have on main.dart. The problem is: As of now, ProfileMenuLocalizations is only visible within the profile_menu package, and your MaterialApp lives inside the main package.

To address that, open the profile_menu.dart file, which lives outside the src folder of the profile_menu package.

img

Delete // TODO: Export ProfileMenuLocalizations., and add this instead:

export 'src/l10n/profile_menu_localizations.dart';

That’s it! Now ProfileMenuLocalizations is visible to any packages depending on profile_menu.

Move on to the main.dart file in your root package.

img

Scroll down until you find // TODO: Add ProfileMenuLocalizations' delegate., and replace it with:

ProfileMenuLocalizations.delegate,

Here, you’re plugging the delegate property of your ProfileMenuLocalizationsclass into your MaterialApp. This delegate property holds an object that knows how to create and recreate instances of ProfileMenuLocalizations based on the device’s language. Notice that the other packages’ delegates are already added for you in that same array.

Build and run your app. It will look exactly the same as before — still supporting English only — but it’s now completely internationalized — meaning it doesn’t contain hard-coded messages and is ready to support new languages.

img

Adding Portuguese Support

Head back to the profile_menu feature package and expand the lib/src/l10n folder. Create a new file in there named messages_pt.arb.

img

Insert the following code inside your new file:

{
  "signInButtonLabel": "Entrar",
  "signedInUserGreeting": "Olá, {username}!",
  "@signedInUserGreeting": {
    "placeholders": {
      "username": {
        "type": "String"
      }
    }
  },
  "updateProfileTileLabel": "Atualizar Perfil",
  "darkModePreferencesHeaderTileLabel": "Configurações de Modo Noturno",
  "darkModePreferencesAlwaysDarkTileLabel": "Sempre Escuro",
  "darkModePreferencesAlwaysLightTileLabel": "Sempre Claro",
  "darkModePreferencesUseSystemSettingsTileLabel": "De Acordo com o Sistema",
  "signOutButtonLabel": "Sair",
  "signUpOpeningText": "Não tem uma conta?",
  "signUpButtonLabel": "Cadastrar"
}

This mirrors what you have in messages_en.arb, but here the messages are all in Portuguese. Also, notice the pattern in the file names: messages_LANGUAGE.arb. Now, all you have to do is ask Flutter to regenerate the localization files for you.

Using a terminal, navigate to the profile_menu package’s root folder and, from there, run the flutter gen-l10n command one more time. You’ll see a profile_menu_localizations_pt.dart file pop up under the l10n folder.

img

Note

Notice you didn’t have to change l10n.yaml to specify your new arb file. It already knows which folder to scan.

Now, you have to explicitly say to your MaterialApp that you want to support Portuguese. For that, go back to the main.dart file.

img

Replace // TODO: Add supported locales. with:

supportedLocales: const [
  Locale('en', ''),
  Locale('pt', ''),
],

Pretty straightforward, right? This is how you tell MaterialApp which languages you want to enable for your app.

Lastly, you need to add two more delegates to the localizationsDelegates list. Do this by replacing // TODO: Add Flutter's delegates. with:

GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,

Here’s what’s going on: The other delegates that were already sitting on this localizationsDelegates list cover localizations for custom code created by you. Now that you officially listed Portuguese as one of your supported languages, you’re also adding the delegates that handle localizations for Flutter’s stock components — from both the Material and the Cupertino libraries.

Test all the work you did by first changing your device’s language to Portuguese — from either Brazil or Portugal. Then, build and run your app for the last time. Your app’s messages should all now be in Portuguese — don’t forget the quotes will continue to be in English. Take a special look at the Profile tab’s first screen, which is the one youinternationalized. Congratulations!

img

Key Points

  • Internationalization is the process of removing any hard-coded values in your codebase that would need to change if you were to support another language. That can include text images, units, date formats, etc.
  • Localization is the process of adding another language’s translations to the translatable resources you have in an internationalized codebase.
  • Ideally, you should internationalize your projects from the start.
  • Always name your internationalized Strings after the place they appear in your app. The same text can have different translations depending on where it appears.
  • Text internationalization in Flutter is heavily based on code generation. All you have to do is maintain your text in JSON-like files and ask Flutter to parse those files and generate Dart versions of them for you to consume from your code.