i18n? Crazy easy Translation and Internationalization for Flutter/Dart

i18n_extension package

Marcelo Glasberg
Published in
7 min readOct 24, 2019

--

I like creating packages to reduce boilerplate. For example, async_redux is Redux simplified, and align_positioned is layout with less widgets.

In this article I talk about the i18n_extension package which is about easy translations. This package was mentioned by Google during a Dart announcement.

Start with a widget containing some text:

Text('How are you?')

Translate it simply by adding .i18n to the string:

Text('How are you?'.i18n)

If the current locale is pt then the text in the screen will be
Olá como vai você which is the Portuguese translation to the above text. And so on for any other locales you want to support.

As shown above, the original English text is itself the “translation key” that’s used to look up the translation. But you can actually use objects of any type as translation keys. By adding .i18n they will turn into translated strings in the current locale:

// Const values  
const greetings = UniqueKey();
greetings.i18n // Turns into "How are you?" in en, "Como vai?" in pt

// Final variables
final faq = 'faq';
faq.i18n // "FAQ" in en, "Perguntas frequentes" in pt

// Enums
enum MyColors { red, blue }
MyColors.red.i18n // "Red" in en, "Vermelho" in pt
MyColors.blue.i18n // "Blue" in en, "Azul" in pt

// Numbers, booleans, Dates
12.i18n // "Twelve" in en, "Doze" in pt
true.i18n // "Yes" in en, "Sim" in pt
false.i18n // "No" in en, "Não"
DateTime(2021, 1, 1).i18n // "New Year" in en, "Ano Novo" in pt

// Your own object types
class User { ... }
User('John').i18n // "Mr. John" in en, "Sr. John" in pt

You can also provide different translations depending on modifiers, for example plural() 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 gender() versions for Gender 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 I18n widget, below the MaterialApp:

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 {

static var t = Translations.byText("en_us") +
{
"en_us": "How are you?",
"pt_br": "Como vai você?",
"es": "¿Cómo estás?",
"fr": "Comment ca va?",
"de": "Wie geht es dir?",
};

String get i18n => localize(this, t);
}

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 {

static var t = Translations.byText('en_us') +
{
'en_us': 'How are you?',
'pt_br': 'Como vai você?',
} +
{
'en_us': 'Hi',
'pt_br': 'Olá',
} +
{
'en_us': 'Goodbye',
'pt_br': 'Adeus',
};

String get i18n => localize(this, t);
}

Strings themselves are the translation keys

The locale you pass in the Translations.byText('en_us') 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 Text below, 'How are you?' is both the translation to English and the key to use when searching for its other translations:

Text('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 howAreYou or simply greetings. And then you could access it like this: MyLocalizations.of(context).greetings.

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

Using identifiers as translation keys

If you don’t like using Strings as translation keys, you can use any other object type, with the Translations.byId<T> factory. For example, here I’m defining appbarTitle and greetings as identifier keys:

final appbarTitle = UniqueKey();
final greetings = UniqueKey();

extension Localization on UniqueKey {

static final _t = Translations.byId<UniqueKey>('en_us', {
appbarTitle: {
'en_us': 'i18n Demo',
'pt_br': 'Demonstração i18n',
},
greetings: {
'en_us': 'Helo there',
'pt_br': 'Olá como vai',
},
});

String get i18n => localize(this, _t);
}

You can actually mix different key types as you see fit. As a personal preference, I usually like to use String for short texts, and UniqueKey for large texts like a Privacy Policy:

final privacyPolicy = UniqueKey();
final termsOfUse = UniqueKey();

extension Localization on Object {

static final _t = Translations.byId<Object>('en_us', {
privacyPolicy: { 'en_us': 'Very Looong text', 'pt_br': 'Very Looong text' },
termsOfUse: { 'en_us': 'Very Looong text', 'pt_br': 'Very Looong text' },
'My Settings': { 'en_us': 'My Settings', 'pt_br': 'Meus ajustes' },
'Ok': { 'en_us': 'Ok', 'pt_br': 'Salvar ajustes' },
'Back': { 'en_us': 'Back', 'pt_br': 'Voltar' },
});

String get i18n => localize(this, _t);
}

And use them like this:

Text(privacyPolicy.i18n);
Text(termsOfUse.i18n);
Text('My Settings'.i18n);
Text('Ok'.i18n);
Text('Back'.i18n);

Defining translations by language

As explained, by using the Translations.byText() 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 Translations.byLocale() 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 .i18n you can use .plural() 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 %d 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 .gender() that accepts Gender modifiers:

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

Then, your translations file should use .modifier() and localizeVersion() 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 .arb, .po, .icu, .json, .yaml, .csv, or .xliff 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 Translation.byText() , Translation.byId() or Translation.byLocale() 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

--

--