Internationalization is obviously a big deal. We want our apps to be understandable and accessible for everyone, not only a single group of people. How exactly is it done? Let’s go over the details on why you need to introduce an additional technology to your project and then see how to localize your app in practice.
Flutter internationalization: Rationale
If you were to decide that you don’t need anything fancy and prefer to implement everything by yourself you’d probably do something like following:
class AppLocalizations {
AppLocalizations(this.strings);
final Map strings;
static final en = {
'counterTitle': 'You have pushed the button this many times:',
};
String get counterTitle => strings["counterTitle"]!;
}
As for actually using this, here’s an example:
AppLocalizations strings() {
// you'd probably want to cache chosen translations
// somewhere, omitted for brevity
switch (window.locale.languageCode) {
case "en":
default:
return AppLocalizations(AppLocalizations.en);
}
}
class CounterHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
strings().counterTitle,
);
}
}
While it may be fine when you’ve just started the project, I’m pretty sure you’ll encounter problems soon, for example translating strings that contain parameters or information about gender/plurality. Implementing necessary logic to handle these cases will surely annoy the heck out of you when it happens more than a couple of times.
enum Gender { male, female, other }
class AppLocalizations {
AppLocalizations(this.strings);
final Map strings;
static final en = {
'counterTitleMale': 'He has pushed the button this many times:',
'counterTitleFemale': 'She has pushed the button this many times:',
'counterTitleOther': 'They have pushed the button this many times:',
};
String counterTitle(Gender gender) {
switch (gender) {
case Gender.male:
return strings['counterTitleMale']!;
case Gender.female:
return strings['counterTitleFemale']!;
case Gender.other:
return strings['counterTitleOther']!;
}
}
}
Now that doesn’t look to good. The level of complexity increases exponentially each time you add a parameter to the string. Such solution is very likely to have bugs as you have to manually handle each case.
Flutter internationalization: Project setup
Single module
If you only have a single module in your project, then you probably don’t need to go beyond what’s in official internationalization docs
Warning: this will only work if you run the app from the same directory as your l10n.yaml
config (in other words, if you don’t have independent modules with separate localization setups). For a solution, read the section below.
It’s because when you execute flutter run
it looks for l10n.yaml
only in the current directory.
Multiple modules
Flutter doesn’t really support this requirement, so a workaround will be necessary. Basically you need to set generate target to the folder with your sources and trigger the build manually. You’ll have to commit generated sources, unless in your CI before flutter build
you trigger the generation as well. The rest (enabling new translations and using them) stays the same.
Let’s say you have the following project structure:
├── apps
│ └── some_fun_application
│ │── lib
│ └── pubspec.yaml
└── modules
├── feature_one
│ │── lib
│ │── l10n.yaml
│ └── pubspec.yaml
├── feature_two
│ │── lib
│ │── l10n.yaml
│ └── pubspec.yaml
└── core
│── lib
└── pubspec.yaml
Firstly, make sure all your feature modules contain the following (you don’t need generate
set to true in the rest of the configs):
modules/feature_*/pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.17.0 # Check the latest version on https://pub.dev/packages/intl
flutter:
generate: true
As for localization configs, tweak the output directory and file/class:
l10n.yaml
arb-dir: lib/l10n
template-arb-file: module_en.arb
output-dir: lib/src
synthetic-package: false
output-localization-file: app_localizations.g.dart
output-class: AppLocalizations
Now, after adding .arb
files just as in a single module case, run the following command (inside directories with localization configs), and you should see the following output:
$ flutter gen-l10n
Because l10n.yaml exists, the options defined there will be used instead.
To use the command line arguments, delete the l10n.yaml file in the Flutter project.
Running this command manually in every feature module each time you clone the project is unreasonable. I strongly advise using the following bash script or an equivalent:
generate_localizations.sh
generate_localizations() {
for DIR in $(ls $1); do
path=$1/$DIR
(
cd "$path" || exit
cat l10n.yaml &>/dev/null && echo "Found localizations config in $path" && flutter gen-l10n
)
done
}
generate_localizations modules
Flutter internationalization: .arb files
ARB stands for Application Resource Bundle
and you can think of it as a buffed JSON. It’s not Flutter/Dart specific, nonetheless it has fantastic support for Flutter environment that allows for seamless integration with any project.
{
"formLabelDone": "Done",
"formLabelMandatory": "Mandatory"
}
Description
Nothing unusual so far, it looks just like any other JSON. However, it starts to shine when you want a third party to contribute a translation as it allows for adding metadata to each and every field. This provides extra context for the translator which directly turns into superb translation quality.
{
"formLabelDone": "Done",
"@formLabelDone": {
"description": "Label being displayed below every form field that is filled and valid"
}
}
The description is also added as a documentation in generated code:
abstract class AppLocalizations {
/// Label being displayed below every form field that is filled and valid
///
/// In en, this message translates to:
/// **'Done'**
String get formLabelDone;
}
Parameters
You can also add Parameters along with type information:
{
"contactDetailsPopupEmailCopiedMessage": "Copied {email} to clipboard",
"@contactDetailsPopupEmailCopiedMessage": {
"description": "Message being displayed in a snackbar upon long-clicking email in contact details popup",
"placeholders": {
"email": {
"type": "String",
"example": "[email protected]"
}
}
}
}
abstract class AppLocalizations {
/// Message being displayed in a snackbar upon long-clicking email in contact details popup
///
/// In en, this message translates to:
/// **'Copied {email} to clipboard'**
String contactDetailsPopupEmailCopiedMessage(String email);
}
Numbers, currencies
These are being parsed with the help of intl package. For a complete list of possible formats and their parameters visit intl’s NumberFormat documentation.
{
"productCostInfo": "Cost: {cost}",
"@productCostInfo": {
"placeholders": {
"cost": {
"type": "double",
"format": "currency",
"optionalParameters": {
"symbol": "€",
"decimalDigits": 3
}
}
}
}
}
Here’s the generated code for the default locale:
@override
String productCostInfo(double cost) {
final intl.NumberFormat costNumberFormat = intl.NumberFormat.currency(
locale: localeName,
symbol: "€",
decimalDigits: 3
);
final String costString = costNumberFormat.format(cost);
return 'Cost: ${costString}';
}
Dates
They work pretty much the same as numbers and currencies with a single exception: the class that’s being used to format the input. In this case, NumberFormat
is being utilized.
{
"postCreatedInfo": "Created: {date}",
"@postCreatedInfo": {
"placeholders": {
"date": {
"type": "DateTime",
"format": "MMMd"
}
}
}
}
@override
String postCreatedInfo(DateTime date) {
final intl.DateFormat dateDateFormat = intl.DateFormat.MMMd(localeName);
final String dateString = dateDateFormat.format(date);
return 'Created: ${dateString}';
}
There’s one catch though. You can’t specify any format you want due to the way it’s implemented right now. The only ones you can use are named constructors in DateFormat class.
Plurals
Just as before, there’s a piece of code responsible for parsing the input of localization. This time it’s a static function, and it’s called plural. This case has its own limitation – anything outside curly braces is being ignored. This means you can’t do something like Please contact {count,plural, =1{an organiser} other{organisers}}
. You have to add two separate entries for that.
{
"roomUnavailableContactOrganiserDialogCount": "{count,plural, =1{an organiser} other{organisers}}",
"@roomUnavailableContactOrganiserDialogCount": {
"placeholders": {
"count": {}
}
}
}
@override
String roomUnavailableContactOrganiserDialogCount(int count) {
return intl.Intl.pluralLogic(
count,
locale: localeName,
one: 'an organiser',
other: 'organisers',
);
}
Summary
Examples above should handle most of your requirements. As you can see, localization with .arb is intuitive and satisfying. If you want to go more in-depth, there’s an amazing document on Google Docs. For exploring all the features of ARB format that aren’t necessarily related with Flutter, visit ARB specification.