How to quickly and easily localize an application on flutter. riverpod + slang
Hello. In this article, I want to share my knowledge on how to quickly localize an application on flutter. The foundation of this knowledge was laid in the development of a product called Weather Today.
As an introduction, I want to draw attention to the difference between the terms localization and internationalization. Internationalization (internationalization, i18n) is the process of developing an application so that it can be adapted to different languages and regions without engineering changes. Localization (localization, L10n) is characterized as the process of adapting internationalized software for a particular region or language by translating text and adding region-specific components. The difference is that localization is done multiple times (for example, when a new language is added) and is based on an internationalization infrastructure that should be done once in well-designed software. Now I will show exactly the process of localization.
This material is part of a series of articles about creating an application weather today (Google Play) – a concise and free product for monitoring weather conditions in your smartphone.
This is how the official approach to flutter app localization looks like: “Internationalizing Flutter apps”. In addition, even more detailed material is offered: Flutter Internationalization User Guide. The official approach is quite verbose and wants a lot from the developer. Therefore, it was decided to pick up something more meek. And, most importantly, functional.
fast_i18n link. Now it’s slang link(v3.12.0 of Feb 13, 2023).
Plastic bag localization too simple and not functional. No, well, almost nothing. However, I will note that the developer tried to make the package even better by providing an application for setting keys (link):
We have an application to help you configure your translation keys. The project is also open-source, so be fine if you want to help it evolve!
The easy_localizationperhaps the most popular package, however, out of the box it cannot achieve adequate reactivity even using BuildContextsee issue 370. And this is outrageous for such a well-liked and publicized package. Nevertheless, it supports (judging by the description) various formats of stored translations (JSON, CSV, XML, Yaml), is able to pluralize, has some useful methods for flutter (reset locale, get device locale, etc.), there is code generation and even logger. Using it is quite simple (with code generation):
But all this was poorly combined with the fantasies of the author of this article. In my case it was important that:
using the package was not frustrating and could clutter code
was friendly and customized to use
with good and (importantly) detailed documentation
reactivity was maintained. This desire to break away BuildContext and use your state controller based on riverpod
there was independence from the flutter framework (only dart). This feature would come in very handy, for example, in console applications, so that you don’t have to fence your bikes with localization.
How to create a dart console application using the weather_pack package?
In this article, we will look at how you can create a console application that allows you to get …
www.habr.com
And everything faded when I found this miracle – fast_i18n, the third monitored package on our list. Soon the developer redesigned this package, incorporating the best ideas into it; that’s how it was born slang (sstructured languide file generator).
Let’s just say that the review of this package below is both a story about the localization of my application and public gratitude Tien Do Nam (github) (and contributors) for such a great package (also under the MIT license).
Using the slang package
A short tour of this package. Let’s say roughly that this package can do everything that it can and easy_localization (roughly, because this package does a lot of things better, starting with the documentation). In addition:
Does not depend on build_runnerbut it can work with it
Has a bunch of tools for all occasions
Deep customization with flags
Pluralization, cardinals and ordinals (cardinal and ordinal numbers), gender forms
Able to work with RichText
Supports lists, maps and dynamic keys, interfaces
Dynamic redefinition of translations
Can integrate with various state managers
Next, we will analyze how I integrated this package into the Weather Today application.
Implementation
The easiest way is described in the documentation Here. It is quite detailed, and there is no point in repeating it. We will consider the integration of this package with flutter_riverpod.
In our main method main Let’s write a couple of initialization lines:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// используем контейнер, чтобы в нём асинхронно инициализировать состояние провайдеров
final container = ProviderContainer();
await container.read(AppLocalization.instance).init();
runApp(
UncontrolledProviderScope(
container: container,
child: WeatherApp(),
),
);
);
}
AppLocalization – this is our class that contains all the logic for working with the locale (locale) of the application and with the package slang. Here is what happens in the method AppLocalization.init():
/// No rebuild after locale change.
TranslationsRu get tr => _tr;
late TranslationsRu _tr;
class AppLocalization {
/// экземпляр класса
static final instance = Provider<AppLocalization>(AppLocalization.new);
/// Текущая локаль приложения.
static final currentLocale = StateProvider<AppLocale>((ref) => AppLocale.ru);
/// Текущий translation.
static final currentTranslation = StateProvider<TranslationsRu>(
(ref) {
final AppLocale locale = ref.watch(currentLocale);
// ignore: join_return_with_assignment
_tr = locale.build(); // we need to assign
return tr;
}
);
Future<void> init() async {
final AppLocale locale = AppLocaleUtils.parse(await _getUserStoredLocale());
Intl.defaultLocale = locale.languageCode;
_tr = locale.build();
ref.read(currentLocale.notifier).update((_) => locale);
}
}
Some explanations:
As soon as we update currentLocaleautomatically updated currentTranslation and all other providers that follow with the method watch.
instance needed to access class methods AppLocalization.
Variable tr needed in some cases where access to currentLocale difficult to get (by and large, due to the reluctance to throw Ref from Riverpod and complicate the code). It is undesirable to do this: if the user changes the language and continues to use the application (without reloading), then those objects that have the old instance tr, will not be updated. However, in situations where we are monitoring the life cycle of objects, this is a good use case. In my case, there is a good example (just below).
Next, in the method AppLocalization.init() in the first line we load the user’s choice locale from the database. Changes in Intl.defaultLocale are necessary for localization to get deep into the bowels of flutter, let’s say. We then change our local variable _tr and provider status currentLocale.
Now that the locale is ‘loaded’, in WeatherApp() set up MaterialApp:
pay attention to supportedLocales And localizationsDelegates. We will return to them a little later, when we review the remaining class methods. AppLocalization.
How to get translation without BuildContext?
Here’s a good example: in a package that I don’t have access to, there is a file like this (simplified):
/// Represents units of pressure measurement.
enum Pressure {
hectoPa('Hectopascal', 'hPa'), // Гектопаскали -- гПа
mbar('Millibar ', 'mbar'), // МиллиБары -- мБар
mmHg('Millimetre of mercury',
'mmHg'), // Миллиметры ртутного столба -- мм. рт. ст.
kPa('Kilopascal', 'kPa'), // Килопаскали -- кПа
atm('Atmosphere', 'atm'), // Атмосферы -- атм
inHg('Inch of mercury', 'inHg'); // Дюймы ртутного столба -- дюйм рт. ст.
const Pressure(this.name, this.abbr);
/// Full name.
final String name;
/// Abbreviation.
final String abbr;
}
Our task is to get the translation of the fields name And abbr. We do the following:
extension PressureTr on Pressure {
String get abbrTr {
switch (this) {
case Pressure.hectoPa:
return tr.units.pressure.abbr.hectoPa;
case Pressure.mbar:
return tr.units.pressure.abbr.mbar;
case Pressure.mmHg:
return tr.units.pressure.abbr.mmHg;
case Pressure.kPa:
return tr.units.pressure.abbr.kPa;
case Pressure.atm:
return tr.units.pressure.abbr.atm;
case Pressure.inHg:
return tr.units.pressure.abbr.inHg;
}
}
String get nameTr {
switch (this) {
case Pressure.hectoPa:
return tr.units.pressure.name.hectoPa;
case Pressure.mbar:
return tr.units.pressure.name.mbar;
case Pressure.mmHg:
return tr.units.pressure.name.mmHg;
case Pressure.kPa:
return tr.units.pressure.name.kPa;
case Pressure.atm:
return tr.units.pressure.name.atm;
case Pressure.inHg:
return tr.units.pressure.name.inHg;
}
}
}
And thus through getter we get the localized values of the corresponding fields. Otherwise, you must use a function with a parameter TranslationsRu. And since similar PressureTr (SpeedTr, TempTr etc. ) is enough, it would be irrational to pass the parameter every time.
In the application it will look like this:
(There is another way that will bind us more strongly to slang – use Custom Contexts / Enums. It is good because now we do not need to monitor the global state tr as well as write extensions of the form PressureTr. The code for getting the current locale in widgets will become even shorter)
How can I access the current translation?
Next, we can use our translation like this:
class _TilePressureUnitsWidget extends ConsumerWidget {
const _TilePressureUnitsWidget();
@override
Widget build(BuildContext context, WidgetRef ref) {
// получаем перевод
final t = ref.watch(AppLocalization.currentTranslation);
// отслеживаем актуальные единицы измерения давления
final Pressure units = ref.watch(SettingsPageController.pressureUnits);
// то самое расширение PressureTr
final String unitsTr = units.abbrTr;
return ListTile(
leading: AppIcons.pressureUnitsTile,
title: t.settingsPage.pressureTile.tileTitle,
subtitle: unitsTr,
onTap: () {...},
);
}
}
Our widget _TilePressureUnitsWidget will be rebuilt whenever the current locale is changed AppLocalization.currentTranslation.
Where are the translation files?
Well, now it remains only to generate these very translations, and also write them 🙂 Let’s create a json file with the following content:
Now let’s generate from json –> dart files with the following command
flutter pub run slang
Or a team flutter pub run slang build.
In the terminal we see the following (attached a screen, because some file settings are visible here slang.yaml):
Notice how fast it is! Now remember the generation rate build_runner and cry, since the author of the package leaves us this opportunity by connecting the following dependencies to the project:
In file translation.g.dart there are a number of useful methods. Use them as needed:
How to customize the slang.yaml configuration file?
Great, our type-safe translations are ready. How do you set up a file? slang.yaml? After all, it is he who is responsible for the correct generation of code. A full list of options is available in the godlike documentation Here. In our case, it looks like this:
base_locale: ru # базовый язык
fallback_strategy: base_locale # в случае ошибки возвращаемся к базовой локали
input_directory: assets/i18n # путь хранения переводов
input_file_pattern: .i18n.json
output_directory: lib/i18n # путь генерации переводов
output_file_name: translations.g.dart
output_format: multiple_files
string_interpolation: braces # в json используем так: "Наш параметр {параметр}"
enumName: AppLocale # название enum локали
key_case: camel # именование переменных в соответствии со спецификацией dart
key_map_case: null
param_case: camel
flat_map: false # нет необходимости в создании Map переводов
namespaces: false # удобство перевода постранично. Не используем.
locale_handling: false # remove unused t variable, LocaleSettings, etc.
translation_class_visibility: public
Now it’s time to remember some additional class methods AppLocalization. Here they are:
class AppLocalization {
AppLocalization(this.ref);
final Ref ref;
/// экземпляр класса
static final instance = Provider<AppLocalization>(
(ref) => AppLocalization(ref)
);
// доступ к базе данных
IDataBase get _dbService => ref.read(dbService);
// .....
/// Текущая локаль девайса.
AppLocale get deviceLocale => AppLocaleUtils.findDeviceLocale();
/// Список поддерживаемых локалей.
List<Locale> get supportedLocales =>
AppLocale.values.map((locale) => locale.flutterLocale).toList();
/// Делегаты.
List<LocalizationsDelegate> get localizationsDelegates =>
GlobalMaterialLocalizations.delegates;
/// Установить новую локаль. (с сохранением в бд)
Future<AppLocale> setLocale(AppLocale locale) async {
await _saveLocale(locale.flutterLocale);
Intl.defaultLocale = locale.languageCode;
ref.read(currentLocale.notifier).update((_) => locale);
return locale;
}
/// Сохранение локали в бд.
Future<void> _saveLocale(Locale locale) async =>
_dbService.save(DbStore.appLocale, locale.languageCode);
}
You may not be familiar with tear off (break) so I rewrote our instance provider more clearly.
supportedLocales uses AppLocaleto collect the entire list of supported locales
localizationsDelegates contained here package:flutter_localizations/src/material_localizations.dart (we mentioned this in dependencies) All these parameters were specified earlier in MaterialApp.
How to change the language in the application?
Perhaps the only thing left to consider is how to change the locale. To do this, you need to call the method AppLocalization.setLocale() from a callback, for example in DropdownButton. In the method itself, we save the locale to the database and then update the provider currentLocale. Thus, all our translations will be updated immediately.
I recently talked about how to make such a background (in fact, it is an animation) on the start screen in the article “Why animated weather is a code from the configurator or The story of one sad package”
Why animated weather is a code from the configurator or the history of one sad package
“Damn, where to find these free and royalty-free resources of yours for commercial and personal use …
www.habr.com
Bonus when using slang
As a killer feature, I want to show you wonderful command line tools (just in case, version slang: ^3.12.0) (in the documentation tools):
flutter pub run slang watch
Acts according to a similar method from the package build_runner. Triggers code generation every time a translation file e.g. translations.i18n.json, changes. An extremely convenient command when a translation is added very often in small portions. Launched and forgot. To run the generation once, remove watch from the team.
flutter pub run slang migrate <type> <source> <destination>
Migration tool for other i18n solutions. Currently supports conversion ARB format in JSON:
flutter pub run slang migrate arb source.arb destination.json
flutter pub run slang analyze
A very handy command to find missing and unused translations. There are additional flags. How it works? You add a new translation to, say, translations.i18n.json. Run this command and get the files:
In file _unused_translations.json will store unused translations in all source code (with the flag --full). Eat modifiersignoreMissing And ignoreUnusedwhich allow certain keys to be ignored during parsing.
And in _missing_translations.json we get missing translations for specific locales:
{
"@@info": [
"Here are translations that exist in <ru> but not in secondary locales.",
"After editing this file, you can run 'flutter pub run slang apply' to quickly apply the newly added translations."
],
"en": {
"settings_page": {
"temp_page": {
"tile_title": "Единицы измерения температуры",
"dialog_sub": "Выбранный параметр будет применен во всех измерениях."
}
}
}
}
You can use flags if you like. --split-missing And --split-unusedto separate missing and unused translations for each locale. After that, it remains to replace the translation in this file and run the following command:
flutter pub run slang apply
And your translation will be added to the corresponding file; in our case in translation_en.i18n.json (starting with v3.12.0 translations will be added to the appropriate places, not just at the end of the file). This is extremely convenient, because you do not need to run around with a copy of the new translation through local files and manually add everything. Don’t forget to run the command after flutter pub run slang buildto generate dart code.
flutter pub run slang stats
Prints some statistics to the console, for example:
Perhaps this is all the information that I wanted to share about the localization of an application written in flutter. I would be happy to discuss this strategy of the Riverpod + slang bundle in the comments.