Skip to content

Integration with Flutter Intl

Roi Peker edited this page Aug 12, 2021 · 12 revisions

This sample shows how to integrate with the official Flutter approach.

Table of contents:

Click to expand

step 1

If you come from the Setup Wiki, delete the following folders and their contents so you can start clean:

  • /strings/
  • /assets/
  • /lib/i18n/

Add the needed dependencies in pubspec.yaml, as stated in the docs:

## pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.17.0 
# ...
flutter:
  uses-material-design: true
  generate: true

Create l10n.yaml (required by intl generator) in your project's root with the following content:

## l10.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

It will look for the following file lib/l10n/app_en.arb to auto-generate the Dart code everytime you run flutter pub get.


step 2

Let's use a handy fts feature to get the Strings you currently have in the counter app.

Open your terminal and execute:

fts extract -s -p lib/ -o strings/strings.yaml

fts extract output.

extract searches recursively in --path lib/ for any dart files (check fts extract --help to define other extensions), and tries to capture the Strings it finds, by default creating the keys based on a counter (textN) and a folder-file structure.

strings/strings.yaml

main:
  text1: Flutter Demo
  text2: Flutter Demo Home Page
  text3: "You have pushed the button this many times:"
  text4: "{{counter}}"
  text5: Increment

In this case the Counter App is way too simple, just a few lines of code. But in real projects, extract can truly help to keep the keys and strings in a single place very fast.

Clean, and rename keys in the master strings file to keep what we will use:

appbarTitle: Flutter Demo Home Page
message: "You have pushed the button {{counter}} times:"
tooltip: Increment

step 3

Configure the output from trconfig.yaml

Open trconfig.yaml and comment (or remove) the following lines:

## output_json_template: assets/i18n/*.json

and

  ## keys_id: TKeys

We will not use the autogenerated Keys nor the JSON files this time.

Intl expects .arb files as defined in l10n.yaml, so add the following lines to trconfig.yaml

output_arb_template: lib/l10n/app_*.arb
# modify the entry_file to use our extracted strings.
entry_file: strings/strings.yaml

With comments removed, trconfig.yaml should look something like this:

output_arb_template: lib/l10n/app_*.arb
entry_file: strings/strings.yaml
param_output_pattern: "{*}"

dart:
  output_dir: lib/i18n
  translations_id: TData
  use_maps: false

locales:
  - en
  - es
  - ja

gsheets:
  use_iterative_cache: false
  credentials_path: credentials.json
  spreadsheet_id: {SHEET_ID} ## Sheet id from Configuration setup page.
  worksheet: Sheet1

step 4

In the Terminal execute fts run again. The output might be a little verbose, but shows you the progress while it tries to sync the data, clearing some, now, inexistent keys from the sheet and creating new rows for the data we just defined in strings.yaml

The worksheet might look like this: new data in google sheet

And your project should have new files and folders generated: intl project view

step 5

Open lib/main.dart and add the missing code from intl article:

import 'package:flutter_localizations/flutter_localizations.dart';
/// auto generated after you run `flutter pub get`
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

And

return MaterialApp(
  title: 'Localizations Sample App',
  localizationsDelegates: [
    AppLocalizations.delegate, // Add this line
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: [
    Locale('en', ''), // English, no country code
    Locale('es', ''), // Spanish, no country code
  ],
  home: MyHomePage(), /// add the title directly inside the widget.
);

Inside _MyHomePageState, let's try to show the localized title first

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          // 'Flutter Demo Home Page',
          AppLocalizations.of(context)!.appbarTitle, 
        ),
      ),

Set an existing locale to MaterialApp... the default template already generated en, es and ja if you didn't modify it.

return MaterialApp(
  title: 'Localizations Sample App',
  locale: Locale('ja'),

After a hot-reload, you should see the localization message applied: counter app Locale ja

Add the missing strings to your widget :

@override
Widget build(BuildContext context) {
  final loc = AppLocalizations.of(context)!;
  return Scaffold(
    appBar: AppBar(
      title: Text(loc.appbarTitle), /// here 
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(loc.message(_counter)), /// here 
          Text(
            '$_counter',
            style: Theme.of(context).textTheme.headline4,
          ),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _incrementCounter,
      tooltip: loc.tooltip, /// here 
      child: Icon(Icons.add),
    ),
  );
}

... try changing local in MaterialApp to Locale('es')

intl counter app localized to 'es'

Seems like GoogleTranslate didn't translate the text properly, you will see later how to modify the translations.

step 6

fts code utilities:

fts generates some utility code at lib/i18n/i18n.dart to simplify part of the integration.

AppLocales contains the available Locales and LangVo objects (LangVo (ValueObjects models) holds some useful properties: english name, native name, emoji flag and locale id and instance) of the current available locales.

For quick tests switching languages, SimpleLangPicker, is a dropdown menu that automatically fills the items with AppLocales.available.

Make language change

In order to change Locale (language), we need to re-build MaterialApp, you might do this with your preferred State Management solution, to keep it simple, we will use ValueNotifier.

/// The "master" locale is the one we use in our master template, the first locale in trconfig.yaml
/// in our case, "en"
final currentLocale = ValueNotifier<Locale>(AppLocales.supportedLocales.first);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<Locale>(
      valueListenable: currentLocale, /// the ValueNotifier
      builder: (_, locale, __) => MaterialApp(
        title: 'Localizations Sample App',
        supportedLocales: AppLocales.supportedLocales, /// a list of predefined Locales
        locale: locale,
        localizationsDelegates: [
          AppLocalizations.delegate,
          GlobalMaterialLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate,
          GlobalCupertinoLocalizations.delegate,
        ],
        home: MyHomePage(),
      ),
    );
  }
}

This way, assigning a new value to currentLocale.value will rebuild MaterialApp.

return Scaffold(
  appBar: AppBar(
    title: Text(loc.appbarTitle),
    actions: [
      SimpleLangPicker(
        onSelected: (locale) => currentLocale.value = locale,
        selected: currentLocale.value,
      ),
    ],
  ),

SimpleLangPicker

The power of fts is the simplicity to keep a single structured data source. Add a a few more languages to trconfig.yaml.

locales:
  - en
  - es
  - ja
  - fa # Persian
  - el # Greek
  - ru # Russian 
  - zh # simplified Chinese

open Terminal and run:

fts run

Right away, new columns in your worksheet will hold the new available locales with the automatic translations and everything will be synced (also the app bundles.)

... hot-reload

new locales on hot reload

pluralization

Based on the official docs, the supported plural forms are:

  • =0 : zero
  • =1 : one
  • =2 : two
  • few : few
  • many : many
  • other : other (required)

To define a plural message, we need a variable that defines quantity. After you define the key:, use this pseudo-code structure as reference:

keyName:
  plural:quantityVariable:
    =1: Singular
    =other: Default plural form

Modify strings/strings.yaml to use plurals:

appbarTitle: Flutter Demo Home Page
tooltip: Increment
counterMessage:
  plural:count:
    =0: "You haven't pushed the button"
    =1: "You pushed the button once"
    =2: "You pushed the button twice"
    other: "You pushed the button {{count}} times"

Sync the changes running fts run

If you check the worksheet, each variant of the plural takes a row.

And the placeholder {{count}} is replaced by {{0}}. This is to avoid the translation of the variable name, in some weird cases GoogleTranslate might break the braces, like {0}}, but fts will detect the pattern anyway. The actual variables names are saved locally next to your master yaml file, in vars.lock.

Modifying translations

As I speak Spanish, I can spot an inaccurate translation in the es column. When you try to edit the cell's text, you will see the translate formula instead: cell formula

Although you can type right away and replace the cell's content with the raw text, might be more practical to have the raw translated text:

  1. Select all cells (⌘+A or Ctrl+A)
  2. Copy (⌘+C or Ctrl+C)
  3. Paste "special" (⌘+Shift+V or Ctrl+Shift+V)

Or right click on the selection and find the context menu item: Paste Special

In this case, the modification is simple: "Empujaste" > "Apretaste". But it requires fts to get the updated strings from the sheet...

Instead of using the run command, we can use fetch, as there's no need to sync the source data into the sheet... only retrieve the values, so overall, it's a faster and light process as long as you didn't change the keys or master text in strings.yaml.

Run in your terminal:

fts fetch

And the output files (arb in this case) will be updated.

Back to main.dart, modify the Scaffold's body to use the counterMessage instead of the default counter Text:

body: Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text(loc.counterMessage(_counter)), /// new line, takes the count (int)
    ],
  ),
),

... hot-reload

Intl plurals

Final note:

For a reference of supported types and format using arb check this link.

Currently Flutter's Intl code generator doesn't support Gender and Selector modifiers.

Localizing iOS and MacOS

If you are on macos, the cli will update the localization for ios and macos projects.

iOS locale sync