diff --git a/README.md b/README.md index ff02b1e..de12e45 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,34 @@ Community: [#fluffychat:matrix.org](https://matrix.to/#/#fluffychat:matrix.org) 4. `flutter config --enable-web` 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_.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`. \ No newline at end of file diff --git a/lib/components/dialogs/new_group_dialog.dart b/lib/components/dialogs/new_group_dialog.dart index 36d4197..d04aa4e 100644 --- a/lib/components/dialogs/new_group_dialog.dart +++ b/lib/components/dialogs/new_group_dialog.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/i18n/i18n.dart'; import 'package:fluffychat/views/chat.dart'; import 'package:fluffychat/views/invitation_selection.dart'; import 'package:flutter/material.dart'; @@ -54,7 +55,7 @@ class _NewGroupDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: Text("Create new group"), + title: Text(I18n.of(context).createNewGroup), content: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -65,12 +66,12 @@ class _NewGroupDialogState extends State { textInputAction: TextInputAction.go, onSubmitted: (s) => submitAction(context), decoration: InputDecoration( - labelText: "(Optional) Group name", + labelText: I18n.of(context).optionalGroupName, icon: Icon(Icons.people), - hintText: "Enter a group name"), + hintText: I18n.of(context).enterAGroupName), ), SwitchListTile( - title: Text("Group is public"), + title: Text(I18n.of(context).groupIsPublic), value: publicGroup, onChanged: (bool b) => setState(() => publicGroup = b), ), @@ -78,14 +79,14 @@ class _NewGroupDialogState extends State { ), actions: [ FlatButton( - child: Text("Close".toUpperCase(), + child: Text(I18n.of(context).close.toUpperCase(), style: TextStyle(color: Colors.blueGrey)), onPressed: () { Navigator.of(context).pop(); }, ), FlatButton( - child: Text("Create".toUpperCase()), + child: Text(I18n.of(context).create.toUpperCase()), onPressed: () => submitAction(context), ), ], diff --git a/lib/components/dialogs/new_private_chat_dialog.dart b/lib/components/dialogs/new_private_chat_dialog.dart index 146cd57..9e1e6c3 100644 --- a/lib/components/dialogs/new_private_chat_dialog.dart +++ b/lib/components/dialogs/new_private_chat_dialog.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/avatar.dart'; +import 'package:fluffychat/i18n/i18n.dart'; import 'package:fluffychat/views/chat.dart'; import 'package:flutter/material.dart'; import 'package:share/share.dart'; @@ -74,12 +75,10 @@ class _NewPrivateChatDialogState extends State { "limit": 1, }), ); - print(response); setState(() => loading = false); if (response == false || !(response is Map) || (response["results"]?.isEmpty ?? true)) return; - print("Set..."); setState(() { foundProfile = response["results"].first; }); @@ -89,7 +88,7 @@ class _NewPrivateChatDialogState extends State { Widget build(BuildContext context) { final String defaultDomain = Matrix.of(context).client.userID.split(":")[1]; return AlertDialog( - title: Text("New private chat"), + title: Text(I18n.of(context).newPrivateChat), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -105,23 +104,23 @@ class _NewPrivateChatDialogState extends State { onFieldSubmitted: (s) => submitAction(context), validator: (value) { if (value.isEmpty) { - return 'Please enter a matrix identifier'; + return I18n.of(context).pleaseEnterAMatrixIdentifier; } final MatrixState matrix = Matrix.of(context); String mxid = "@" + controller.text.trim(); if (mxid == matrix.client.userID) { - return "You cannot invite yourself"; + return I18n.of(context).youCannotInviteYourself; } if (!mxid.contains("@")) { - return "Make sure the identifier is valid"; + return I18n.of(context).makeSureTheIdentifierIsValid; } if (!mxid.contains(":")) { - return "Make sure the identifier is valid"; + return I18n.of(context).makeSureTheIdentifierIsValid; } return null; }, decoration: InputDecoration( - labelText: "Enter a username", + labelText: I18n.of(context).enterAUsername, icon: loading ? Container( width: 24, @@ -137,7 +136,8 @@ class _NewPrivateChatDialogState extends State { ) : Icon(Icons.account_circle), prefixText: "@", - hintText: "username:$defaultDomain", + hintText: + "${I18n.of(context).username.toLowerCase()}:$defaultDomain", ), ), ), @@ -179,7 +179,7 @@ class _NewPrivateChatDialogState extends State { onTap: () => Share.share( "https://matrix.to/#/${Matrix.of(context).client.userID}"), title: Text( - "Your own username:", + "${I18n.of(context).yourOwnUsername}:", style: TextStyle( color: Colors.blueGrey, fontSize: 12, @@ -194,14 +194,14 @@ class _NewPrivateChatDialogState extends State { ), actions: [ FlatButton( - child: Text("Close".toUpperCase(), + child: Text(I18n.of(context).close.toUpperCase(), style: TextStyle(color: Colors.blueGrey)), onPressed: () { Navigator.of(context).pop(); }, ), FlatButton( - child: Text("Continue".toUpperCase()), + child: Text(I18n.of(context).confirm.toUpperCase()), onPressed: () => submitAction(context), ), ], diff --git a/lib/i18n/i18n.dart b/lib/i18n/i18n.dart new file mode 100644 index 0000000..d5b2b71 --- /dev/null +++ b/lib/i18n/i18n.dart @@ -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 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(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"); +} diff --git a/lib/i18n/intl_de.arb b/lib/i18n/intl_de.arb new file mode 100644 index 0000000..bb6156e --- /dev/null +++ b/lib/i18n/intl_de.arb @@ -0,0 +1,9 @@ +{ + "@@last_modified": "2020-01-20T09:30:07.159333", + "title": "FluffyChat", + "@title": { + "description": "Title for the application", + "type": "text", + "placeholders": {} + } +} \ No newline at end of file diff --git a/lib/i18n/intl_messages.arb b/lib/i18n/intl_messages.arb new file mode 100644 index 0000000..2cee095 --- /dev/null +++ b/lib/i18n/intl_messages.arb @@ -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": {} + } +} \ No newline at end of file diff --git a/lib/i18n/messages_all.dart b/lib/i18n/messages_all.dart new file mode 100644 index 0000000..1a72961 --- /dev/null +++ b/lib/i18n/messages_all.dart @@ -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 LibraryLoader(); +Map _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 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); +} diff --git a/lib/i18n/messages_de.dart b/lib/i18n/messages_de.dart new file mode 100644 index 0000000..6ac5e58 --- /dev/null +++ b/lib/i18n/messages_de.dart @@ -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 args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'de'; + + final messages = _notInlinedMessages(_notInlinedMessages); + static _notInlinedMessages(_) => { + + }; +} diff --git a/lib/i18n/messages_messages.dart b/lib/i18n/messages_messages.dart new file mode 100644 index 0000000..0bb624a --- /dev/null +++ b/lib/i18n/messages_messages.dart @@ -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 args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'messages'; + + final messages = _notInlinedMessages(_notInlinedMessages); + static _notInlinedMessages(_) => { + + }; +} diff --git a/lib/main.dart b/lib/main.dart index 45d7738..a246dff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/views/sign_up.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'components/matrix.dart'; import 'views/chat_list.dart'; @@ -46,6 +47,15 @@ class App extends StatelessWidget { iconTheme: IconThemeData(color: Colors.black), ), ), + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en'), // English + const Locale('de'), // German + ], home: Builder( builder: (BuildContext context) => FutureBuilder( future: Matrix.of(context).client.onLoginStateChanged.stream.first, diff --git a/pubspec.lock b/pubspec.lock index e582c26..dd06d00 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile 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: dependency: transitive description: @@ -71,6 +85,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.1" cupertino_icons: dependency: "direct main" description: @@ -78,6 +99,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.3" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.3" famedlysdk: dependency: "direct main" description: @@ -127,6 +155,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.9.1+2" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_speed_dial: dependency: "direct main" description: @@ -144,6 +177,20 @@ packages: description: flutter source: sdk 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: dependency: transitive description: @@ -172,6 +219,27 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: @@ -214,6 +282,27 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: @@ -256,6 +345,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" quiver: dependency: transitive description: @@ -394,6 +490,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+13" webview_flutter: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d81f6cf..1227ba2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,11 @@ dependencies: share: ^0.6.3+5 receive_sharing_intent: ^1.3.2 + intl: ^0.16.0 + intl_translation: ^0.17.9 + flutter_localizations: + sdk: flutter + dev_dependencies: flutter_test: sdk: flutter