Hello, Welcome back. Glad that you’re here.
We're building a Movie App with the best coding practices and tools. In this tutorial, we'll introduce language options in the app.
Languages Entity
We're supporting two languages English and Spanish. In the future, the number of languages can increase so to properly provide a setup that can scale over time, let's create LanguageEntity
.
In domain/entities folder, create a new file language_entity.dart:
class LanguageEntity {
//1
final String code;
//2
final String value;
//3
const LanguageEntity({@required this.code, @required this.value});
}
- Declare
code
as the final field. Code stores the language code -en
andes
. - Now declare
value
as another final field. The value will store theEnglish
andSpanish
. Navigation Drawer will use these values in the dropdown. - Create the constructor with both fields as required.
Now declare a list of languages that you want to support.
In common/constants folder, create a new file languages.dart:
class Languages {
//1
const Languages._();
//2
static const languages = [
LanguageEntity(code: 'en', value: 'English'),
LanguageEntity(code: 'es', value: 'Spanish'),
];
}
- Declare a private constructor.
- Create an array of languages with 2 Language Entities.
In the previous tutorial of the navigation drawer, we have used direct language strings. Let's replace them with the entries in the language constants.
NavigationExpandedListTile(
title: TranslationConstants.language,
//1
children: Languages.languages.map((e) => e.value).toList(),
onPressed: (index) {},
)
- Use the map operator that would return a list of values in the Language Entity. Since the map operator returns Iterator, you need to use
toList
to convert it to a list.
Supported Locales & Default Locale
Now you have declared the locales, you need to tell MaterialApp about those locales and also set a locale that the app will use at the launch. Open movie_app.dart and add below snippet in MaterialApp:
//1
supportedLocales: Languages.languages.map((e) => Locale(e.code)).toList(),
//2
locale: Locale(Languages.languages[0].code),
supportedLocales
take in an array as well, but an array of codes. So this time instead of value, you will use the code. i.e.en
andes
.- Assign the
en
locale, which will be the language used at the start of the app.
Default Localization Delegates
Open pubspec.yaml and add the _flutterlocalization dependency:
flutter_localizations:
sdk: flutter
Widgets in the Flutter framework already have delegates that help in RTL support. So, to provide that to all the widgets in the app, you can add localizationsDelegates
:
localizationsDelegates: [
//1
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
]
- As the name suggests, the first one is for MaterialWidgets and another one for any type of widget.
Language JSON
We'll declare all the strings used in the app in JSON files. As we're supporting 2 languages, we will add 2 JSON files in the assets folder.
Create a languages folder under assets folder Add 2 JSONs in languages folder, en.json and es.json
Update pubspec.yaml with new assets entry:
- assets/language/
Add the strings that are there in the app till now. Like, tab texts and text in the navigation drawer.
In en.json add the English text and in es.json add the Spanish text.
I have taken Spanish text from Google Translate.
{
"favoriteMovies": "Favorite Movies",
"language": "Language",
"about": "About",
"feedback": "Feedback",
"popular": "POPULAR",
"now": "NOW",
"soon": "SOON",
}
{
"favoriteMovies": "Peliculas favoritas",
"language": "Idioma",
"about": "Acerca de",
"feedback": "Realimentación",
"popular": "POPULAR",
"now": "AHORA",
"soon": "PRONTO",
}
It is important to have the same key in both the JSON files.
Custom Localizations Delegate
As we added MaterialLocalization Delegate before in MaterialApp, we can also create custom delegates to load our custom translated strings from en.json
and es.json
.
In presentation folder, create a new file app_localizations.dart:
//1
class AppLocalizations {
//2
final Locale locale;
AppLocalizations(this.locale);
//6
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationDelegate();
}
//3
class _AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
//4
const _AppLocalizationDelegate();
@override
bool isSupported(Locale locale) {
throw UnimplementedError();
}
@override
Future<AppLocalizations> load(Locale locale) {
throw UnimplementedError();
}
//5
@override
bool shouldReload(covariant LocalizationsDelegate<AppLocalizations> old) =>
false;
}
- Create a class
AppLocalizations
. - We will load translations bases on Locale so add a Locale final field.
- Declare a delegate that extends
LocalizationsDelegate
of typeAppLocalizations
. - Give it a const constructor.
- Override the 3 methods and return false from
shouldReload()
. We'll work on other methods in a moment. - In
AppLocalizations
, add a delegate field that contains an instance of_AppLocalizationDelegate
.
Now, in movie_app.dart, in the delegates add AppLocalizations.delegate as well.
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
You'll get UnimplementedException as we are not completely done with loading specific translations for a specific Locale.
In the delegate class add body to the other 2 overridden methods:
//1
@override
bool isSupported(Locale locale) {
return Languages.languages
.map((e) => e.code)
.toList()
.contains(locale.languageCode);
}
@override
Future<AppLocalizations> load(Locale locale) async {
//2
AppLocalizations localizations = AppLocalizations(locale);
//3
await localizations.load();
//4
return localizations;
}
- This is basically for safety checks that the locale that we are loading is supported by the application. So, you'll check whether the locale selected is in the list of your locales.
- Here, you'll create an instance of
AppLocalization
with the selected locale. - Then, you'll load the JSON that you have created based on the language code. The
load()
will be created in just a moment. - After successfully loading the JSON map, you'll return the localization.
Now, create load method in
AppLocalizations
:
//1
Map<String, String> _localizedStrings;
//2
Future<bool> load() async {
//3
final String jsonString = await rootBundle
.loadString('assets/language/${locale.languageCode}.json');
//4
final Map<String, dynamic> jsonMap = json.decode(jsonString);
//5
_localizedStrings = jsonMap.map((key, value) {
return MapEntry(key, value.toString());
});
return true;
}
- Declare a map of String as key and String as value. This will hold the parsed JSON for
en.json
andes.json
. - Create the
load
method to return true or false. - First thing in the method you'll load the JSON file from assets. You'll load a dynamic JSON based on the
locale.languageCode
. - Now decode the JSON to the map.
- Since, convert library cannot guarantee the value type, we need to use the
toString
method on the value. For that, use the map operator and insert each value in thelocalizedStrings
map.
Now, you have got the map that has keys and values from en.json
or es.json
. The next thing would be to get a string by key from this map. So, create a function to return the value.
String translate(String key) {
//1
return _localizedStrings[key];
}
- Very straight-forward as this just return the value by key from
localizedStrings
.
One last thing before we start using translated strings is a way to get an instance of the AppLocalizations
.
Create a static of
in AppLocalizations
:
//1
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
- Using the type of object, you can get the instance of
AppLocalizations
fromLocalizations
.
Now, use translations in one of the places. Open navigation_drawer.dart
and update the Favorite Movies title:
AppLocalizations.of(context).translate('favoriteMovies')
- First, we get the instance of
AppLocalizations
, then we pass the key intranslate()
. This will returnFavorite Movies
in English locale.
Run the app now. Everything is the same as previously.
Change the locale
in MaterialApp
to Spanish and run the app again.
You can see the translated text for Favorite Movies
as Peliculas favoritas
.
Translation Constants
If you see hardcoded string 'favoriteMovies' used as the key. When you've many instances of this key, you'll have to write it again. So, it is better to put these in a constants file.
In common/constants folder, create a new file translation_constants.dart:
//1
class TranslationConstants {
TranslationConstants._();
static const String favoriteMovies = 'favoriteMovies';
static const String language = 'language';
static const String about = 'about';
static const String feedback = 'feedback';
static const String popular = 'popular';
static const String now = 'now';
static const String soon = 'soon';
}
- Here we will store keys in constants so that we can now use them when translating a text.
Use these constants and AppLocalizations everywhere in the project where you are using text.
We'll not use them for English and Spanish text as they will never change when locale changes.
After you're done using translated strings, run the app. You'll see when you change the default locale in MaterialApp
, the text changes based on Locale.
String Extension
You've seen everywhere we use AppLocalizations.of(context).translate('key')
. To reduce this, let's create a string extension.
In common/extensions folder, create a new file string_extensions.dart:
extension StringExtension on String {
String t(BuildContext context) {
return AppLocalizations.of(context).translate(this);
}
}
- Just create a method
t()
, that will replace the key in the translate method.
Now, go on and update everywhere in the app where you used AppLocalizations.of(context).translate('some_key')
.
Run the app now, absolutely no difference. We have now created a method that can be used anywhere in the application.
Language Bloc
You've seen I every time change the locale in MaterialApp
to see different translations. Let's add a Bloc to handle this for us.
In presentation/blocs folder, create a new bloc - language_bloc:
Create event - ToggleLanguageEvent
:
//1
class ToggleLanguageEvent extends LanguageBlocEvent {
//2
final LanguageEntity language;
const ToggleLanguageEvent(this.language);
//3
@override
List<Object> get props => [language.code];
}
- An event that extends the
LanguageBlocEvent
. - This event will take a language entity when we select a specific language from the navigation drawer.
- Give code in the props.
Delete the InitialState as it is not required here. We'll use the new state instead of the initial state.
Create a state - LanguageLoaded
:
//1
class LanguageLoaded extends LanguageBlocState {
//2
final Locale locale;
const LanguageLoaded(this.locale);
//3
@override
List<Object> get props => [locale.languageCode];
}
- A state that extends the
LanguageBlocState
. - Based on language code, we will generate a Locale, so give a field of
Locale
type. - In
props
add the code again.
Handle the ToggleLanguageEvent
in the Bloc now.
if (event is ToggleLanguageEvent) {
yield LanguageLoaded(Locale(event.language.code));
}
- For now, use the language code coming from an event in creating the state and yield the state.
Also, you want to load a default language on Bloc initialization.
//1
super(
//2
LanguageLoaded(
//3
Locale(Languages.languages[0].code),
),
)
- Update the
super()
of the Bloc. - Pass in the
LanguageLoaded
state. - Take the first language, that is
English
/en
in our case, and create Locale from it.
By mistake, I have created language_bloc, instead of language. So kindly rename everything in event, state, and bloc files.
Register this bloc in get_it.dart:
getItInstance.registerSingleton<LanguageBloc>(LanguageBloc());
Now in movie_app.dart make the MovieApp
a stateful widget.
//1
LanguageBloc _languageBloc;
@override
void initState() {
super.initState();
//2
_languageBloc = getItInstance<LanguageBloc>();
}
@override
void dispose() {
//3
_languageBloc.close();
super.dispose();
}
- Declare the Language Bloc variable.
- In
initState
, get the instance ofLanguageBloc
fromgetItInstance
. - Don't forget to close the Bloc in the dispose method.
Now wrap MaterialApp
with BlocProvider
and BlocBuilder
:
//1
BlocProvider<LanguageBloc>.value(
value: _languageBloc,
//2
child: BlocBuilder<LanguageBloc, LanguageState>(
builder: (context, state) {
//3
if (state is LanguageLoaded) {
return MaterialApp(
//.....
//.....Rest code remains same
//4
locale: state.locale,
//.....Rest code remains same
);
}
//5
return const SizedBox.shrink();
},
),
)
- Use
BlocProvider
to provide the app withLanguageBloc
. - Use
BlocBuilder
to read theLanguageBloc
down the tree. - Now if the state is
LanguageLoaded
, you have the locale with you. BuildMaterialApp
only if you haveLanguageLoaded
state. - Instead of taking locale from one of the languages, take it from the state now. When the state changes, the locale will also change and will update all the strings in the application.
- When the state is not
LanguageLoaded
, you can return aSizedBox
.
Now on tap of English
or Spanish
in the navigation drawer, we'll dispatch the LanguageToggleEvent
. Open navigation_drawer.dart:
NavigationExpandedListItem(
title: TranslationConstants.language.t(context),
children: Languages.languages.map((e) => e.value).toList(),
//1
onPressed: (index) {
BlocProvider.of<LanguageBloc>(context).add(
ToggleLanguageEvent(
Languages.languages[index],
),
);
},
),
- First, we can get the instance of
LanguageBloc
fromBlocProvider
. Dispatch theToggleLanguageEvent
. Based on what index we get, we will take the language from the languages list.
In navigation_expanded_list_tile.dart, call the onPressed()
with index:
NavigationSubListItem(
title: children[i],
//1
onPressed: () => onPressed(i),
),
- This will call the
onPressed()
passed fromNavigationExpandedListTile
.
Also, in the previous tutorial, I added GestureDetector in the NavigationExpandedListTile
. You've to remove that. Because we are handling tap of sub-list tiles and not the main tile. Tap on the main tile is already handled using ExpansionTile
.
Now, run the application and play with switching languages.
One last thing, I forgot to mention in the last tutorial.
we have to give a color to unselectedWidgetColor
in ThemeData
because ExpansionTile uses this color when in a collapsed state.
Without this, we are unable to see the collapsed arrow on the right side of the Language tile.
unselectedWidgetColor: AppColor.royalBlue,
Now, run the application and you'll see the down arrow animating to up arrow when the expansion tile expands and vice-versa.
By this, we've added 2 languages that you can switch in the app. This was all about creating Language Management in the app. See you in the next part of the series.