Add localizations system

This commit is contained in:
Christian Pauly 2020-01-20 09:50:49 +01:00
parent 4f4e3a4df5
commit 1f230c0a63
12 changed files with 437 additions and 18 deletions

View file

@ -23,3 +23,34 @@ Community: [#fluffychat:matrix.org](https://matrix.to/#/#fluffychat:matrix.org)
4. `flutter config --enable-web` 4. `flutter config --enable-web`
5. `flutter run` 5. `flutter run`
## How to add translations for your language
1. Replace the non-translated string in the codebase:
```
Text("Hello world"),
```
with a method call:
```
Text(I18n.of(context).helloWorld),
```
And add the method to `/lib/i18n/i18n.dart`:
```
String get helloWorld => Intl.message('Hello world');
```
2. Add the string to the .arb files with this command:
```
flutter pub run intl_translation:extract_to_arb --output-dir=lib/i18n lib/i18n/i18n.dart
```
3. Copy the new translation objects from `/lib/i18n/intl_message.arb` to `/lib/i18n/intl_<yourlanguage>.arb` and translate it or create a new file for your language by copying `intl_message.arb`.
4. Update the translations with this command:
```
flutter pub run intl_translation:generate_from_arb \
--output-dir=lib/i18n --no-use-deferred-loading \
lib/main.dart lib/i18n/intl_*.arb
```
5. Make sure your language is in `supportedLocales` in `/lib/main.dart`.

View file

@ -1,3 +1,4 @@
import 'package:fluffychat/i18n/i18n.dart';
import 'package:fluffychat/views/chat.dart'; import 'package:fluffychat/views/chat.dart';
import 'package:fluffychat/views/invitation_selection.dart'; import 'package:fluffychat/views/invitation_selection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -54,7 +55,7 @@ class _NewGroupDialogState extends State<NewGroupDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text("Create new group"), title: Text(I18n.of(context).createNewGroup),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@ -65,12 +66,12 @@ class _NewGroupDialogState extends State<NewGroupDialog> {
textInputAction: TextInputAction.go, textInputAction: TextInputAction.go,
onSubmitted: (s) => submitAction(context), onSubmitted: (s) => submitAction(context),
decoration: InputDecoration( decoration: InputDecoration(
labelText: "(Optional) Group name", labelText: I18n.of(context).optionalGroupName,
icon: Icon(Icons.people), icon: Icon(Icons.people),
hintText: "Enter a group name"), hintText: I18n.of(context).enterAGroupName),
), ),
SwitchListTile( SwitchListTile(
title: Text("Group is public"), title: Text(I18n.of(context).groupIsPublic),
value: publicGroup, value: publicGroup,
onChanged: (bool b) => setState(() => publicGroup = b), onChanged: (bool b) => setState(() => publicGroup = b),
), ),
@ -78,14 +79,14 @@ class _NewGroupDialogState extends State<NewGroupDialog> {
), ),
actions: <Widget>[ actions: <Widget>[
FlatButton( FlatButton(
child: Text("Close".toUpperCase(), child: Text(I18n.of(context).close.toUpperCase(),
style: TextStyle(color: Colors.blueGrey)), style: TextStyle(color: Colors.blueGrey)),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
FlatButton( FlatButton(
child: Text("Create".toUpperCase()), child: Text(I18n.of(context).create.toUpperCase()),
onPressed: () => submitAction(context), onPressed: () => submitAction(context),
), ),
], ],

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/components/avatar.dart'; import 'package:fluffychat/components/avatar.dart';
import 'package:fluffychat/i18n/i18n.dart';
import 'package:fluffychat/views/chat.dart'; import 'package:fluffychat/views/chat.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:share/share.dart'; import 'package:share/share.dart';
@ -74,12 +75,10 @@ class _NewPrivateChatDialogState extends State<NewPrivateChatDialog> {
"limit": 1, "limit": 1,
}), }),
); );
print(response);
setState(() => loading = false); setState(() => loading = false);
if (response == false || if (response == false ||
!(response is Map) || !(response is Map) ||
(response["results"]?.isEmpty ?? true)) return; (response["results"]?.isEmpty ?? true)) return;
print("Set...");
setState(() { setState(() {
foundProfile = response["results"].first; foundProfile = response["results"].first;
}); });
@ -89,7 +88,7 @@ class _NewPrivateChatDialogState extends State<NewPrivateChatDialog> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final String defaultDomain = Matrix.of(context).client.userID.split(":")[1]; final String defaultDomain = Matrix.of(context).client.userID.split(":")[1];
return AlertDialog( return AlertDialog(
title: Text("New private chat"), title: Text(I18n.of(context).newPrivateChat),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -105,23 +104,23 @@ class _NewPrivateChatDialogState extends State<NewPrivateChatDialog> {
onFieldSubmitted: (s) => submitAction(context), onFieldSubmitted: (s) => submitAction(context),
validator: (value) { validator: (value) {
if (value.isEmpty) { if (value.isEmpty) {
return 'Please enter a matrix identifier'; return I18n.of(context).pleaseEnterAMatrixIdentifier;
} }
final MatrixState matrix = Matrix.of(context); final MatrixState matrix = Matrix.of(context);
String mxid = "@" + controller.text.trim(); String mxid = "@" + controller.text.trim();
if (mxid == matrix.client.userID) { if (mxid == matrix.client.userID) {
return "You cannot invite yourself"; return I18n.of(context).youCannotInviteYourself;
} }
if (!mxid.contains("@")) { if (!mxid.contains("@")) {
return "Make sure the identifier is valid"; return I18n.of(context).makeSureTheIdentifierIsValid;
} }
if (!mxid.contains(":")) { if (!mxid.contains(":")) {
return "Make sure the identifier is valid"; return I18n.of(context).makeSureTheIdentifierIsValid;
} }
return null; return null;
}, },
decoration: InputDecoration( decoration: InputDecoration(
labelText: "Enter a username", labelText: I18n.of(context).enterAUsername,
icon: loading icon: loading
? Container( ? Container(
width: 24, width: 24,
@ -137,7 +136,8 @@ class _NewPrivateChatDialogState extends State<NewPrivateChatDialog> {
) )
: Icon(Icons.account_circle), : Icon(Icons.account_circle),
prefixText: "@", prefixText: "@",
hintText: "username:$defaultDomain", hintText:
"${I18n.of(context).username.toLowerCase()}:$defaultDomain",
), ),
), ),
), ),
@ -179,7 +179,7 @@ class _NewPrivateChatDialogState extends State<NewPrivateChatDialog> {
onTap: () => Share.share( onTap: () => Share.share(
"https://matrix.to/#/${Matrix.of(context).client.userID}"), "https://matrix.to/#/${Matrix.of(context).client.userID}"),
title: Text( title: Text(
"Your own username:", "${I18n.of(context).yourOwnUsername}:",
style: TextStyle( style: TextStyle(
color: Colors.blueGrey, color: Colors.blueGrey,
fontSize: 12, fontSize: 12,
@ -194,14 +194,14 @@ class _NewPrivateChatDialogState extends State<NewPrivateChatDialog> {
), ),
actions: <Widget>[ actions: <Widget>[
FlatButton( FlatButton(
child: Text("Close".toUpperCase(), child: Text(I18n.of(context).close.toUpperCase(),
style: TextStyle(color: Colors.blueGrey)), style: TextStyle(color: Colors.blueGrey)),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
FlatButton( FlatButton(
child: Text("Continue".toUpperCase()), child: Text(I18n.of(context).confirm.toUpperCase()),
onPressed: () => submitAction(context), onPressed: () => submitAction(context),
), ),
], ],

62
lib/i18n/i18n.dart Normal file
View file

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'messages_all.dart';
class I18n {
I18n(this.localeName);
static Future<I18n> load(Locale locale) {
final String name =
locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
final String localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
return I18n(localeName);
});
}
static I18n of(BuildContext context) {
return Localizations.of<I18n>(context, I18n);
}
final String localeName;
/* <=============> Translations <=============> */
String get close => Intl.message("Close");
String get confirm => Intl.message("Confirm");
String get create => Intl.message("Create");
String get createNewGroup => Intl.message("Create new group");
String get enterAGroupName => Intl.message("Enter a group name");
String get enterAUsername => Intl.message("Enter a username");
String get groupIsPublic => Intl.message("Group is public");
String get makeSureTheIdentifierIsValid =>
Intl.message("Make sure the identifier is valid");
String get newPrivateChat => Intl.message("New private chat");
String get optionalGroupName => Intl.message("(Optional) Group name");
String get pleaseEnterAMatrixIdentifier =>
Intl.message('Please enter a matrix identifier');
String get title => Intl.message(
'FluffyChat',
name: 'title',
desc: 'Title for the application',
locale: localeName,
);
String get username => Intl.message("Username");
String get youCannotInviteYourself =>
Intl.message("You cannot invite yourself");
String get yourOwnUsername => Intl.message("Your own username");
}

9
lib/i18n/intl_de.arb Normal file
View file

@ -0,0 +1,9 @@
{
"@@last_modified": "2020-01-20T09:30:07.159333",
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
"type": "text",
"placeholders": {}
}
}

View file

@ -0,0 +1,79 @@
{
"@@last_modified": "2020-01-20T09:50:21.386565",
"Close": "Close",
"@Close": {
"type": "text",
"placeholders": {}
},
"Confirm": "Confirm",
"@Confirm": {
"type": "text",
"placeholders": {}
},
"Create": "Create",
"@Create": {
"type": "text",
"placeholders": {}
},
"Create new group": "Create new group",
"@Create new group": {
"type": "text",
"placeholders": {}
},
"Enter a group name": "Enter a group name",
"@Enter a group name": {
"type": "text",
"placeholders": {}
},
"Enter a username": "Enter a username",
"@Enter a username": {
"type": "text",
"placeholders": {}
},
"Group is public": "Group is public",
"@Group is public": {
"type": "text",
"placeholders": {}
},
"Make sure the identifier is valid": "Make sure the identifier is valid",
"@Make sure the identifier is valid": {
"type": "text",
"placeholders": {}
},
"New private chat": "New private chat",
"@New private chat": {
"type": "text",
"placeholders": {}
},
"(Optional) Group name": "(Optional) Group name",
"@(Optional) Group name": {
"type": "text",
"placeholders": {}
},
"Please enter a matrix identifier": "Please enter a matrix identifier",
"@Please enter a matrix identifier": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
"type": "text",
"placeholders": {}
},
"Username": "Username",
"@Username": {
"type": "text",
"placeholders": {}
},
"You cannot invite yourself": "You cannot invite yourself",
"@You cannot invite yourself": {
"type": "text",
"placeholders": {}
},
"Your own username": "Your own username",
"@Your own username": {
"type": "text",
"placeholders": {}
}
}

View file

@ -0,0 +1,67 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that looks up messages for specific locales by
// delegating to the appropriate library.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:implementation_imports, file_names, unnecessary_new
// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
// ignore_for_file:argument_type_not_assignable, invalid_assignment
// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
// ignore_for_file:comment_references
import 'dart:async';
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart';
import 'messages_de.dart' as messages_de;
import 'messages_messages.dart' as messages_messages;
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'de': () => new Future.value(null),
'messages': () => new Future.value(null),
};
MessageLookupByLibrary _findExact(String localeName) {
switch (localeName) {
case 'de':
return messages_de.messages;
case 'messages':
return messages_messages.messages;
default:
return null;
}
}
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) async {
var availableLocale = Intl.verifiedLocale(
localeName,
(locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
if (availableLocale == null) {
return new Future.value(false);
}
var lib = _deferredLibraries[availableLocale];
await (lib == null ? new Future.value(false) : lib());
initializeInternalMessageLookup(() => new CompositeMessageLookup());
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new Future.value(true);
}
bool _messagesExistFor(String locale) {
try {
return _findExact(locale) != null;
} catch (e) {
return false;
}
}
MessageLookupByLibrary _findGeneratedMessagesFor(String locale) {
var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor,
onFailure: (_) => null);
if (actualLocale == null) return null;
return _findExact(actualLocale);
}

26
lib/i18n/messages_de.dart Normal file
View file

@ -0,0 +1,26 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a de locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'de';
final messages = _notInlinedMessages(_notInlinedMessages);
static _notInlinedMessages(_) => <String, Function> {
};
}

View file

@ -0,0 +1,26 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a messages locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'messages';
final messages = _notInlinedMessages(_notInlinedMessages);
static _notInlinedMessages(_) => <String, Function> {
};
}

View file

@ -2,6 +2,7 @@ import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/sign_up.dart'; import 'package:fluffychat/views/sign_up.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'components/matrix.dart'; import 'components/matrix.dart';
import 'views/chat_list.dart'; import 'views/chat_list.dart';
@ -46,6 +47,15 @@ class App extends StatelessWidget {
iconTheme: IconThemeData(color: Colors.black), iconTheme: IconThemeData(color: Colors.black),
), ),
), ),
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
const Locale('en'), // English
const Locale('de'), // German
],
home: Builder( home: Builder(
builder: (BuildContext context) => FutureBuilder<LoginState>( builder: (BuildContext context) => FutureBuilder<LoginState>(
future: Matrix.of(context).client.onLoginStateChanged.stream.first, future: Matrix.of(context).client.onLoginStateChanged.stream.first,

View file

@ -1,6 +1,20 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.39.4"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@ -71,6 +85,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -78,6 +99,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.3" version: "0.1.3"
dart_style:
dependency: transitive
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.3"
famedlysdk: famedlysdk:
dependency: "direct main" dependency: "direct main"
description: description:
@ -127,6 +155,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.1+2" version: "0.9.1+2"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_speed_dial: flutter_speed_dial:
dependency: "direct main" dependency: "direct main"
description: description:
@ -144,6 +177,20 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.0+3"
http: http:
dependency: transitive dependency: transitive
description: description:
@ -172,6 +219,27 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.2+3" version: "0.6.2+3"
intl:
dependency: "direct main"
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.0"
intl_translation:
dependency: "direct main"
description:
name: intl_translation
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.9"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1+1"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@ -214,6 +282,27 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.4" version: "0.2.4"
node_interop:
dependency: transitive
description:
name: node_interop
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
node_io:
dependency: transitive
description:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1+2"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -256,6 +345,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.2"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:
@ -394,6 +490,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.8" version: "2.0.8"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+13"
webview_flutter: webview_flutter:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -47,6 +47,11 @@ dependencies:
share: ^0.6.3+5 share: ^0.6.3+5
receive_sharing_intent: ^1.3.2 receive_sharing_intent: ^1.3.2
intl: ^0.16.0
intl_translation: ^0.17.9
flutter_localizations:
sdk: flutter
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter