2

How to Internationalize Your Flutter App | The Phrase Blog

 2 years ago
source link: https://phrase.com/blog/posts/how-to-internationalize-a-flutter-app/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
How to Internationalize Your Flutter App

Here’s a simplified diagram of our app’s widget tree:

App
└── ContactsProvider
    └── MaterialApp
        └── {Routes}
            ├── HomeScreen
            │   └── Scaffold
            │       ├── AppBar
            │       ├── ListView
            │       │   └── ContactListItem
            │       └── FloatingActionButton
            └── AddContactScreen
                └── Scaffold
                    ├── AppBar
                    └── Form
                        ├── WordFormField
                        └── DatePickerField

You can grab the code for the starter app at its GitHub repo here.

Once you clone the repo, and assuming you have Flutter installed, a quick flutter packages get on the command line from the project root directory should have you all set. You should be able to run the app on an Android simulator or iOS emulator after that. Let’s go over the business logic of our starter app.

Our main model is a simple Contact list.

lib/src/models/contact.dart

class Contact {
  final String name;
  final DateTime contactNextAt;

  Contact({this.name, this.contactNextAt});
}

A Contact represents each row in our main list. The contactNextAt is supposedly when we would like to get a reminder about contacting a person. For brevity, we don’t have a reminder functionality in the current version of our app.

We export a List<Contact> to our app’s widgets through an InheritedWidget.

lib/src/shared_state/contacts_provider.dart

import 'package:flutter/material.dart';

import '../models/contact.dart';

class ContactsProvider extends InheritedWidget {
  final List<Contact> contacts = [
    Contact(name: 'Adam Doe', contactNextAt: DateTime(2018, 10, 1)),
    Contact(name: 'Sally Doe', contactNextAt: DateTime(2018, 10, 12)),
  ];

  ContactsProvider({Key key, Widget child}) : super(key: key, child: child);

  static List<Contact> of(BuildContext context) {
    final provider = (context.inheritFromWidgetOfExactType(ContactsProvider)
        as ContactsProvider);

    return provider.contacts;
  }

  @override
  bool updateShouldNotify(ContactsProvider old) =>
      old.contacts != this.contacts;
}

Our InheritedWidget subclass, ContactsProvider, exposes our list of contacts to the concerned widgets. Let’s see how this is wired up in our app before we look a bit more closely at InheritedWidgets.

lib/src/app.dart

import 'package:flutter/material.dart';

import 'screens/add_contact_screen.dart';
import 'screens/home_screen.dart';
import 'shared_state/contacts_provider.dart';

class App extends StatelessWidget {
  static const String title = 'Stay in Touch (i18n Demo)';

  @override
  Widget build(BuildContext context) {
    return ContactsProvider(
      child: MaterialApp(
        title: title,
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        onGenerateRoute: _getCurrentRoute,
      ),
    );
  }

  MaterialPageRoute _getCurrentRoute(RouteSettings settings) {
    switch (settings.name) {
      case '/add':
        return MaterialPageRoute(
          builder: (context) =>
              AddContactScreen(contacts: ContactsProvider.of(context)),
        );

      default:
        return MaterialPageRoute(
          builder: (context) => HomeScreen(
                title: title,
                contacts: ContactsProvider.of(context),
              ),
        );
    }
  }
}

Since our app is small and we want our List<Contact> to be available to its both screens, we wrap our entire MaterialApp with our ContactsProvider. But what exactly is this doing and what is an InheritedWidget? Our internationalization code will make use of the inherited pattern, so let’s dive into that a bit.

Shared State & InheritedWidget

If you’ve ever used a reactive, component-based framework like Flutter or React, you know that, inevitably, you have pieces of state that you want to share across different components or widgets. This can mean passing these bits of state down widget sub-trees in your app, which can quickly become a maintenance headache. If you want to change the shape or interface of your shared state, you may have to update several pieces of your app. Another problem is that you may need a bit of shared state several levels down a widget sub-tree. To do so, you may have to pass that state down each widget in the sub-tree until you get to the one concerned with the respective state. These in-between widgets shouldn’t need to know about the shared state, since they do nothing with it. This can create confusing widget APIs, where a widget’s parameters don’t directly reflect its needs.

To deal with this, Flutter provides InheritedWidgets. An InheritedWidget carries a state that can be shared with a specific widget sub-tree in your app, and Flutter is designed so, that a widget can ask for an InheritedWidget that was provided to its sub-tree. Flutter will use the InheritedWidget.updateShouldNotify method to inform a concerned widget whether it should rebuild as a reaction to a change in the information held by the InheritedWidget.

In our app’s case, we return true from updateShouldNotify only when our contact list changes. We also provide our shared state to our entire app, since our app is quite small and only has the contact list to share. However, to keep a bigger app efficient, we would want to be more granular when we share our InheritedWidgets.

Flutter makes use of the inherited pattern in its i18n, and we’ll see that in action, when we begin internationalizing our app, which incidentally is what we’ll be doing next.

Flutter i18n: Internationalizing the App

Installing our i18n Packages

The first step to internationalizing our app is to add three packages.

  • flutter_localizations is included with Flutter and contains several localizations for Flutter’s own widgets (a full list of the available localizations can be found in the Flutter documentation).
  • intl, an official Dart package, provides many of the i18n and l10n capabilities we need. It supports working with translation messages among other i18n-related things.
  • intl_translation, another package provided by the Dart team, provides command-line tools for generating code and translation files which we’ll use to localize our app.

We can add these three files to our pubspec.yaml file.

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter

  # ...

  intl: ^0.15.7
  intl_translation: ^0.17.0

  # ...

Running flutter packages get should install the packages and have us good to go.

Updating Info.plist for iOS

We’ll be working mostly right in Dart and Flutter here. However, iOS won’t see our supported locales unless we explicitly set them in our Info.plist file.

ios/Runner/Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>en</string>

	<key>CFBundleLocalizations</key>
	<array>
		<string>en</string>
		<string>ar</string>
	</array>

	<!-- ... -->

</dict>
</plist>

Since we’re supporting English and Arabic, we set their ISO 639-1 codes in the file.

Creating the Localizations and LocalizationsDelegate Classes

Next, let’s create a localizations class that we can provide to our app. Later, this class will include our internationalized messages.

lib/src/lang/sit_localizations.dart

import 'dart:async';

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

// We have to build this file before we uncomment the next import line,
// and we'll get to that shortly
// import '../../l10n/messages_all.dart';

class SitLocalizations {
  /// Initialize localization systems and messages
  static Future<SitLocalizations> load(Locale locale) async {
    // If we're given "en_US", we'll use it as-is. If we're
    // given "en", we extract it and use it.
    final String localeName =
        locale.countryCode == null || locale.countryCode.isEmpty
            ? locale.languageCode
            : locale.toString();

    // We make sure the locale name is in the right format e.g.
    // converting "en-US" to "en_US".
    final String canonicalLocaleName = Intl.canonicalizedLocale(localeName);

    // Load localized messages for the current locale.
    // await initializeMessages(canonicalLocaleName);
    // We'll uncomment the above line after we've built our messages file

    // Force the locale in Intl.
    Intl.defaultLocale = canonicalLocaleName;

    return SitLocalizations();
  }

  /// Retrieve localization resources for the widget tree
  /// corresponding to the given `context`
  static SitLocalizations of(BuildContext context) =>
      Localizations.of<SitLocalizations>(context, SitLocalizations);
}

Our class is called SitLocalizations to differentiate it from Flutter’s own Localizations class. “Sit” is just a namespace and it stands for “Stay in Touch”, which is our demo app’s name.

The SitLocalizations.of method takes a BuildContext and returns an instance of SitLocalizations, much like an InheritedWidget would. We’ll use this method in our widgets to retrieve our translated messages.

Note » We need to comment out our import of the messages_all.dart file and its initializeMessages() function call until we generate the file through intl_translations. We’ll do that a bit later and come back to un-comment these lines.

This class is pretty useless without a LocalizationDelegate, which we’ll pass to our app. Let’s take a look at the delegate before we wire it up to our app.

lib/src/lang/sit_localizations_delegate.dart

import 'dart:async';

import 'package:flutter/material.dart';

import 'sit_localizations.dart';

class SitLocalizationsDelegate extends LocalizationsDelegate<SitLocalizations> {
  const SitLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) => ['ar', 'en'].contains(locale.languageCode);

  @override
  Future<SitLocalizations> load(Locale locale) => SitLocalizations.load(locale);

  @override
  bool shouldReload(LocalizationsDelegate<SitLocalizations> old) => false;
}

Our SitLocalizationsDelegate class derives from Flutter’s LocalizationDelegate, and its job is to provide basic localization functions to our app. The isSupported method returns true if a given locale is supported by our app. load will get the current locale’s messages ready for usage. Our delegate’s load method itself delegates to our SitLocalizations.load method, which extracts the given locale’s ISO code, loads the locale’s translated messages and forces the Intl package to use the locale. shouldReload is similar to InheritedWidget.updateShouldNotify, and should return true when our localizations change. Since our app’s localizations won’t change after they’ve initially loaded, we always return false from shouldReload.

Wiring Our LocalizationDelegate Up to Our App

Okay, with that in place, let’s go to our app.dart file and connect our SitLocalizationDelegate to our app. We’ll also bring in Flutter’s built-in localizations and tell our app which locales we support while we’re at it.

lib/src/app.dart

// ...

import 'package:flutter_localizations/flutter_localizations.dart';

import 'lang/sit_localizations.dart';
import 'lang/sit_localizations_delegate.dart';

// ...

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ContactsProvider(
      child: MaterialApp(

        // ...

        localizationsDelegates: [
          const SitLocalizationsDelegate(),
          GlobalMaterialLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate,
        ],
        supportedLocales: [
          // The order of this list matters. By default, if the
          // device's locale doesn't exactly match a locale in
          // supportedLocales then the first locale in
          // supportedLocales with a matching
          // Locale.languageCode is used. If that fails then the
          // first locale in supportedLocales is used.
          const Locale('en'),
          const Locale('ar'),
        ],

        // ...

Note » The supportedLocales List will be Flutter’s source of truth for supported locales. So if a user switches his or her device’s language, our app will only follow suit if that language is in our ‘supportedLocales’ list.

The great thing about using Flutter is that it has a lot of built-in niceties. One of them is first-class support for i18n and l10n. The provided MaterialApp widget’s constructor takes two params relevant to our solution: localizationDelegates and supportedLocales. These are where we register our delegates and our app’s supported locales, respectively. Notice that we added GlobalMaterialLocalizations.delegate and GlobalWidgetsLocalizations.delegate to our localizationDelegates. These provide Flutter’s built-in localizations for its own widgets, and we’ll see that in action a bit later. Before those, we provide an instance of our own SitLocalizationsDelegate, and that wires us all up to use our translations. Speaking of which…

The Flutter l10n Workflow

We’ve wired up our Flutter localization delegate, which will provide our SitLocalizations to our app’s widgets. However, we haven’t really used SitLocalizations to provide any localizations. Let’s do that. Our app’s title is a good place to start. Let’s add it as a localized message.

lib/src/lang/sit_localizations.dart

class SitLocalizations {

  // ...

  // Localized Messages
  String get title => Intl.message(
        'Stay in Touch (i18n Demo)',
        name: 'title',
        desc: 'App title',
  )
}

We can add the message at the bottom of our SitLocalizations class. We use the intl package’s Intl.message method to specify the default string, name, and description of the message. The latter is for the benefit of translators, but the message’s name is required by default by intl.

Generating the Messages ARB file

The intl_translation package provides a command line tool to create an ARB (application resource bundle) file. We can run it from our command line, in our project’s root directory, to generate the file.

flutter pub pub run intl_translation:extract_to_arb --output-dir=lib/l10n lib/src/lang/sit_localizations.dart

Note » The command won’t create the lib/l10n directory by itself, and will squawk if it doesn’t find the directory. So make sure to create the directory manually before running the command.

This will generate a lib/l10n/intl_messages.arb file for us. If you open the file, you should see something like the following:

{
  "@@last_modified": "2018-09-26T19:32:39.619278",
  "title": "Stay in Touch (i18n Demo)",
  "@title": {
    "description": "App title",
    "type": "text",
    "placeholders": {}
  }
}

Let’s make a copy of this file for each locale we support. Our app supports English and Arabic, so we can create two copies and name them intl_messages_ar.arb and intl_messages_en.arb. Both files can live in the lib/l10n directory, just like the original ARB file. Now let’s open the English intl_message_en.arb file and add its locale key. When we’re done, it should look like the following:

lib/l10n/intl_messages_en.arb

{
  "@@locale": "en",
  "@@last_modified": "2018-09-26T19:32:39.619278",
  "title": "Stay in Touch (i18n Demo)",
  "@title": {
    "description": "App title",
    "type": "text",
    "placeholders": {}
  }
}

Similarly, let’s open the Arabic version and edit it, specifying the locale key and adding our translated version of the string.

lib/l10n/intl_messages_ar.arb

{
  "@@locale": "ar",
  "@@last_modified": "2018-09-26T19:32:39.619278",
  "title": "إبقى على تواصل - عرض ترجمة",
  "@title": {
    "description": "App title",
    "type": "text",
    "placeholders": {}
  }
}

Generating the Dart Message Files from the ARB Files

We need a second step to have our messages ready to use by our app. The intl_translation package provides a command that generates Dart code files from our ARB files. We can run it like this:

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

This will generate four additional files in our lib/l10n directory:

  • messages_all.dart
  • messages_ar.dart
  • messages_en.dart
  • messages_messages.dart

We don’t really need to be too concerned with what these files do, since intl and intl_packages are responsible for them, given that we’ve created our ARB files correctly. For the curious ones, however, suffice it to say that these files help to load our localized messages and provide them as a Dart code to our app.

Wiring Up Our Messages Dart File to our Localizations Class

We can now return our SitLocalizations class and un-comment the lines we had commented out before.

lib/src/lang/sit_localizations.dart

// ...

import '../../l10n/messages_all.dart';

class SitLocalizations {
  /// Initialize localization systems and messages
  static Future<SitLocalizations> load(Locale locale) async { 

    // ...

    // Load localized messages for the current locale.
    await initializeMessages(canonicalLocaleName);

// ...

Okay, we’re ready to bring in our first translation.

Accessing our Flutter Localization Data

Let’s return to our main App widget and swap out our hard-coded title for the localized one.

lib/src/app.dart

// ...

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ContactsProvider(
      child: MaterialApp(
       
        onGenerateTitle: (context) => SitLocalizations.of(context).title,
       
        // ...

        onGenerateRoute: _getCurrentRoute,
      ),
    );
  }

  MaterialPageRoute _getCurrentRoute(RouteSettings settings) {
   
 // ...

        return MaterialPageRoute(
          builder: (context) => HomeScreen(
                title: SitLocalizations.of(context).title,
                contacts: ContactsProvider.of(context),
              ),
        );
    }
  }
}

Notice that we can’t use the title param in our MaterialApp‘s constructor anymore. That’s because we won’t have access to our SitLocalizations before the MaterialApp is constructed itself. To deal with that, MaterialApp provides the onGenerateTitle param, which accepts a function that is passed a BuildContext that we can use to access our SitLocalizations. We pull our title message out of the SitLocalizations, and do so again in our home route to pass the title to the HomeScreen.

Now if we go into our device’s settings and switch the language to Arabic, we should see our Arabic title in the home screen when we return to our app.

Starter app home screen Arabic localization | Phrase

Also, notice that the app’s direction is right-to-left in Arabic. That is so sweet! Flutter’s built-in support for Arabic and locale direction can save us a lot of time when internationalizing.

I18n Manager for Global Success
I18n Manager for Global Success

Learn how to find the best i18n manager and follow our best practices for making your business a global success.

Check out the guide

Handling Interpolation & Plurals

Our home screen’s app bar has a counter that displays the number of contacts in our list. Let’s internationalize this counter. We’ll have to pass the count in as a param to our message, and we’ll have to handle pluralization. Arabic can be a bit tricky with plurals since it has different plural forms for zero, one, two, three to ten, and eleven and up. Luckily, the intl package can handle all this. Let’s add a plural message to our SitLocalizations class.

bin/src/lang/sit_localizations.dart

class SitLocalizations {

  // ...

  String contactCount(int howMany) => Intl.plural(
        howMany,
        zero: 'No contacts',
        one: '$howMany contact',
        two: '$howMany contacts',
        few: '$howMany contacts',
        many: '$howMany contacts',
        other: '$howMany contacts',
        args: [howMany],
        name: 'contactCount',
        desc: 'Contact counter',
      );
}

The Intl.plural takes the count param, called howMany, as its first param. Note that when using interpolation with Intl messages, we have to provide the arguments we’re accepting as a list to the args param. Just like before, the name param is required for ARB file generation. You can skip the desc param if you want to, although I’m choosing to keep it here.

The pluralization params, zero, one, two, etc. are optional, with the exception of the other param. This gives us great flexibility when dealing with plurals. For example, if we were dealing only with languages that had three plural forms, like English, we could have built our message as follows:

  String contactCount(int howMany) => Intl.plural(
        howMany,
        zero: 'No contacts',
        one: '$howMany contact',
        other: '$howMany contacts',
        args: [howMany],
        name: 'contactCount',
        desc: 'Contact counter',
      );

Now let’s run the command to regenerate our ARB file.

flutter pub pub run intl_translation:extract_to_arb --output-dir=lib/l10n lib/src/lang/sit_localizations.dart

This will update our lib/l10n/intl_messages.arb file to look like this:

{
  "@@last_modified": "2018-09-27T16:51:21.790906",
  "title": "Stay in Touch (i18n Demo)",
  "@title": {
    "description": "App title",
    "type": "text",
    "placeholders": {}
  },
  "contactCount": "{howMany,plural, =0{No contacts}=1{{howMany} contact}=2{{howMany} contacts}few{{howMany} contacts}many{{howMany} contacts}other{{howMany} contacts}}",
  "@contactCount": {
    "description": "Contact counter",
    "type": "text",
    "placeholders": {
      "howMany": {}
    }
  }
}

We can copy our new contactCount key-value pairs to our intl_messages_en.arb and intl_messages_ar.arb files. Our Arabic version will need to be translated and can look like this when we’re done with it:

lib/l10n/intl_messages_ar.arb

{
  "@@locale": "ar",
  "@@last_modified": "2018-09-26T19:32:39.619278",
  "title": "إبقى على تواصل - عرض ترجمة",
  "@title": {
    "description": "App title",
    "type": "text",
    "placeholders": {}
  },
  "contactCount": "{howMany,plural, =0{لا توجد أطراف}=1{طرف {howMany}}=2{طرفان}few{{howMany} أطراف}many{{howMany} طرف}other{{howMany} طرف}}",
  "@contactCount": {
    "description": "Contact counter",
    "type": "text",
    "placeholders": {
      "howMany": {}
    }
  }
}

We can now re-run our command to generate the Dart code from our ARB files.

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

Let’s bring it all together by updating our HomeScreen to make use of our new message.

lib/src/screens/home_screen.dart

// ...

import '../lang/sit_localizations.dart';

// ...

class HomeScreen extends StatelessWidget {

  // ...

  @override
  Widget build(BuildContext context) {
    final sortedContacts = List<Contact>.from(contacts)
      ..sort((a, b) => a.contactNextAt.compareTo(b.contactNextAt));

    final l10n = SitLocalizations.of(context);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        centerTitle: false,
        elevation: 0.0,
        actions: <Widget>[
          Center(
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16.0),
              child: Text(l10n.contactCount(sortedContacts.length)),
            ),
          ),
        ],
      ),
      
// ...

After importing our sit_localizations.dart file, we can access our current localizations through the handy SitLocalizations.of method. This just returns an instance of SitLocalizations, so we can store it in a variable, l10n. We then use this object to access our new message, passing it the current number of contacts so that it can do its magic.

If we restart the app, we should now see this:

Starter app home screen Arabic translation interpolation | Phrase

Working With Dates

Our app is starting to look 🌍-friendly, but we still have that “Contact next YYYY/MM/DD” string in each contact row. Let’s get that string i18n-ized, shall we? You’ll know the drill by now. We start by adding a new message to our SitLocalizations class.

lib/src/lang/sit_localizations.dart

// ...

class SitLocalizations {
  
  // ...

  String contactNextAtDate(String date) => Intl.message(
        'Contact next at $date',
        name: 'contactNextAtDate',
        args: [date],
        desc: 'Indicator for when to make next contact with person',
      );
}

Just like before, we do our ARB generation, copy the message to each locale’s ARB file, and generate the Dart files. After that, we can incorporate the message in our ContactListItem widget.

lib/src/widgets/contact_list_item.dart

// ...

import '../lang/sit_localizations.dart';

// ...

class ContactListItem extends StatelessWidget {
  
  // ...

  String get _formattedDate {
    final date = contact.contactNextAt;

    return '${date.year}/${date.month}/${date.day}';
  }

  @override
  Widget build(BuildContext context) {
    final l10n = SitLocalizations.of(context);

    return Column(
      key: Key(contact.name),
      children: <Widget>[
        ListTile(
          title: Text(contact.name),
          subtitle: Text(l10n.contactNextAtDate(_formattedDate)),
        ),
        
// ...

After we make that swap, the Arabic version of our app should look like the following.

Starter app home screen with US date format in Arabic localization | Phrase

Notice that while the alphabetic part is translated now, we’re still seeing the English date form. We can correct this by using the intl package’s date formatting functions. First, we need to initialize date formatting in our locale loading method.

lib/src/lang/sit_localizations.dart

// ...

import 'package:intl/date_symbol_data_local.dart';

// ...

class SitLocalizations {
  /// Initialize localization systems and messages
  static Future<SitLocalizations> load(Locale locale) async {
    
    // ...

    // Date formatting is loaded on an as-needed basis. Since
    // we need it for our app, we load it here.
    await initializeDateFormatting(canonicalLocaleName);

    return SitLocalizations();
  }

// ...

date_symbol_data_local.dart is imported from the intl package, and its initialzeDateFormatting() function is called in our SitLocalizations.load method.

Note » At time of writing, date formatting seemed to work fine for me even if I didn’t call initialzeDateFormatting(). However, the official intl documentation states that the function should be called at least once before any of the package’s date formatting methods are called. I suppose it’s safe enough to make the call, so I’ve left it in here.

Now let’s update our ContactListItem to use intl’s date formatting.

lib/src/widgets/contact_list_item.dart

import 'package:intl/intl.dart';

// ...

class ContactListItem extends StatelessWidget {
  
  // ...

  String get _formattedDate =>
      DateFormat('d/M/y').format(contact.contactNextAt);

// ...

intl’s DateFormat class takes a format string and returns a formatter function. We can pass our Date object to this formatter function to get a formatted date in the current locale.

Et voilà!

Starter app with Arabic date format | Phrase

Note » intl’s documentation covers all the format characters that DateFormat() accepts.

Using Flutter’s Localized Widgets

When we add a new contact in our AddContactScreen, we use Flutter’s own showDatePicker function behind the scenes.

Starter app Flutter's showDatePicker | Phrase

We didn’t have to localize the date picker widget ourselves. The showDatePicker function is part of Flutter’s material library, and the Flutter team has localized it in Arabic and English for us. Several other localizations come out of the box with Flutter, which can be a huge time saver.

I won’t bore you with internationalizing the rest of the app, since it’s basically “rinse and repeat” from here. However, if you want to see the fully internationalized and localized app, check out the Git repo for the completed project.

Do More with intl

The intl library can do more than what we covered here. It can help you work with numbers, genders, bidirectional text, and money. Check out the intl documentation to see how you can save time in your i18n work by using the package.

And Now I Flutter Away (Sorry!)

Writing code to localize your app is one task, but working with translations is a completely different story. Many translations for multiple languages may quickly overwhelm you which will lead to the user’s confusion. Fortunately, Phrase can make your life as a developer easier!

Now with built-in ARB file support (Flutter devs rejoice!), Phrase is your one stop shop i18n/l10n platform. Built by developers for developers, Phrase features a robust API and CLI. Phrase also offers native integration with GitHub, GitLab, and Bitbucket. Branching, versioning, over-the-air (OTA) translations, machine learning translations, and a beautiful web console for your translators are just some of the perks Phrase gives to your development team. Check out all of Phrase’s features and take it for a free spin for 14 days.

I hope this article helped you get started with Flutter i18n and l10n. If you have any questions or would like to see different Flutter or non-Flutter i18n topics covered, please let us know in the comments. Happy Coding 🙂


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK