i18n? Mind-blowing Easy Translations and Internationalization for Flutter

Unleash the power of Dart extensions

Marcelo Glasberg
Oct 24, 2019 · 6 min read

I’m always interested in creating packages to reduce boilerplate. For example, async_redux is about Redux without boilerplate, and align_positioned is about creating layouts with less widgets.

In this article I talk about the i18n_extension package which is about non-boilerplate translations. This package was mentioned by Google during the Dart 2.7 announcement.

Start with a widget with some text in it:

Text("Hello, how are you?")

Translate it simply by adding to the string:

Text("Hello, how are you?".i18n)

If the current locale is then the text in the screen will be
which is the Portuguese translation to the above text.

And so on for any other locales you want to support.

You can also provide different translations depending on modifiers, for example quantities:

print("There is 1 item".plural(0)); // Prints There are no items
print("There is 1 item".plural(1)); // Prints There is 1 item
print("There is 1 item".plural(2)); // Prints There are 2 items

And you can invent your own modifiers according to any conditions. For example, some languages have different translations for different genders.
So you could create versions for modifiers:

print("It's a person".gender(Gender.male)); // Prints It's a man
print("It's a person".gender(Gender.female)); // Prints It's a woman
print("It's a person".gender(Gender.they)); // Prints It's a person

See it working

Try running the example.

Setup

Wrap your widget tree with the widget, below the :

import "package:i18n_extension/i18n_widget.dart";
...
@override
Widget build(BuildContext context) {
return MaterialApp(
home: I18n(child: ...)
);
}

The above code will translate your strings to the current system locale.

Or you can override it with your own locale, like this:

I18n(
initialLocale: Locale("pt", "BR"),
child: ...

Translating a widget

For each widget (or related group of widgets) you should create a translation file like this:

import "package:i18n_extension/i18n_extension.dart";
extension Localization on String {
String get i18n => localize(this, t);
static var t = Translations("en_us") +
{
"en_us": "Hello, how are you?",
"pt_br": "Olá, como vai você?",
"es": "¿Hola! Cómo estás?",
"fr": "Salut, comment ca va?",
"de": "Hallo, wie geht es dir?",
};
}

The above example shows a single “translatable string”, translated to American English, Brazilian Portuguese, and general Spanish, French and German.

You can, however, translate as many strings as you want, by simply adding more translation maps:

import "package:i18n_extension/i18n_extension.dart";
extension Localization on String {
String get i18n => localize(this, t);
static var t = Translations("en_us") +
{
"en_us": "Hello, how are you?",
"pt_br": "Olá, como vai você?",
} +
{
"en_us": "Hi",
"pt_br": "Olá",
} +
{
"en_us": "Goodbye",
"pt_br": "Adeus",
};
}

Strings themselves are the translation keys

The locale you pass in the constructor is called the default locale. All translatable strings in the widget file should be in the language of that locale.

The strings themselves are used as keys when searching for translations to the other locales. For example, in the below, is both the translation to English and the key to use when searching for its other translations:

Text(“Hello, how are you?”.i18n)

Managing keys

Other translation packages ask you to define key identifiers for each translation, and use those. For example, the above text key could be or simply . And then you could access it like this: .

However, having to define identifiers is not only a boring task, but it also forces you to navigate to the translation if you need to remember the exact text of the widget.

With you can simply type the text you want and that’s it.
If some string is already translated and you later change it in the widget file,
this will break the link between the key and the translation map. However, the package is smart enough to let you know when that happens, so it’s easy to fix. You can add this check to tests, as to make sure all translations are linked and complete. Or you can throw an error, or log the problem, or send an email to the person responsible for the translations.

Defining translations by language

As explained, by using the constructor you define each key and then provide all its translations at the same time. This is the easiest way when you are doing translations manually, for example, when you speak English and Spanish and are creating yourself the translations to your app.

However, in other situations, such as when you are hiring professional translation services or crowdsourcing translations, it may be easier if you can provide the translations by locale/language, instead of by key. You can do that by using the constructor.

static var t = Translations.byLocale("en_us") +
{
"en_us": {
"Hi": "Hi",
"Goodbye": "Goodbye",
},
"es_es": {
"Hi": "Hola",
"Goodbye": "Adiós",
}
};

Translation modifiers

Sometimes you have different translations that depend on a number quantity.
Instead of you can use and pass a numeric modifier. For example:

int numOfItems = 3;
return Text("You clicked the button %d times".plural(numOfItems));

This will be translated, and if the translated string contains it will be replaced by the number.

Your translations file can contain something like this:

static var t = Translations("en_us") +
{
"en_us": "You clicked the button %d times"
.zero("You haven’t clicked the button")
.one("You clicked it once")
.two("You clicked a couple times")
.many("You clicked %d times")
.times(12, "You clicked a dozen times"),
"pt_br": "Você clicou o botão %d vezes"
.zero("Você não clicou no botão")
.one("Você clicou uma única vez")
.two("Você clicou um par de vezes")
.many("Você clicou %d vezes")
.times(12, "Você clicou uma dúzia de vezes"),
};

Custom modifiers

You can actually create any modifiers you want. For example, some languages have different translations for different genders. So you can create that accepts modifiers:

enum Gender {they, female, male}int gnd = Gender.female;
return Text("There is a person".gender(gnd));

Then, your translations file should use and like this:

static var t = Translations("en_us") +
{
"en_us": "There is a person"
.modifier(Gender.male, "There is a man")
.modifier(Gender.female, "There is a woman")
.modifier(Gender.they, "There is a person"),
"pt_br": "Há uma pessoa"
.modifier(Gender.male, "Há um homem")
.modifier(Gender.female, "Há uma mulher")
.modifier(Gender.they, "Há uma pessoa"),
};
String gender(Gender gnd) => localizeVersion(gnd, this, t);

Importing and exporting

This package is optimized so that you can easily create and manage all of your translations yourself, by hand, in small projects. But for larger projects, when you hire professional translation services or want to implement crowdsourcing translations, you will need to import/export to external formats like , , , , , , or files.

This is easy to do, because the Translation constructors use maps as input. So you can simply generate maps from any file format, and then use the or constructors to create the translation objects.

This was just a quick look into i18n_extension. Head to the package documentation for more details, a FAQ section, and the other many features not mentioned here.

Este artigo tem uma versão em Português do Brasil

Flutter Community

Articles and Stories from the Flutter Community