Supporting multiple languages for constant strings in Flutter - flutter

I would like to start putting all my constant strings (like labels etc.) into a place that can be translated at a later stage.
How is this handled in Flutter?

Create a Localizations.dart file
Add the following code to that file:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show SynchronousFuture;
class DemoLocalizations {
DemoLocalizations(this.locale);
final Locale locale;
static DemoLocalizations of(BuildContext context) {
return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
}
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'title': 'App title',
'googleLogin': 'Login with Google'
},
'es': {
'title': 'Título de App',
'googleLogin': 'Conectar con Google'
},
};
String get title {
return _localizedValues[locale.languageCode]['title'];
}
String get googleLogin {
return _localizedValues[locale.languageCode]['googleLogin'];
}
}
class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations> {
const DemoLocalizationsDelegate();
#override
bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode);
#override
Future<DemoLocalizations> load(Locale locale) {
// Returning a SynchronousFuture here because an async "load" operation
// isn't needed to produce an instance of DemoLocalizations.
return new SynchronousFuture<DemoLocalizations>(new DemoLocalizations(locale));
}
#override
bool shouldReload(DemoLocalizationsDelegate old) => false;
}
Import Localizations.dart into the file where you use the strings.
Add the delegate DemoLocalizationsDelegate in the MaterialApp
MaterialApp(
localizationsDelegates: [
MyLocalizationsDelegate(),
],
...
)
Substitute new Text("App Title"), with new Text(DemoLocalizations.of(context).title),
For each new string you want to localize, you need to add the translated text to each language's map and then add the String get... line.
It's a bit cumbersome but it does what you need it to.
This is a quick overview of one way of doing it.
You can read more about it in the Flutter docs: https://flutter.io/tutorials/internationalization/

I asked on gitter and I got the following:
Translation/Internationalization isn't a feature we consider "done"
yet. https://pub.dartlang.org/packages/intl works in Flutter. We have
a bug tracking this more generally: flutter/flutter#393
More complete internationalization (i18n) and accessibility support
are two of the big arcs of work ahead of Flutter in the coming months.
Another example of i18n work we have planned, is completing
Right-to-left (RTL) layouts for our provided Widgets (e.g. teaching
the Material library's Scaffold to place the Drawer on the left when
the locale is an RTL language). RTL Text support works today, but
there are no widgets which are out-of-the-box RTL-layout aware at this
moment.

Related

Flutter Hive opens existing box, but is not reading values from it

I am having one of those programming moments where I think I am going mad so hopefully someone can help me.
I have a Flutter app that uses Hive to store data between runs. When the app initially starts, it opens a box and retrieves some information to set the saved theme for the MaterialApp. It then builds the main page for the app and retrieves a range of other options. This was working perfectly (I have a version of it on my phone that works perfectly), but it has stopped working for some reason.
When the app executes, the initial MyApp states that the Hive box is open, but it has no values in it. This is true for a call to an options class to retrieve the options data. After that call, the box suddenly does have values and I am able to retrieve and print out the keys. When the app then builds the main page, it states that the box is open and it has values and is able to retrieve the options data from the options class. Previously, I have had no problems with the first reading of data to extract the theme. I have posted the relevant sections of code below long with the print output from a run.
I am running the app in web and have also run it on a mobile emulator. It has previously worked fine on both platforms, but is now not working on the web platform. It appears to be working fine on the mobile emulator.
The app is using the following versions:
Flutter 2.10.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision c860cba910 (6 days ago) • 2022-03-25 00:23:12 -0500
Engine • revision 57d3bac3dd
Tools • Dart 2.16.2 • DevTools 2.9.2
pubspec.yaml dependencies:
hive: ^2.0.6
hive_flutter: ^1.1.0
I have upgraded to the latest version of Flutter today to see if that fixed the problem. I had the same issue on the previous stable release.
I have updated to hive 2.1.0 and get the same problem/output.
I have also tried downgrading Flutter to 2.10.0 with Dart 2.16.0, which I know worked fine, and that hasn't solved the problem.
main.dart
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:responsive_sizer/responsive_sizer.dart';
import 'package:lettercloud/data/colours.dart';
import 'package:lettercloud/options/option_page.dart';
const String _boxName = 'lettercloud';
void main() async {
await Hive.initFlutter();
Hive
..registerAdapter(CellAdapter())
..registerAdapter(ThemeModeOptionAdapter());
await Hive.openBox(_boxName);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
final Box _box = Hive.box(_boxName); // Object for hive data access
final Options _options = Options();
final Colours _colours = Colours();
late bool _firstRun = true; // Flag to only read Hive options on first run
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
print('build() Before first run. Extracting box keys. Attempt 1...');
for (String key in _box.keys) {
print('box keys: $key');
}
if (_firstRun) {
print(
'First run. Hive box is open: ${_box.isOpen} Box has values: ${_box.isNotEmpty}');
_options.setHiveBox(_box); // Pass hive object and retrieve options
_firstRun = false;
}
print('');
print('build() After first run. Extracting box keys. Attempt 2...');
for (String key in _box.keys) {
print('box keys: $key');
}
return AnimatedBuilder(
animation: _options.getThemeNotifier(),
builder: (context, child) {
return MaterialApp(
title: 'Lettercloud',
theme: FlexThemeData.light(scheme: FlexScheme.jungle),
darkTheme: FlexThemeData.dark(scheme: FlexScheme.jungle),
themeMode: _options.getThemeMode(),
home: ResponsiveSizer(
builder: (context, orientation, screenType) {
return const MyPage(title: 'Lettercloud Anagram Helper');
},
),
);
});
}
}
class MyPage extends StatefulWidget {
const MyPage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyPage> createState() => MyPageState();
}
class MyPageState extends State<MyPage> {
final Options _options = Options();
late final Box _box; // Object for hive data access
late Widget _displayGrid;
#override
void initState() {
super.initState();
print('Doing init MyPageState');
_box = Hive.box(_boxName);
_options.setHiveBox(_box); // Pass hive object and retrieve options
_setGrid(_options.getGridType());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Lettercloud'),
),
resizeToAvoidBottomInset: false, // Overlay on-screen keyboard
body: SafeArea(
child: _displayGrid,
),
);
}
// Set the grid to display based on the grid type option
void _setGrid(GridType type) {
_displayGrid = _options.getGridType() == GridType.square
? GridSquare(box: _box, options: _options, update: updateGrid)
: GridDiamond(box: _box, options: _options, update: updateGrid);
}
// Callback to set the grid type if the option changes
void updateGrid(GridType type) {
setState(() {
_setGrid(type);
});
}
}
options.dart
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:hive_flutter/hive_flutter.dart';
class Options {
bool _lightMode = true; // Use light colours, or dark
static const String _lightModeName = 'lightMode';
bool _showGrid = true; // Show grid around tiles, or not
static const String _showGridName = 'showGrid';
bool _firstEdit =
true; // Flag to show edit on first start, doesn't need saving
bool _editOnStart = false; // Show edit at startup, or not
static const String _editOnStartName = 'editOnStart';
CharType _charType = CharType.mixed; // Type of letters to show
static const String _charTypeName = 'charType';
ThemeModeOption _themeMode = ThemeModeOption()..setMode(ThemeMode.light);
static const String _themeModeName = 'themeMode';
late Box _box; // Hive object
late final double _tabletF; // Reduction factor for tablet displays
late GridType _gridType = GridType.square;
static const String _gridTypeName = 'gridType';
late GridType _savedGridType = _gridType;
static const String _savedGridTypeName = 'savedGridType';
// last page name - used to control text entry on startup
String _lastPage = PageName.main.toString();
static const String _lastPageName = 'lastPageName';
// Flag to show if the grid type has change. Used to prevent 'show on start'
// triggering the text entry box after the grid layout has been changed by the user
bool _backFromOptionsPage = false;
final String _backFromOptionsPageName = 'fromOptions';
///
/// Hive management methods and global options setting
///
void setHiveBox(Box b) {
_box = b; // Pass the hive management object
print(
'Options hive box. Box is open: ${_box.isOpen} Box has values: ${_box.isNotEmpty}.');
// Set screen size factor for web vs tablet
if (kIsWeb) {
_tabletF = 0.4; // Factor components by 0.4 for web
} else {
_tabletF = 0.6; // Factor components by 0.6 for tablets
}
// Retrieve any option data values
if (_box.get(_lightModeName) != null) {
_lightMode = _box.get(_lightModeName);
} else {
print('Cannot find $_lightModeName');
_box.put(_lightModeName, _lightMode);
}
if (_box.get(_showGridName) != null) {
_showGrid = _box.get(_showGridName);
} else {
_box.put(_showGridName, _showGrid);
}
if (_box.get(_editOnStartName) != null) {
_editOnStart = _box.get(_editOnStartName);
} else {
_box.put(_editOnStartName, _editOnStart);
}
if (_box.get(_charTypeName) != null) {
String temp = _box.get(_charTypeName);
_charType = getCharEnum(temp);
} else {
_box.put(_charTypeName, _charType.toString());
}
if (_box.get(_themeModeName) != null) {
_themeMode = _box.get(_themeModeName);
} else {
_box.put(_themeModeName, _themeMode);
}
if (_box.get(_gridTypeName) != null) {
String temp = _box.get(_gridTypeName);
_gridType = getGridEnum(temp);
} else {
_box.put(_gridTypeName, _gridType.toString());
}
if (_box.get(_savedGridTypeName) != null) {
String temp = _box.get(_savedGridTypeName);
_savedGridType = getGridEnum(temp);
} else {
_box.put(_savedGridTypeName, _savedGridType.toString());
}
if (_box.get(_backFromOptionsPageName) != null) {
_box.put(_backFromOptionsPageName, _backFromOptionsPage);
} else {
_box.put(_backFromOptionsPageName, _backFromOptionsPage);
}
// Load last page value or reset if doesn't exit
if (_box.get(_lastPageName) != null) {
_box.put(_lastPageName, _lastPage);
} else {
_box.put(_lastPageName, _lastPage);
}
_box.flush(); // Make sure everything is written to the disk
}
}
Command line output:
flutter run -d chrome --web-renderer html --web-port 5555
Launching lib\main.dart on Chrome in debug mode...
Waiting for connection from debug service on Chrome... 18.6s
This app is linked to the debug service: ws://127.0.0.1:54752/JAXqfQgauf4=/ws
Debug service listening on ws://127.0.0.1:54752/JAXqfQgauf4=/ws
Running with sound null safety
To hot restart changes while running, press "r" or "R".
For a more detailed help message, press "h". To quit, press "q".
An Observatory debugger and profiler on Chrome is available at: http://127.0.0.1:54752/JAXqfQgauf4=
The Flutter DevTools debugger and profiler on Chrome is available at:
http://127.0.0.1:9101?uri=http://127.0.0.1:54752/JAXqfQgauf4=
build() Before first run. Extracting box keys. Attempt 1...
First run. Hive box is open: true Box has values: false
Options hive box. Box is open: true Box has values: false.
Cannot find lightMode
build() After first run. Extracting box keys. Attempt 2...
box keys: charType
box keys: editOnStart
box keys: fromOptions
box keys: gridType
box keys: lastPageName
box keys: lightMode
box keys: savedGridType
box keys: showGrid
box keys: themeMode
Doing init MyPageState
Options hive box. Box is open: true Box has values: true.
Application finished.
Update #1
Since originally posting I have tried deleting the box and re-running the app in case this was caused by a corrupt file. That hasn't made any difference.
I have also tried adding a .then to the openBox() command in case this is yet another async programming issue, but that hasn't made a difference either, i.e.
await Hive.openBox(_boxName).then((value) {
print('value is $value');
runApp(MyApp());
});
Update #2
So, it took me a while to work this out, but I create my box values on the first run if they don't already exist (to address the use case of the first ever run of the app). If I remove all the put statements in the setHiveBox() method then I get the problem consistently. In other words, there are no values in the box until my Options class creates them when the app runs. This suggests that the data is not being saved to the disk by app. I have compared both main.dart and options.dart with last known working versions and can't see any obvious differences. What could stop the application from saving the data to the disk? Just to note, I have tested another app I developed that uses Hive and this continues to work perfectly. That uses the same version of Hive as this app does.
I fixed this by doing a flutter clean on the project, deleting the flutter installation (deleting the install folder from the disk completely), downloading and re-installing flutter and then doing a flutter pub get on the project folder.
I had previously tried a flutter clean and flutter pub get on their own and this didn't fix the problem so maybe something had gone wrong in the flutter folder itself after the last upgrade? Anyway, a clean install of everything has solved the problem.

Flutter Google Calendar API - clientViaServiceAccount not responding

I know that there are already answers to this question but I couldn't solve my issue looking at them, so I hope you will be able to help me or at least suggest me what to try because I am not sure what to do or how to debug in order to solve my issue.
I am trying to list all the events in my company calendar using Google Calendar Api.
I used the same code on this question: Using dart and flutter with google calendar api to get a list of events on the a user's calendar
And I followed all the 6 steps of the answer in the question above.
For some reason, the call clientViaServiceAccount do not return any result and any errors.
It seems like it is executing an infinity loop.
This is the code I am using, and I can only see printed "getCalendarEvents". I cannot see the msg "HERE" or any error printed. So The issue is for sure in clientViaServiceAccount.
I edited the code taking into account Iamblichus suggestion, but the issue is that I cannot even reach the CalendarApi. The code is stacked on the clientViaServiceAccount.
import 'package:flutter/material.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:googleapis/calendar/v3.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
CalendarAPI.getCalendarEvents();
return MaterialApp(
title: 'Flutter Demo',
home: Container(),
);
}
}
class CalendarAPI {
static final _accountCredentials = new ServiceAccountCredentials.fromJson(r'''
{
"private_key_id": myPrivatekeyId,
"private_key": myPrivateKey,
"client_email": myClientEmail,
"client_id": myClientId,
"type": "service_account"
}
''');
static final _scopes = [CalendarApi.CalendarScope];
static void getCalendarEvents() {
print('getCalendarEvents');
clientViaServiceAccount(_accountCredentials, _scopes).then((client) {
print('HERE');
var calendar = new CalendarApi(client);
print(calendar);
// Added iamblichus answer
var calendarListEntry = CalendarListEntry();
calendarListEntry.id = calendarId;
calendar.calendarList.insert(calendarListEntry).then((_) {
print('CALENDAR ADDED');
var calEvents = calendar.events.list(calendarId);
calEvents.then((Events events) {
events.items.forEach((Event event) {
print(event.summary);
});
}).catchError((e) => print(e));
}).catchError((e) => print(e));
}).catchError((e) => print(e));
}
}
EDIT FOR ANSWERING iamblichus COMMENT on Jul 15 at 12:31
I created the Service account Credentials on the page of the image shown below.
As soon as I created the key, I file .json was downloaded with the credentials.
I copied the content of that file and paste into ServiceAccountCredentials.fromJson function, so the credentials cannot be wrong.
And even if the credentials were wrong, why cannot I see an error that clientViaServiceAccount call is failing?
I am catching any error and print them in the screen with the last }).catchError((e) => print(e));.
For some reason the call clientViaServiceAccount is not doing anything and I cannot understand how to find the reason for that.
This method works in the cloud. If you want to use it in the flutter app, you have to get the user authenticated using the following plugin,
extension_google_sign_in_as_googleapis_auth

Extracting Class Members like Widget Builders to a Different File?

In developing some of the screens for my flutter app, I regularly need to dynamically render widgets based on the state of the screen. For circumstances where it makes sense to create a separate widget and include it, I do that.
However, there are many use cases where what I need to render is not fit for a widget, and leverages existing state from the page. Therefore I use builder methods to render the appropriate widgets to the page. As anyone who uses Flutter knows, that can lead to lengthy code where you need to scroll up/down a lot to get to what you need to work on.
For better maintainability, I would love to move those builder methods into separate files, and then just include them. This would make it much easier to work on specific code widgets rendered and make the screen widget much cleaner.
But I haven't found a proper way to extract that dynamic widget code, which makes use of state, calls to update state, etc. I'm looking for a type of "include" file that would insert code into the main screen and render as if it's part of the core code.
Is this possible? How to achieve?
With the introduction of extension members, I came across this really neat way of achieving exactly what your described!
Say you have a State class defined like this:
class MyWidgetState extends State<MyWidget> {
int cakes;
#override
void initState() {
super.initState();
cakes = 0;
}
#override
Widget build(BuildContext context) {
return Builder(
builder: (context) => Text('$cakes'),
);
}
}
As you can see, there is a local variable cakes and a builder function. The very neat way to extract this builder now is the following:
extension CakesBuilderExtension on MyWidgetState {
Widget cakesBuilder(BuildContext context) {
return Text('$cakes');
}
}
Now, the cakes member can be accessed from the extension even if the extension is placed in another file.
Now, you would update your State class like this (the builder changed):
class MyWidgetState extends State<MyWidget> {
int cakes;
#override
void initState() {
super.initState();
cakes = 0;
}
#override
Widget build(BuildContext context) {
return Builder(
builder: cakesBuilder,
);
}
}
The cakesBuilder can be referenced from MyWidgetState, even though it is only declared in the CakesBuilderExtension!
Note
The extension feature requires Dart 2.6. This is not yet available in the stable channel, but should be around the end of 2019 I guess. Thus, you need to use the dev or master channels: flutter channel dev or flutter channel master and update the environment constraint in your pubspec.yaml file:
environment:
sdk: '>=2.6.0-dev.8.2 <3.0.0'

Add localization to Widget without using MaterialApp?

I understand how to add localization to an app by adding localizationsDelegates and supportedLocales to the MaterialApp widget. Localizing my app is working fine.
I'm creating a Flutter package that can be used within other Flutter apps. Some of the widgets within the package need to have localized text, like some of the error messages and button labels. The package contains all of its own localized strings. How can I localize the strings in my package without MaterialApp?
I've used Localizations widget in this way for testing without using a MaterialApp:
A simple demo widget:
class WidgetToTest extends StatelessWidget {
#override
Widget build(BuildContext context) {
//AppLocalizations.of(context)!.hello = 'hello' in generated file
return Text(AppLocalizations.of(context)!.hello);
}
}
The test:
testWidgets('test localizations widget', (tester) async {
await tester.pumpWidget(
Localizations(
locale: const Locale('en'),
delegates: AppLocalizations.localizationsDelegates,
child: WidgetToTest(),
)
);
expect(find.text('hello'), findsOneWidget);
});
You can use Localizations widget if you just need to enable AppLocalizations to work inside your widgets.

Use multiple LocalizationsDelegates in Flutter

I'm facing an issue where I'm trying to use multiple LocalizationsDelegates in a MaterialApp.
I'm using the Dart intl tools to provide translations to my labels. When I have multiple LocalizationsDelegates only the one that is specified the first gets the translated values. The labels of the next delegate, get the default value provided in the Intl.message() function.
Short, self contained, correct example
I've set up a minimal project as an example of this issue on GitHub.
Code snippets
In the MaterialApp, I define a bunch of localizationsDelegates, including two app specific ones: DogLocalizationsDelegate and CatLocalizationsDelegate.
MaterialApp(
// other properties
locale: Locale("en"),
localizationsDelegates: [
CatLocalizationsDelegate(),
DogLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
const Locale('en'),
const Locale('nl'),
],
);
The delegates have the same boilerplate code, but provide different labels.
Here's how the DogLocalizations and its DogLocalizationsDelegate look like.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'messages_all.dart';
class DogLocalizations {
static Future<DogLocalizations> load(Locale locale) {
final String name = locale.languageCode;
final String localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
return DogLocalizations();
});
}
static DogLocalizations of(BuildContext context) {
return Localizations.of<DogLocalizations>(context, DogLocalizations);
}
String get bark {
return Intl.message(
'<insert dog sound>',
name: 'bark',
);
}
}
class DogLocalizationsDelegate extends LocalizationsDelegate<DogLocalizations> {
const DogLocalizationsDelegate();
#override
bool isSupported(Locale locale) => ['en', 'nl'].contains(locale.languageCode);
#override
Future<DogLocalizations> load(Locale locale) => DogLocalizations.load(locale);
#override
bool shouldReload(DogLocalizationsDelegate old) => false;
}
The CatLocalizations are the same, but with a meow String getter. Full example in the GitHub project.
Commands used to generate translations files
I'm using multiple extraction and generation commands instead of having multiple files in one command. This is because I'm actually having this problem with a library (with its own labels) and a consumer of that library (that also has its own labels).
Extract labels of both cats and dogs
flutter pub run intl_translation:extract_to_arb --output-dir=lib/cat_labels/gen lib/cat_labels/CatLabels.dart
flutter pub run intl_translation:extract_to_arb --output-dir=lib/dog_labels/gen lib/dog_labels/DogLabels.dart
Translate the generated intl_messages.arb to have two language files
intl_en.arb
intl_nl.arb
And then add the correct translated values to these files.
Generate the dart files from ARB
flutter pub run intl_translation:generate_from_arb --output-dir=lib/cat_labels lib/cat_labels/CatLabels.dart lib/cat_labels/gen/intl_*.arb
flutter pub run intl_translation:generate_from_arb --output-dir=lib/dog_labels lib/dog_labels/DogLabels.dart lib/dog_labels/gen/intl_*.arb
Issue
In this demo project, having the following order of delegates:
// main.dart (line 20)
DogLocalizationsDelegate(),
CatLocalizationsDelegate(),
will give the translation for the bark label, but not for the meow label.
When switching it:
// main.dart (line 20)
CatLocalizationsDelegate(),
DogLocalizationsDelegate(),
will give the translation for the meow label, but not for the bark label.
Why multiple localization delegates
In case you're wondering why: I'm using labels in a library and in the consumer apps of that library.
Important to know is that it's not (really) possible to specify both localization files in the same generator command because of this.
From what I learned, no, you can’t use multiple localizations delegates like this. That is because intl’s initializeMessages can only be called once per locale.
So in your example, once your CatLocalizationsDelegate runs initializeMessages, the one for DogLocalizationsDelegate has no effect. That’s why you are only seeing the translation Meow! but not Dog’s, or whichever one gets to run it first.
For additional reading, check out https://phrase.com/blog/posts/how-to-internationalize-a-flutter-app/ and please share feedback at https://github.com/flutter/flutter/issues/41437.
I find a solution with a dedicated flutter package available here: [https://pub.dev/packages/multiple_localization][1]
You can use the multiple_localizations package. it worked for me