Blizzard Intl
Blizzard Intl provides a means of facilitating the internationalization of
Flutter apps by offering a set of text-related widgets that take a
Map<T, String>
instead of a String
data type allowing you to pass in
multilingual translations instead of monolingual text.
Basic Setup
- Create an enum to represent the supported languages.
- Optional: Create aliases for each language.
- Optional: Create a typedef for the language map.
- Wrap the app in a
LanguageWrapper
and set the default language.
Step 1
Create an enumerated type to represent the supported languages of the app.
Example:
enum Language {
english,
german,
}
Step 2 (Optional)
It is recommended to create aliases in the form of abbreviations for each value of the enumerated language type.
Example:
const de = Language.german;
const en = Language.english;
Step 3 (Optional)
It is recommended to create a typedef for the language map.
Example:
typedef Translation = Map<Language, String>;
Step 4
Wrap the app in a LanguageWrapper
and set the default language. You can
optionally set the selected language.
Example:
LanguageWrapper<Language>(
defaultLanguage: en,
child: MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text({
de: 'Meine App',
en: 'My App',
}),
),
),
),
);
Note: If selectedLanguage
is null
, then defaultLanguage
will be used for
text widgets until a language is selected. If a translation is missing for the
selected language, the translation for the default language will be used.
Complete Example
import 'package:blizzard_intl/blizzard_intl.dart' as intl;
import 'package:flutter/material.dart';
enum Language {
english,
german,
}
const de = Language.german;
const en = Language.english;
typedef Translation = Map<Language, String>;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return intl.LanguageWrapper<Language>(
defaultLanguage: en,
child: MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const intl.Text({
de: 'Meine App',
en: 'My App',
}),
),
),
),
);
}
}
Obtaining the Language
The default or selected language can be obtained via LanguageManager
.
Example:
final defaultLanguage =
LanguageManager.of<Language>(context).defaultLanguage;
final selectedLanguage =
LanguageManager.of<Language>(context).selectedLanguage;
Note: This is generally not needed as the provided text widgets handle this under the hood.
Selecting the Language
The language can be selected by calling the onLanguageSelected
method of
LanguageManager
.
Example:
LanguageManager.of<Language>(context).onLanguageSelected(de);
Separation of Concerns
Given that Blizzard Intl favors Locality of Behavior (LoB), it is recommended to split translations up into local and global translations, whereby local should be preferred over global by default and only promoted to global when necessary.
Local Translations
Local translations define the translations directly on the widget where the text should appear.
Example:
class ConfirmButton extends StatelessWidget {
const ConfirmButton();
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {},
child: intl.Text({
de: 'Bestätigen',
en: 'Confirm',
}),
);
}
}
Global Translations
Global translations define the translations in a global file which is then imported locally.
Example:
translations.dart
const Translation confirmText = {
de: 'Bestätigen',
en: 'Confirm',
};
confirm_button.dart
import 'package:blizzard_intl/blizzard_intl.dart' as intl;
import 'package:flutter/material.dart';
import 'translations.dart' as translations;
class ConfirmButton extends StatelessWidget {
const ConfirmButton();
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {},
child: intl.Text(translations.confirmText),
);
}
}
Import Conflicts
The text widgets provided by Blizzard Intl correspond exactly to their Flutter
equivalents both in naming and parameters. This means they function exactly in
the same way, the only difference being that any String
parameter should be
replaced by Map<T, String>
. This includes semanticsLabel
.
There are cases when providing multiple translations would be unnecessary, such
as when displaying a counter to the user. So Blizzard Intl does not make
Flutter's String
-based widgets obsolete.
It is therefore recommended to use a qualified import for Blizzard Intl as in the following example:
import 'package:blizzard_intl/blizzard_intl' as intl;
An alternative approach that may be useful for projects that rarely use
monolingual text would be to create a prelude file such as prelude.dart
in the
base of lib
, export the desired defaults, and then use a qualified import for
Flutter when using monolingual text as in the following example:
prelude.dart
export 'package:blizzard_intl/blizzard_intl.dart';
export 'package:flutter/material.dart'
hide InlineSpan, RichText, Text, TextSpan, WidgetSpan;
my_app.dart
import 'package:flutter/material.dart' as flutter;
import 'prelude.dart';
Rationale
Actually, I've spent quite a bit of time over the past few years, sort of on and off, researching different ways to structure a front-end application in a scalable way. I've tried many different things both professionally and in my free time from making simple game engines in C to developing e-learning applications in vanilla JavaScript, React and Elm to writing business software in Dart and Flutter. What, in my opinion, tends to make scaling front-end applications difficult is the way in which people deal with separation of concerns. This is a very deep topic, but to keep myself from turning this README into a book, let's just say I like to keep related things together. This includes markup, styling, internationalization, behavior etc. Let me give you a simple example.
Let's say we want to make our app multilingual. What a lot of people do is use something like i18n where all the text is stored in some central file. You then end up naming each piece of text. If the text is short and simple, then you can have something like this:
en:
submit: 'Submit'
de:
submit: 'Bestätigen'
And for simple, frequently used text, this is mostly fine. One issue with this kind of global text, however, is that sometimes you want different translations for a particular language. Perhaps for a particular button, we don't want the English version to be 'Submit', but rather 'Confirm'. We want the German version to be the same in both cases. So now we end up with this:
en:
submit_button_on_form: 'Confirm'
global_submit_button: 'Submit'
de:
submit_button_on_form: 'Bestätigen'
global_submit_button: 'Bestätigen'
As the application grows and becomes more complicated, the translations become more and more fragmented and it becomes difficult to know which components in the app will be affected if I change this line. Usually you end up having to do a search and then manually confirm each change. There was actually a post on Hacker News not long ago about how Microsoft mistranslated "Zip" for "Postal Code" in the context of zipping a file. This stuff is hard.
Another issue is that you often end up with longer text that is much more difficult to name like this:
en:
warning2: 'Please note that you are legally required to take a break for '
'at least 15 minutes every 4 hours of work.'
de:
warning2: 'Bitte beachten Sie, dass Sie verpflichtet sind, alle 4 Stunden '
'eine Pause von mindestens 15 Minuten zu machen.'
warning2 is pretty vague here and if poorly named, can also be very misleading. It says it's a warning, but in the app I see red text and an error symbol. This isn't a warning at all. Also, our UX team said to make a text change to warning3, but the text for warning3 is completely different. Did they confuse warning2 with warning3 or was it wrong in the app all along?
Naming stuff is a language and communication problem in general. And again, these differences in naming, understanding and communicating often result in fragmentations that can go unnoticed and make their way into production.
What helps, although it's certainly not a solution to the limitations of language and communication, is putting stuff where it belongs. If you have a component or some markup, put the translation on that component. If you make any changes, then you'll immediately see exactly which component it will affect. If that component is being used in multiple places, then you can do a direct search without first having to map the translation to the key to the component or components.
I feel the same fragmentation happens with CSS and that's why people are moving toward something like Tailwind CSS. That isn't to say that globals should always be avoided, but it often feels like the programming community is split between those who always use globals and those who never use globals, ever, and there is no middle ground that uses both appropriately when needed. (I'm using the terms local and global here in a very abstract sense, not only referring to state.)
Libraries
- blizzard_intl
- A library that facilitates the internationalization and localization of Flutter applications.