Localize your Flutter packages

With a summary of localization in Flutter

Flutter's basic blocks are reusable widgets. You create a widget and you can use it everywhere in your application.

But what happens when you have multiple applications and you want to use the same widgets? You can create a Flutter package, which can be shared amongst your project and with everyone else by publishing it on pub.dev.

When you develop your app to support multiple locales, your Flutter package should support that as well. Let's see how it works with the built-in solutions using the flutter_localizations and intl packages.

Basically, you can follow the great tutorial on flutter.dev about the localization of an application. We need to do the same with minor differences.

Prepare the package

First, let's create our Flutter package with

flutter create --template=package my_awesome_widgets

Let's add the required dependencies to pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.17.0

With the help of flutter_localizations and intl packages, Flutter can generate the localization classes based on your strings and translations. But we need to enable the code generation by extending the Flutter specific part in the pubspec.yaml file:

flutter:
  generate: true

Configure the code generation

The next step is to create the configuration file for the localization. It's name is l10n.yaml and it should be at the root of your Flutter project.

# This is the home of your localization files.
arb-dir: lib/l10n
# The name of the file where you define your translatable strings
template-arb-file: my_awesome_widgets_en.arb
# Your generated code will be placed to this directory
output-dir: lib/l10n/generated
# The name of the generated class 
output-class: MyAwesomeWidgetsLocalizations
# The file name for the generated code
output-localization-file: my_awesome_widgets_localizations.dart
# This tells Flutter where it should generate the code.
synthetic-package: false
# This tells the code generator to create nullable getters for our strings or not.
nullable-getter: false

The option that we will need for our package is the synthetic-package, which controls whether the generated code should go under .dart_tool or to the output directory defined in output-dir. Since we want to share our package, we will need to export it, so it should be in a place that can be added as an export statement.

Translatable strings

Let's define our translatable strings. Based on the configuration above, the base translation template is lib/l10n/my_awesome_widgets_en.arb. It is a JSON like file using the ICU format

{
  "appTitle": "My Awesome Widgets",
  "@appTitle": {
    "description": "This is the title of the application"
  }
}

All translatable string has key and optional meta information, which helps the translator and provide additional input for the code generator. We have lots of options like parameters, pluralization.

Using a placeholder

{
  "welcome": "Welcome {name}",
  "@welcome": {
    "description": "The welcome messge on the home page",
    "placeholders": {
      "name": {
        "type": "String"
      }
    }
  }
}

Pluralization

{
  "friendRequests": "{count, plural, =0{You have no new friend request} =1{You have one reuquest} other{You have {count} requests}}",
  "@friendRequests": {
    "description": "The number of pending friends requests",
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  }
}

Selection

You can define different strings based on the value of the input parameter. This could be useful for example for texts that are different for the genders in your language.

{
  "sendMessage": "{sex, select, male{Send him a message} female{Send her a message} other{Send a message}}",
  "@sendMessage": {
      "description": "Message on the send button",
      "placeholders": {
          "sex": {
            "type": "String"
          }
      }
  }
}

But the select is not restricted to only gender-based strings, you can use it for any input that requires a different string representation based on its content.

Number and date formatting

You can specify the format for numbers or dates in the arb file, so it will use the locale-specific version when it is displayed.

{
  "discountPrice": "Discount: {price}",
  "@discountPrice": {
    "placeholders": {
      "price": {
        "type": "double",
        "format": "currency"
      }
    }
  },
  "shippingDate": "Estimated shipping at {date}",
  "@shippingDate": {
    "placeholders": {
      "date": {
        "type": "DateTime",
        "format": "yMMMd"
      }
    }
  }
}

Usage

So far we have the translation strings, we can generate the classes by running:

flutter gen-l10n

Which will output the following

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.

This will generate the MyAwesomeWidgetsLocalizations class under lib/l10n/generated. Let's export the class by adding the following line to lib/my_awesome_widgets.dart

export 'l10n/generated/my_awesome_widgets_localizations.dart';

Now we can use it in one of our cool widget

import 'package:flutter/material.dart';
import 'package:my_awesome_widgets/my_awesome_widgets.dart';

class CoolTexts extends StatelessWidget {
  const CoolTexts({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(MyAwesomeWidgetsLocalizations.of(context).appTitle),
        Text(MyAwesomeWidgetsLocalizations.of(context).welcome('John')),
        Text(MyAwesomeWidgetsLocalizations.of(context).friendRequests(0)),
        Text(MyAwesomeWidgetsLocalizations.of(context).friendRequests(1)),
        Text(MyAwesomeWidgetsLocalizations.of(context).friendRequests(2)),
        Text(MyAwesomeWidgetsLocalizations.of(context).sendMessage('male')),
        Text(MyAwesomeWidgetsLocalizations.of(context).sendMessage('female')),
        Text(
          MyAwesomeWidgetsLocalizations.of(context).sendMessage('unspecfied'),
        ),
        Text(
          MyAwesomeWidgetsLocalizations.of(context).discountPrice(20),
        ),
        Text(
          MyAwesomeWidgetsLocalizations.of(context)
              .shippingDate(DateTime.now()),
        ),
      ],
    );
  }
}

Add another locale

If you want to add another locale to your package, you have to create a new arb file next to the my_awesome_widgets_en.arb. For example a Hungarian translation will look like

{
  "appTitle": "My Awesome Widgets",
  "welcome": "Üdvözöllek {name}",
  "friendRequests": "{count, plural, =0{Nincs új követési kérésed} =1{Egy új követési kérésed van} other{{count} követési kérésed van}}",
  "sendMessage": "{sex, select, male{Küldj neki egy üzenetet} female{Küldj neki egy üzenetet} other{Küldj neki egy üzenetet}}",
  "discountPrice": "Akciós ár: {price}",
  "shippingDate": "Tervezett szállítási dátum {date}"
}

It is fine to have only the strings in your other locales, you don't have to repeat the meta-information.

Running flutter gen-l10n will create a class for the Hungarian translation next to the English version.

Use the package in a Flutter application

Once we have our package with the shareable widgets, we can use it as a dependency in our Flutter applications. The only thing we have to do is to add the localization delegate to our application, so the localization SDK will know how to create the `` for each locales.

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:my_awesome_widgets/my_awesome_widgets.dart';

class MyAwesomeApp extends StatelessWidget {
  const MyAwesomeApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: const [
        // Add the localization delegate of your package
        MyAwesomeWidgetsLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: const [
        Locale('en'),
        Locale('hu'),
      ],
      home: Scaffold(
        appBar: AppBar(),
        body: const Center(
          child: CoolTexts(),
        ),
      ),
    );
  }
}
EnglishHungarian
Simulator Screen Shot - iPhone 12 Pro - 2022-01-21 at 10.50.20.pngSimulator Screen Shot - iPhone 12 Pro - 2022-01-21 at 10.54.12.png

Testing the package

We want to share a reliable package that is not sensitive to changes we make by adding new features. So we add tests to our package. In order to test the widgets that use locales, you have to provide the same environment for them as they would be used in a real app. This means you have to provide a MaterialApp and the delegates as well.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:my_awesome_widgets/my_awesome_widgets.dart';

void main() {
  group('CoolTexts', () {
    testWidgets('displays the texts', (tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          // Add the delegates defined by our package
          localizationsDelegates:
              MyAwesomeWidgetsLocalizations.localizationsDelegates,
          home: Scaffold(
            // Add our widget that we want to test
            body: CoolTexts(),
          ),
        ),
      );
      // Check it works correctly
      expect(find.text('Welcome John'), findsOneWidget);
    });
  });
}

Summary

This article became longer than I expected. My goal was to show how to add locales to your Flutter package and how can you use it in your applications. Feel free to leave your thoughts in the comment section.

Resources