String internationalization in Flutter

I was recently reviewing the options for app internationalization in Flutter and there was quite a lot of discussion and content written about it. In this post I’ll describe what I think the right choice is and why, as well as provide a few missing how to guides. The short version of the post is that I think you should use the built-in Flutter tools and avoid some of the suggested alternatives.

Alternatives

Flutter tools

Flutter has a set of tools for handling Internationalization, the intl and intl_translation packages. I think the Flutter team has done a very good job of creating a powerful set of tools that make sensible tradeoffs between complexity and capability. The documentation provided on the Flutter site, Internationalizing Flutter apps, is decent, if a bit complicated, but is definitely the right place to start. The article that filled in some of the blanks left after reading the Flutter docs for me was this one How to Internationalize a Flutter App Using intl and intl_translation. If you read through the Flutter documentation and there are still some unclear concepts, don’t worry, read the second article and it will become much clearer.

JSON

The other main option that I’ve seen suggested online is to dynamically load translations from JSON. At least some of the implementations and tooling for this is based on similar tools from the JS world. There are also some good tutorials out there. Searching Google the top two hits I see are these: Flutter Localization the Easy Way and Flutter internalization tutorial. The second covers three different approaches, the middle of which is the standard Flutter approach. The JSON approach and minor variations have worked well for other projects and there are some very good IDE tools that support it, so it is pretty attractive. Ultimately I decided not to go this way because I don’t think it does a great job on cases like plurality that will turn out to be important.

Comparison

The main negatives of using the Flutter tools that are usually pointed out are complexity and the amount of manual work. So far I don’t find those arguments convincing. There is some setup complexity for the Flutter tools but that’s a one-time thing at the beginning of a project. Once a team has it running it shouldn’t be a problem. The manual effort is a stronger point because there definitely is effort required when adding new translations and you have to copy entries across multiple files. On the other hand translation requires work and I don’t think the amount of manual work is going to be dwarfed by all the other thought and effort that goes into internationalization.

Positives for the Flutter tools to me are completeness and integration. Completeness comes from the thought that obviously went into the details of things like pluralization and gender in language. Pluralization refers to cases like in English where you have ‘1 bowl’ but ‘2 bowls’ while other languages might not pluralize their nouns. Similarly pronouns can vary by gender in ways that require customization to read well and accommodate changing standards for gender and pronoun usage. See the docs here for good examples of both pluralization and gender support. I’m comfortable that using the Flutter tools I’m not going to run into a situation where the translation is held back by the tool. So far in my review of the other approaches I don’t have the same level of comfort.

Usage details

There are a few things that still caught me, despite being relatively simple, once I’d read through those articles so here’s my simple formula for how to make changes once you have a working set of translations. To get to the point where you have a working set of translations I strongly suggest following the steps from How to internationalize a Flutter app using intl and intl_translation. The end result should be a source tree that has these elements (I’ve omitted many directories and files to focus on the important files for internationalization):

my_flutter_app
  ....
  - lib
    - l10n
      intl_messages_en.arb
      intl_messages_fr.arb
      intl_messages.arb
      messages_all.dart
      messages_en.dart
      messages_fr.dart
      messages_messages.dart
    my_app_l10n.dart
    main.dart
    pubspec.yaml
    README.md
    ...

The files shown above in bold are those that have to be manually edited in order to add and change translations. The other files in the l10n folder are generated using tools from the intl packages so they should never be edited by hand.

Here’s the first version of the MyAppLocalizations from the my_flutter_app_l10n.dart file:

class MyAppLocalizations {
  static Future<MyAppLocalizations> load(Locale locale) {
    final String name =
        locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
    final String localeName = Intl.canonicalizedLocale(name);
    return initializeMessages(localeName).then((_) {
      Intl.defaultLocale = localeName;
      return MyAppLocalizations();
    });
  }

  static MyAppLocalizations of(BuildContext context) {
    return Localizations.of<MyAppLocalizations>(
        context, MyAppLocalizations);
  }

  String get title {
    return Intl.message(
      'My App',
      name: 'title',
      desc: 'Title for the application',
    );
  }
}

To add a new translation start with this class and add the new property, see the greeting property in the next version.

class MyAppLocalizations {
  static Future<MyAppLocalizations> load(Locale locale) {
    final String name =
        locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
    final String localeName = Intl.canonicalizedLocale(name);
    return initializeMessages(localeName).then((_) {
      Intl.defaultLocale = localeName;
      return MyAppLocalizations();
    });
  }

  static MyAppLocalizations of(BuildContext context) {
    return Localizations.of<MyAppLocalizations>(
        context, MyAppLocalizations);
  }

  String get title {
    return Intl.message(
      'My App',
      name: 'title',
      desc: 'Title for the application',
    );
  }

  String get greeting {
    return Intl.message(
      'Welcome to My App, so glad you decided to try it!',
      name: 'greeting',
      desc: 'A welcome message for the user',
    );
  }  
}

Now, regenerate the base arb file using the command line tools from the project folder.

flutter pub run intl_translation:extract_to_arb --output-dir=lib/l10n lib/my_app_l10n.dart

The result of that will be an updated copy of the file l10n/intl_messages.arb which has the new greeting property in it, like so:

  "greeting": "Welcome to My App, so glad you decided to try it!",
  "@greeting": {
    "description": "A welcome message for the user",
    "type": "text",
    "placeholders": {}
  }

Note that this new property is not yet in any of the translated files. This is the manual step, copy that definition for the new greeting property from the l10n/intl_messages.arb to l10n/intl_messages_en.arb and l10n/intl_messages_fr.arb and then get the translation for the l10n/intl_messages_fr.arb version. Once you have all the translations you need regenerate the generated Dart files to incorporate the changes. The command to regenerate the Dart files is:

flutter pub run intl_translation:generate_from_arb lib/my_app_l10n.dart lib/l10n/*.arb  --output-dir=lib/l10n

The app is now ready to run with the new property available. Here’s a couple of bonus examples that show the syntax for defining properties with placeholders and with plurals.

  String userGreeting( String name ) {
    return Intl.message(
      'Hi $name',
      name: 'userGreeting',
      args: [name],
      desc:
          'Used to greet the user when the app first opens',
    );
  }

String unreadMessages(int unread, String name) {
    Intl.message(
      '''${Intl.plural(unread,
          zero: 'No unread messages for $name.',
          one: 'There is $unread unread message for $name.',
          other: 'There are $unread unread messages for $name.')}''',
    name: 'unreadMessages',
    args: [unread, name],
    desc: How many current unread messages.',
    examples: const {'unread': 2, 'name': 'Taylor'});
  }

Late breaking news

Turns out that Google Translate, pretty much the only service in the world that uses .arb files, is shutting down in December 2019. Likely the Flutter team will migrate from the .arb format to something else fairly soon. Hopefully the translation workflow won’t change, just the file format. I’ll update this post as more information becomes available. See this issue for some more details.

Leave a Reply