diff --git a/README.md b/README.md deleted file mode 100644 index 16f26c7..0000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Official Outbag App -Source code of the official outbag app, -written in flutter. diff --git a/README.org b/README.org new file mode 100644 index 0000000..7f46d35 --- /dev/null +++ b/README.org @@ -0,0 +1,15 @@ +* Official Outbag App +Source code of the official outbag app, +written in flutter. + +** Translating +This app uses /l10n/ according to the official flutter +[[https://docs.flutter.dev/development/accessibility-and-localization/internationalization][internationalization guide]]. + +1. Check if there is a ~.arb~ file available for your language, + and add missing translations in there, + if the file exists +2. Otherwise copy the ~app_en.arb~ file + and paste it as ~arb_.arb~ +3. Edit the translations in the file +4. Run ~flutter gen-l10n~ to generate the required ~.dart~ files diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..15338f2 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart diff --git a/lib/backend/errors.dart b/lib/backend/errors.dart index fe5c2dd..2bd7b58 100644 --- a/lib/backend/errors.dart +++ b/lib/backend/errors.dart @@ -1,30 +1,32 @@ +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + /* * Tool to automatically generate english text from error */ -String errorAsString(Map json) { +String errorAsString(Map json, AppLocalizations trans) { switch (json['data']) { case 'notfound': - return 'Endpoint not found'; + return trans.errorNotFound; case 'wrongstate': - return 'Missing data'; + return trans.errorDataIncomplete; case 'data': - return 'Invalid data'; + return trans.errorDataInvalid; case 'roomAdmin': case 'right': - return 'You are not allowed to perform this action'; + return trans.errorPermissions; case 'server': - return 'Server error'; + return trans.errorServer; case 'closed': - return 'Server cannot be reached'; + return trans.errorUnreachable; case 'auth': - return 'Username or password wrong'; + return trans.errorAuth; case 'ota': - return 'Invalid OTA'; + return trans.errorInvalidOTA; case 'existence': - return 'Username unavailable'; + return trans.errorUsernameUnavailable; case 'config': - return 'Server reached user limit'; + return trans.errorServerLimit; } - return "Unknown Error"; + return trans.errorUnknown; } diff --git a/lib/backend/permissions.dart b/lib/backend/permissions.dart index 61ddaf9..2509a71 100644 --- a/lib/backend/permissions.dart +++ b/lib/backend/permissions.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // implementation of permission types // according to the server permissions.ts // https://codeberg.org/outbag/server/src/branch/dev/src/server/permissions.ts @@ -100,45 +102,49 @@ class RoomPermission { ]; } - static String name(int permission) { + static String name(int permission, BuildContext context) { + final trans = AppLocalizations.of(context); + switch (permission) { case 1: - return 'Add Articles'; + return trans!.roomPermissionAddArticles; case 2: - return 'Remove Articles'; + return trans!.roomPermissionRemoveArticles; case 4: - return 'List Groups and Items'; + return trans!.roomPermissionList; case 8: - return 'Change Room Metadata'; + return trans!.roomPermissionChangeMeta; case 16: - return 'Manage OTAs'; + return trans!.roomPermissionManageOTA; case 32: - return 'Manage Admins'; + return trans!.roomPermissionManageAdmins; case 64: - return 'Manage Members'; + return trans!.roomPermissionManageMembers; } - return "Unknown permission"; + return trans!.roomPermissionUnknown; } - static String describe(int permission) { + static String describe(int permission, BuildContext context) { + final trans = AppLocalizations.of(context); + switch (permission) { case 1: - return 'Allows users to add items to the shopping list'; + return trans!.roomPermissionAddArticlesSubtitle; case 2: - return 'Allows users to remove items from the shopping list'; + return trans!.roomPermissionRemoveArticlesSubtitle; case 4: - return 'Allows the user to view groups and products'; + return trans!.roomPermissionListSubtitle; case 8: - return 'Allows the user to edit the room name, description and icon'; + return trans!.roomPermissionChangeMetaSubtitle; case 16: - return 'Alloww the user to create, share and delete authentification tokens'; + return trans!.roomPermissionManageOTASubtitle; case 32: - return 'Allows the user to change the admin status of other members'; + return trans!.roomPermissionManageAdminsSubtitle; case 64: - return 'Allows the user to invite and kick room members'; + return trans!.roomPermissionManageMembersSubtitle; } - return "No description available"; + return trans!.roomPermissionUnknownSubtitle; } } diff --git a/lib/backend/room.dart b/lib/backend/room.dart index 67f52c7..64dfe0e 100644 --- a/lib/backend/room.dart +++ b/lib/backend/room.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:localstore/localstore.dart'; import 'package:outbag_app/tools/assets.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class RoomVisibility { final int type; @@ -28,14 +29,16 @@ class RoomVisibility { return Icons.lock; } - String get text { + String text(BuildContext context) { + final trans = AppLocalizations.of(context); + if (type == 2) { - return "Global"; + return trans!.roomVisibilityGlobal; } else if (type == 1) { - return "Local"; + return trans!.roomVisibilityLocal; } - return "Private"; + return trans!.roomVisibilityPrivate; } static List list() { diff --git a/lib/backend/themes.dart b/lib/backend/themes.dart index 2075479..69f3541 100644 --- a/lib/backend/themes.dart +++ b/lib/backend/themes.dart @@ -1,20 +1,23 @@ import 'package:flutter/material.dart'; import 'package:localstore/localstore.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class AppTheme { ThemeMode mode; AppTheme(this.mode); - String get name { - if (mode == ThemeMode.light) { - return 'Light'; + String name(BuildContext context) { + final trans = AppLocalizations.of(context); + + if (mode == ThemeMode.light) { + return trans!.themeLight; } if (mode == ThemeMode.dark) { - return 'Dark'; + return trans!.themeDark; } - return 'System'; + return trans!.themeSystem; } IconData get icon { @@ -80,5 +83,4 @@ class AppTheme { @override int get hashCode => mode.index; - } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..48fca22 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,287 @@ +{ + "helloWorld": "Hello World!", + "@helloWorld": { + "description": "The conventional newborn programmer greeting" + }, + + "welcomeTitle": "Welcome to Outbag", + "@welcomeTitle": { + "description": "Title shown on welcome screen" + }, + "welcomeSubtitle": "Shopping lists made easy", + "@welcomeSubtitle": { + "description": "Subtitle shown on welcome screen" + }, + "userHasAnAccount": "I already have an account", + "@userHasAnAccount": { + "description": "Button displayed on welcome screen (bottom-center) used to launch sign in screen" + }, + "letsGo": "Let's go", + "@letsGo": { + "description": "Text for button on welcome screen used to move the page viewer the first time" + }, + "page2Title": "Open. Decentralized", + "page2Subtitle": "One account, multiple servers", + + "next": "Next", + + "page3Title": "Made to share", + "page3Subtitle": "Collaborate on your shopping lists in real time", + + "continueTour": "Continue Tour", + "takeTour": "Take Tour", + + "page4Title": "Pocket-size", + "page4Subtitle": "Always have your shopping lists with you", + "signUp": "Sign Up", + + + "signIn": "Sign In", + "logIntoAccount": "Log into account", + "createNewAccount": "Create new account", + + "inputServerLabel": "Server", + "inputServerHint": "Your homeserver URL", + "inputServerHelp": "Your data will be stored on your homeserver", + + "inputUsernameLabel": "Username", + "inputUsernameHint": "Your username", + "inputUsernameHelp": "Your username and server-tag allow others to identify you", + + "inputPasswordLabel": "Password", + "inputPasswordHint": "Your password", + "inputPasswordHelp": "Password has to be at least six characters long", + + "inputPasswordRepeatLabel": "Repeat Password", + "inputPasswordRepeatHint": "Type your password again", + "inputPasswordRepeatHelp": "Make sure to type the correct password", + + "inputOTALabel": "OTA", + "inputOTAHint": "One-Time-Authorization token", + "inputOTAHelp": "This token might be required if the server is rate limited", + + "errorPasswordLength": "Password has to be at least six characters long", + "errorPasswordsDoNotMatch": "Passwords do not match", + + "errorInvalidServer": "Unable to find outbag server on {server}", + "@errorInvalidServer": { + "description": "Error shown when there is no outbag server on the given url", + "placeholders": { + "server": { + "type": "String", + "example": "outbag.example.com" + } + } + }, + + "search": "Search", + "settings": "Settings", + "about": "About", + "serverDashboard": "Server Dashboard", + + "addRoom": "Add Room", + "addRoomHint": "Add a new room", + + "noNewRoomsFound": "No new rooms found", + "joinRoom": "Join Room", + "refresh": "Refresh", + "joinRoomInvite": "Join invite-only room", + + "newRoom": "New Room", + "newRoomShort": "New", + "createRoom": "Create Room", + "createRoomShort": "Create", + + "changeRoomIcon": "Change room icon", + "chooseRoomIcon": "Choose a room icon", + + "inputRoomIdLabel": "Room ID", + "inputRoomIdHint": "Unique room id", + "inputRoomIdHelp": "The room id and server tag allow the room to be identified", + + "inputRoomNameLabel": "Room Name", + "inputRoomNameHint": "Give your room a name", + "inputRoomNameHelp": "Choose a human-readable name to easily identify a room", + + "inputRoomDescriptionLabel": "Room Description", + "inputRoomDescriptionHint": "Briefly describe your room", + "inputRoomDescriptionHelp": "Make it easier for others to know what this room is used for", + + "roomVisibilityTitle": "Visibility", + "roomVisibilitySubtitle": "Specify who has access to your room", + "roomVisibilityPrivate": "Private", + "roomVisibilityLocal": "Local", + "roomVisibilityGlobal": "Global", + + "errorNoRoomId": "Please specify a room ID", + "errorRoomIdLength": "Room ID has to be at least three characters long", + "errorNoRoomName": "Please specify a room name", + + "errorNetwork": "Network error", + "errorUnknown": "Unknown error", + "errorServer":"Server error", + + "errorNotFound":"Not found", + "errorDataIncomplete":"Missing data", + "errorDataInvalid":"Invalid data", + "errorPermissions":"You are not allowed to perform this action", + "errorUnreachable":"Server cannot be reached", + "errorAuth":"Username or password wrong", + "errorInvalidOTA":"Invalid OTA", + "errorUsernameUnavailable":"A user with that name already exists", + "errorServerLimit":"Server reached user limit", + + "themeLight": "Light", + "themeDark": "Dark", + "themeSystem": "System", + + "roomListTitle": "List", + "roomListSubtitle": "View shopping list", + "roomProductsTitle": "Products", + "roomProductsSubtitle": "View saved items", + "roomCategoriesTitle": "Categories", + "roomCategoriesSubtitle": "View categories", + "roomAboutTitle": "About", + "roomAboutSubtitle": "View room info", + + "changeRoomVisibilityTitle": "Change Room Visibility", + "changeRoomVisibilitySubtitle": "Do you really want to change the room visibility to: {visibility}", + "@changeRoomVisibilitySubtitle": { + "placeholders": { + "visibility": { + "type": "String", + "example": "Local" + } + } + }, + + "editRoomMetadata": "Edit Metadata", + "editRoomMetadataShort": "Edit Room", + "editRoomMetadataSubtitle": "Edit the room name, description and icon", + "showRoomMembers": "Members", + "showRoomMembersSubtitle": "Show Member list", + "editRoomPermissions": "Edit Permissions", + "editRoomPermissionsSubtitle": "Change the default permission-set for all members", + "manageRoomOTA": "OTA", + "manageRoomOTASubtitle": "Add and delete OTAs", + "manageRoomInvites": "Invites", + "manageRoomInvitesSubtitle": "Invite people to this room", + + "leaveRoom": "Leave Room", + "leaveRoomShort": "Leave", + "leaveRoomConfirm": "Do you really want to leave this room?", + "deleteRoom": "Delete Room", + "deleteRoomShort": "Delete", + "deleteRoomConfirm": "Do you really want to delete this room?", + + "updateRoomPermissions": "Edit", + "updateRoomPermissionsHint": "Update default permission set", + "roomDefaultPermissions": "Default Permissions", + + "roomPermissionAddArticles": "Add Articles", + "roomPermissionAddArticlesSubtitle": "Allows users to add items to the shopping list", + "roomPermissionRemoveArticles": "Remove Articles", + "roomPermissionRemoveArticlesSubtitle": "Allows users to remove items from the shopping list", + "roomPermissionList": "List Groups and Items", + "roomPermissionListSubtitle": "Allows the user to view groups and products", + "roomPermissionChangeMeta": "Change Room Metadata", + "roomPermissionChangeMetaSubtitle": "Allows the user to edit the room name, description and icon", + "roomPermissionManageOTA": "Manage OTAs", + "roomPermissionManageOTASubtitle": "Alloww the user to create, share and delete authentification tokens", + "roomPermissionManageAdmins": "Manage Admins", + "roomPermissionManageAdminsSubtitle": "Allows the user to change the admin status of other members", + "roomPermissionManageMembers": "Manage Members", + "roomPermissionManageMembersSubtitle": "Allows the user to invite and kick room members", + "roomPermissionUnknown": "Unknown Permission", + "roomPermissionUnknownSubtitle": "No information available", + + "roomMembersTitle": "Room Members ({count})", + "@roomMembersTitle": { + "placeholders": { + "count": { + "type": "int", + "example": "0" + } + } + }, + "roleOwner": "Owner", + "roleAdmin": "Admin", + "roleMember": "Member", + + "makeAdminTitle": "Make Admin", + "makeAdminSubtitle": "Grants the user the permission to do everything", + "makeAdminConfirm": "Do you really want to make {user} admin?", + "@makeAdminConfirm": { + "placeholders": { + "user": { + "type": "String", + "example": "ash@example.com" + } + } + }, + "removeAdminTitle": "Remove admin privileges", + "removeAdminSubtitle": "Revokes admin privileges from the user", + "removeAdminConfirm": "Do you really want to remove {user}'s admin privileges", + "@removeAdminConfirm": { + "placeholders": { + "user": { + "type": "String", + "example": "ash@example.com" + } + } + }, + "kickUserTitle": "Kich User", + "kickUserSubtitle": "Temporarily remove user from server (they'll be able to join the room again)", + "kichUserConfirm": "Do you really want to kick {user}?", + "@kickUserConfirm": { + "placeholders": { + "user": { + "type": "String", + "example": "ash@example.com" + } + } + }, + + "limitRoomCount": "Room count limit", + "limitRoomCountSubtitle": "How many rooms you are allowed to own", + "limitRoomSize": "Room size limit", + "limitRoomSizeSubtitle": "How many items/products/categories each room may contain", + "limitRoomMemberCount": "Room member limit", + "limitRoomMemberCountSubtitle": "How many members each of your rooms may have", + "userDiscoverable": "Discoverable", + "userDiscoverableSubtitle": "Determines if your account can be discovered by users from other servers", + + "changeThemeTitle": "Change Theme", + "changeThemeSubtitle": "Choose your preferred color-scheme", + "changePasswordTitle": "Change Password", + "changePasswordSubtitle": "Choose a new password for your account", + "exportAccountTitle": "Export Account", + "exportAccountSubtitle": "Export account data", + "deleteAccountTitle": "Delete Account", + "deleteAccountSubtitle": "Delete your account from your homeserver", + "deleteAccountConfirm": "Do you really want to delete your account?", + + "logOut": "Log out", + "logOutConfirm": "Do you really want to log out?", + + "inputOldPasswordLabel": "Old Password", + "inputOldPasswordHint": "Your current password", + "inputOldPasswordHelp": "Type your current password here", + "inputNewPasswordLabel": "New Password", + "inputNewPasswordHint": "Your new password", + "inputNewPasswordHelp": "Password has to be at least six characters long", + "inputNewPasswordRepeatLabel": "Repeat new Password", + "inputNewPasswordRepeatHint": "Type your new password again", + "inputNewPasswordRepeatHelp": "Make sure this matches your new password", + + "errorPasswordsDontMatch": "New passwords do not match", + "errorOldPasswordWrong": "Your old password is wrong", + + "yes": "Yes", + "loading": "Loading", + "dismiss": "Dismiss", + "cancel": "Cancel", + "ok": "OK", + "close": "Close", + "update": "Update" +} diff --git a/lib/main.dart b/lib/main.dart index 06740a8..a58cfe7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,8 @@ import 'package:outbag_app/screens/room/new.dart'; import 'package:outbag_app/screens/settings/main.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import './screens/home.dart'; import './screens/welcome.dart'; import './screens/room/main.dart'; @@ -142,6 +144,13 @@ class _OutbagAppState extends State { ], child: MaterialApp.router( title: "Outbag", + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + AppLocalizations.delegate + ], + supportedLocales: AppLocalizations.supportedLocales, themeMode: theme.mode, theme: ThemeData(useMaterial3: true, brightness: Brightness.light), darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark), @@ -223,10 +232,6 @@ class _OutbagAppState extends State { GoRoute( name: 'room', path: 'r/:server/:id', - redirect: (context, state) { - print(state.subloc); - return null; - }, builder: (context, state) => RoomPage( state.params['server'] ?? '', state.params['id'] ?? ''), diff --git a/lib/screens/auth.dart b/lib/screens/auth.dart index 582c220..7bf231a 100644 --- a/lib/screens/auth.dart +++ b/lib/screens/auth.dart @@ -4,6 +4,7 @@ import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/snackbar.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../backend/resolve_url.dart'; enum Mode { @@ -31,13 +32,13 @@ class _AuthPageState extends State { @override Widget build(BuildContext context) { - String modeName = "Sign In"; + String modeName = AppLocalizations.of(context)!.signIn; if (widget.mode != Mode.signin) { - modeName = "Sign Up"; + modeName = AppLocalizations.of(context)!.signUp; } - String modeDescription = "Log into account"; + String modeDescription = AppLocalizations.of(context)!.logIntoAccount; if (widget.mode != Mode.signin) { - modeDescription = "Create new account"; + modeDescription = AppLocalizations.of(context)!.createNewAccount; } final textTheme = Theme.of(context) @@ -52,7 +53,7 @@ class _AuthPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(), - Text('Loading', style: textTheme.titleLarge), + Text(AppLocalizations.of(context)!.loading, style: textTheme.titleLarge), ]))) : Scaffold( appBar: AppBar( @@ -70,13 +71,12 @@ class _AuthPageState extends State { child: TextField( controller: _ctrServer, keyboardType: TextInputType.url, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.dns), - labelText: 'Server', - hintText: 'Your homeserver url', - helperText: - 'Your data will be stored on your homeserver', - border: OutlineInputBorder(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.dns), + labelText: AppLocalizations.of(context)!.inputServerLabel, + hintText: AppLocalizations.of(context)!.inputServerHint, + helperText:AppLocalizations.of(context)!.inputServerHelp, + border: const OutlineInputBorder(), ), ), ), @@ -85,13 +85,12 @@ class _AuthPageState extends State { child: TextField( controller: _ctrUsername, keyboardType: TextInputType.emailAddress, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.person), - labelText: 'Username', - hintText: 'Your username', - helperText: - 'your username and server tag allow others to identify you', - border: OutlineInputBorder(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.person), + labelText: AppLocalizations.of(context)!.inputUsernameLabel, + hintText: AppLocalizations.of(context)!.inputUsernameHint, + helperText:AppLocalizations.of(context)!.inputUsernameHelp, + border: const OutlineInputBorder(), ), ), ), @@ -101,13 +100,12 @@ class _AuthPageState extends State { controller: _ctrPassword, keyboardType: TextInputType.visiblePassword, obscureText: true, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.lock), - labelText: 'Password', - hintText: 'Your password', - helperText: - 'Password have to be at least six characters long', - border: OutlineInputBorder(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.lock), + labelText: AppLocalizations.of(context)!.inputPasswordLabel, + hintText: AppLocalizations.of(context)!.inputPasswordHint, + helperText:AppLocalizations.of(context)!.inputPasswordHelp, + border: const OutlineInputBorder(), ), ), ), @@ -120,13 +118,12 @@ class _AuthPageState extends State { controller: _ctrPasswordRpt, keyboardType: TextInputType.visiblePassword, obscureText: true, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.lock), - labelText: 'Repeat Password', - hintText: 'Type your password again', - helperText: - 'Make sure to type the correct password', - border: OutlineInputBorder(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.lock), + labelText: AppLocalizations.of(context)!.inputPasswordRepeatLabel, + hintText: AppLocalizations.of(context)!.inputPasswordRepeatHint, + helperText:AppLocalizations.of(context)!.inputPasswordRepeatHelp, + border: const OutlineInputBorder(), ), ), ) @@ -140,13 +137,12 @@ class _AuthPageState extends State { child: TextField( controller: _ctrOTA, keyboardType: TextInputType.visiblePassword, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.key), - labelText: 'OTA', - hintText: 'One-Time-Authorization token', - helperText: - 'This token might be required if the server is rate limited', - border: OutlineInputBorder(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.key), + labelText: AppLocalizations.of(context)!.inputOTALabel, + hintText: AppLocalizations.of(context)!.inputOTAHint, + helperText:AppLocalizations.of(context)!.inputOTAHelp, + border: const OutlineInputBorder(), ), ), ) @@ -170,7 +166,8 @@ class _AuthPageState extends State { }); showSimpleSnackbar(scaffMgr, - text: 'Passwords do not match', action: 'Dismiss'); + text: AppLocalizations.of(context)!.errorPasswordsDoNotMatch, + action: AppLocalizations.of(context)!.dismiss); _ctrPasswordRpt.clear(); return; @@ -184,8 +181,8 @@ class _AuthPageState extends State { }); showSimpleSnackbar(scaffMgr, - text: 'Password has to be at least 6 characters longs', - action: 'Dismiss'); + text: AppLocalizations.of(context)!.errorPasswordLength, + action: AppLocalizations.of(context)!.dismiss); _ctrPasswordRpt.clear(); return; @@ -204,9 +201,8 @@ class _AuthPageState extends State { }); showSimpleSnackbar(scaffMgr, - text: - 'Unable to find valid outbag server on ${_ctrServer.text}', - action: 'Dismiss'); + text: AppLocalizations.of(context)!.errorInvalidServer(_ctrServer.text), + action: AppLocalizations.of(context)!.dismiss); return; } diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 9f340ec..0d01f9b 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -6,6 +6,7 @@ import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:provider/provider.dart'; import 'package:go_router/go_router.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../backend/room.dart'; class HomePage extends StatefulWidget { @@ -69,7 +70,7 @@ class _HomePageState extends State { actions: [ IconButton( icon: const Icon(Icons.search), - tooltip: "Search", + tooltip: AppLocalizations.of(context)!.search, onPressed: () { // show searchbar // NOTE: location currently unknown @@ -91,7 +92,7 @@ class _HomePageState extends State { menuChildren: [ MenuItemButton( leadingIcon: const Icon(Icons.settings), - child: const Text('Settings'), + child: Text(AppLocalizations.of(context)!.settings), onPressed: () { // show settings screen context.goNamed('settings'); @@ -103,7 +104,7 @@ class _HomePageState extends State { ? [ MenuItemButton( leadingIcon: const Icon(Icons.dns), - child: const Text('Server Dashboard'), + child: Text(AppLocalizations.of(context)!.serverDashboard), onPressed: () { // show settings screen context.goNamed('dash'); @@ -112,7 +113,7 @@ class _HomePageState extends State { : [], MenuItemButton( leadingIcon: const Icon(Icons.info_rounded), - child: const Text('About'), + child: Text(AppLocalizations.of(context)!.about), onPressed: () { // show about screen context.goNamed('about'); @@ -154,13 +155,13 @@ class _HomePageState extends State { }, ), floatingActionButton: FloatingActionButton.extended( - label: const Text('Add Room'), + label: Text(AppLocalizations.of(context)!.addRoom), icon: const Icon(Icons.add), onPressed: () { // create new room context.goNamed('add-room'); }, - tooltip: 'Add new Room', + tooltip: AppLocalizations.of(context)!.addRoomHint, ), ); } diff --git a/lib/screens/room/edit.dart b/lib/screens/room/edit.dart index d96e96e..11c74c4 100644 --- a/lib/screens/room/edit.dart +++ b/lib/screens/room/edit.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:outbag_app/backend/errors.dart'; import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/room.dart'; import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; +import 'package:outbag_app/tools/snackbar.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'dart:math'; class EditRoomPage extends StatefulWidget { @@ -69,7 +70,7 @@ class _EditRoomPageState extends State { } catch (_) { // no room data available // close screen - router.pushReplacementNamed('homoe'); + router.pushReplacementNamed('home'); } })(); return true; @@ -77,7 +78,7 @@ class _EditRoomPageState extends State { onServerErr: (json) { // user not allowed to be here // close screen - router.pushReplacementNamed('homoe'); + router.pushReplacementNamed('home'); return true; }, ); @@ -127,11 +128,11 @@ class _EditRoomPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(), - Text('Loading', style: textTheme.titleLarge), + Text(AppLocalizations.of(context)!.loading, style: textTheme.titleLarge), ]))) : Scaffold( appBar: AppBar( - title: const Text('Edit Room'), + title: Text(AppLocalizations.of(context)!.editRoomMetadataShort), ), body: SingleChildScrollView( child: Center( @@ -149,13 +150,13 @@ class _EditRoomPageState extends State { width: smallest * 0.3, height: smallest * 0.3, ), - tooltip: 'Change room icon', + tooltip: AppLocalizations.of(context)!.changeRoomIcon, onPressed: () { showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text( - 'Choose a room Icon'), + title: Text( + AppLocalizations.of(context)!.chooseRoomIcon), actions: const [], content: SizedBox( width: smallest * 0.3 * 3, @@ -195,13 +196,15 @@ class _EditRoomPageState extends State { enabled: false, controller: _ctrID, keyboardType: TextInputType.emailAddress, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.fact_check), - labelText: 'Room ID', - hintText: 'Unique room id', - helperText: - 'the room id and server tag allow the room to be identified', - border: OutlineInputBorder(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.fact_check), + labelText: AppLocalizations.of(context)! + .inputRoomIdLabel, + hintText: AppLocalizations.of(context)! + .inputRoomIdHint, + helperText: AppLocalizations.of(context)! + .inputRoomIdHelp, + border: const OutlineInputBorder(), ), ), ), @@ -210,13 +213,15 @@ class _EditRoomPageState extends State { child: TextField( controller: _ctrName, keyboardType: TextInputType.name, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.badge), - labelText: 'Room Name', - hintText: 'Give your room a name', - helperText: - 'Easily identify a room with a human readable name', - border: OutlineInputBorder(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.badge), + labelText: AppLocalizations.of(context)! + .inputRoomNameLabel, + hintText: AppLocalizations.of(context)! + .inputRoomNameHint, + helperText: AppLocalizations.of(context)! + .inputRoomNameHelp, + border: const OutlineInputBorder(), ), ), ), @@ -225,13 +230,15 @@ class _EditRoomPageState extends State { child: TextField( controller: _ctrDescription, keyboardType: TextInputType.text, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.dns), - labelText: 'Room Description', - hintText: 'Briefly describe your Room', - helperText: - 'Make it easier for other to know what this room is used for', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: AppLocalizations.of(context)! + .inputRoomDescriptionLabel, + hintText: AppLocalizations.of(context)! + .inputRoomDescriptionHint, + helperText: AppLocalizations.of(context)! + .inputRoomDescriptionHelp, + prefixIcon: const Icon(Icons.dns), + border: const OutlineInputBorder(), ), ), ), @@ -241,22 +248,12 @@ class _EditRoomPageState extends State { onPressed: () async { final scaffMgr = ScaffoldMessenger.of(context); final nav = Navigator.of(context); + final trans = AppLocalizations.of(context); // name may not be empty if (_ctrName.text.isEmpty) { - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: const Text('Please specify a room name'), - action: SnackBarAction( - label: 'Ok', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); + showSimpleSnackbar(scaffMgr, + text: trans!.errorNoRoomName, action: trans.ok); return; } @@ -275,8 +272,8 @@ class _EditRoomPageState extends State { clone.description = _ctrDescription.text; clone.icon = _ctrIcon; - try { - final resp = await postWithCreadentials( + doNetworkRequest(scaffMgr, + req: ()=>postWithCreadentials( target: user.server, credentials: user, path: 'changeRoomMeta', @@ -286,45 +283,16 @@ class _EditRoomPageState extends State { 'title': clone.name, 'description': clone.description, 'icon': clone.icon?.type, - }); - if (resp.res == Result.ok) { + }), + onOK: (_) async { // room was created // save room await clone.toDisk(); nav.pop(); - } else { - // error - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: Text(errorAsString(resp.body)), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); } - } catch (_) { - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: const Text('Network error'), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); - } + ); }, - label: const Text('Update'), + label: Text(AppLocalizations.of(context)!.update), icon: const Icon(Icons.edit)), ); } diff --git a/lib/screens/room/join.dart b/lib/screens/room/join.dart index adcbf44..8ccf5c1 100644 --- a/lib/screens/room/join.dart +++ b/lib/screens/room/join.dart @@ -6,6 +6,7 @@ import 'package:outbag_app/backend/room.dart'; import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'dart:math'; class JoinRoomPage extends StatefulWidget { @@ -102,7 +103,7 @@ class _JoinRoomPageState extends State { actions: [ IconButton( icon: const Icon(Icons.search), - tooltip: "Search", + tooltip: AppLocalizations.of(context)!.search, onPressed: () { // show searchbar // NOTE: location currently unknown @@ -110,7 +111,7 @@ class _JoinRoomPageState extends State { ), IconButton( icon: const Icon(Icons.refresh), - tooltip: "Refresh", + tooltip: AppLocalizations.of(context)!.refresh, onPressed: () { // fetch public rooms again didChangeDependencies(); @@ -132,7 +133,7 @@ class _JoinRoomPageState extends State { menuChildren: [ MenuItemButton( leadingIcon: const Icon(Icons.drafts), - child: const Text('Join invite-only room'), + child: Text(AppLocalizations.of(context)!.joinRoomInvite), onPressed: () { // show settings screen context.goNamed('join-room-ota'); @@ -146,7 +147,7 @@ class _JoinRoomPageState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text('No new Rooms found', style: textTheme.titleLarge), + Text(AppLocalizations.of(context)!.noNewRoomsFound, style: textTheme.titleLarge), ], )) : ListView.builder( @@ -159,7 +160,7 @@ class _JoinRoomPageState extends State { semanticContainer: true, child: InkWell( onTap: () { - // TODO: show modalBottomSheet + // show modalBottomSheet // with room information // and join button showModalBottomSheet( @@ -202,7 +203,7 @@ class _JoinRoomPageState extends State { children: [ Icon(room.visibility?.icon), Text((room - .visibility?.text)!), + .visibility?.text(context))!), ]), ])), // action buttons @@ -217,7 +218,7 @@ class _JoinRoomPageState extends State { child: ElevatedButton.icon( icon: const Icon(Icons.close), - label: const Text('Cancel'), + label: Text(AppLocalizations.of(context)!.cancel), onPressed: () { // close sheet Navigator.pop(context); @@ -230,7 +231,7 @@ class _JoinRoomPageState extends State { child: FilledButton.icon( icon: const Icon(Icons.check), - label: const Text('Join'), + label: Text(AppLocalizations.of(context)!.joinRoom), onPressed: () async { final scaffMgr = ScaffoldMessenger.of( @@ -294,13 +295,13 @@ class _JoinRoomPageState extends State { }, ), floatingActionButton: FloatingActionButton.extended( - label: const Text('New'), + label: Text(AppLocalizations.of(context)!.newRoom), icon: const Icon(Icons.add), onPressed: () { // create new room context.goNamed('new-room'); }, - tooltip: 'Create Room', + tooltip: AppLocalizations.of(context)!.createRoomShort, ), ); } diff --git a/lib/screens/room/main.dart b/lib/screens/room/main.dart index 339e624..fbd9ba9 100644 --- a/lib/screens/room/main.dart +++ b/lib/screens/room/main.dart @@ -8,6 +8,7 @@ import 'package:outbag_app/screens/room/pages/products.dart'; import 'package:outbag_app/screens/room/pages/list.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class RoomPage extends StatefulWidget { final String server; @@ -121,23 +122,27 @@ class _RoomPageState extends State { duration: const Duration(milliseconds: 300)); }, selectedIndex: page, - destinations: const [ + destinations: [ NavigationDestination( - icon: Icon(Icons.list), - label: "List", - tooltip: 'View shopping list'), + icon: const Icon(Icons.list), + label: AppLocalizations.of(context)!.roomListTitle, + tooltip: AppLocalizations.of(context)!.roomListSubtitle + ), NavigationDestination( - icon: Icon(Icons.inventory_2), - label: "Products", - tooltip: 'View saved items'), + icon: const Icon(Icons.inventory_2), + label: AppLocalizations.of(context)!.roomProductsTitle, + tooltip: AppLocalizations.of(context)!.roomProductsSubtitle + ), NavigationDestination( - icon: Icon(Icons.category), - label: "Categories", - tooltip: 'View categories'), + icon: const Icon(Icons.category), + label: AppLocalizations.of(context)!.roomCategoriesTitle, + tooltip: AppLocalizations.of(context)!.roomCategoriesSubtitle + ), NavigationDestination( - icon: Icon(Icons.info_rounded), - label: "About", - tooltip: 'View room info'), + icon: const Icon(Icons.info_rounded), + label: AppLocalizations.of(context)!.roomAboutTitle, + tooltip: AppLocalizations.of(context)!.roomAboutSubtitle + ), ], ), ); diff --git a/lib/screens/room/members.dart b/lib/screens/room/members.dart index f897484..97d4716 100644 --- a/lib/screens/room/members.dart +++ b/lib/screens/room/members.dart @@ -6,6 +6,7 @@ import 'package:outbag_app/backend/room.dart'; import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ManageRoomMembersPage extends StatefulWidget { final String server; @@ -95,15 +96,8 @@ class _ManageRoomMembersPageState extends State { return Scaffold( appBar: AppBar( - title: Text('Room Members (${list.length})'), - leading: IconButton( - onPressed: () { - // go back - Navigator.of(context).pop(); - }, - icon: const Icon(Icons.arrow_back), - tooltip: "Go back", - ), + title: Text( + AppLocalizations.of(context)!.roomMembersTitle(list.length)), //actions: [ // // NOTE: Maybe add a search icon // // and general search functionality here @@ -113,13 +107,13 @@ class _ManageRoomMembersPageState extends State { itemBuilder: (BuildContext context, int index) { final item = list[index]; - String role = "Member"; + String role = AppLocalizations.of(context)!.roleMember; if (info != null && (info?.owner)! == item.id && widget.server == item.serverTag) { - role = "Owner"; + role = AppLocalizations.of(context)!.roleOwner; } else if (item.isAdmin) { - role = "Admin"; + role = AppLocalizations.of(context)!.roleAdmin; } bool enable = true; @@ -172,11 +166,11 @@ class _ManageRoomMembersPageState extends State { leading: const Icon( Icons.supervisor_account), title: Text(item.isAdmin - ? 'Remove admin privileges' - : 'Make Admin'), + ? AppLocalizations.of(context)!.removeAdminTitle + : AppLocalizations.of(context)!.makeAdminTitle), subtitle: Text(item.isAdmin - ? 'Revokes admin privileges from the user' - : 'Grants the user the permission to do everything'), + ? AppLocalizations.of(context)!.removeAdminSubtitle + : AppLocalizations.of(context)!.makeAdminSubtitle), onTap: () { // make user admin showDialog( @@ -189,12 +183,12 @@ class _ManageRoomMembersPageState extends State { .supervisor_account), title: Text(item .isAdmin - ? 'Remove admin privileges' - : 'Make Admin'), + ? AppLocalizations.of(context)!.removeAdminTitle + : AppLocalizations.of(context)!.makeAdminTitle), content: Text(item .isAdmin - ? "Do you really want to remove ${item.humanReadableName}'s admin privileges" - : 'Do you really want to make ${item.humanReadableName} admin?'), + ? AppLocalizations.of(context)!.removeAdminConfirm(item.humanReadableName) + : AppLocalizations.of(context)!.makeAdminConfirm(item.humanReadableName)), actions: [ TextButton( onPressed: @@ -205,8 +199,7 @@ class _ManageRoomMembersPageState extends State { Navigator.of(context) .pop(); }, - child: const Text( - 'Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), FilledButton( onPressed: @@ -238,8 +231,7 @@ class _ManageRoomMembersPageState extends State { nav.pop(); }); }, - child: const Text( - 'OK'), + child: Text(AppLocalizations.of(context)!.ok), ) ], )); @@ -257,10 +249,8 @@ class _ManageRoomMembersPageState extends State { ListTile( leading: const Icon( Icons.person_remove), - title: - const Text('Kick User'), - subtitle: const Text( - "Temporarrily remove user from server (they'll be able to join the room again)"), + title: Text(AppLocalizations.of(context)!.kickUserTitle), + subtitle: Text(AppLocalizations.of(context)!.kickUserSubtitle), onTap: () { // remove user from room showDialog( @@ -271,10 +261,10 @@ class _ManageRoomMembersPageState extends State { icon: const Icon( Icons .person_remove), - title: const Text( - 'Kick User'), + title: Text( + AppLocalizations.of(context)!.kickUserTitle), content: Text( - 'Do you really want to kick ${item.humanReadableName}?'), + AppLocalizations.of(context)!.kichUserConfirm(item.humanReadableName)), actions: [ TextButton( onPressed: @@ -286,8 +276,8 @@ class _ManageRoomMembersPageState extends State { Navigator.of(ctx) .pop(); }, - child: const Text( - 'Cancel'), + child: Text( + AppLocalizations.of(context)!.cancel), ), FilledButton( onPressed: @@ -318,8 +308,8 @@ class _ManageRoomMembersPageState extends State { nav.pop(); }); }, - child: const Text( - 'Kick User'), + child: Text( + AppLocalizations.of(context)!.ok), ) ], )); @@ -331,7 +321,7 @@ class _ManageRoomMembersPageState extends State { ), ), FilledButton( - child: const Text('Close'), + child: Text(AppLocalizations.of(context)!.close), onPressed: () { Navigator.of(context).pop(); }, diff --git a/lib/screens/room/new.dart b/lib/screens/room/new.dart index 6a144e0..e4118a5 100644 --- a/lib/screens/room/new.dart +++ b/lib/screens/room/new.dart @@ -7,6 +7,7 @@ import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/snackbar.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'dart:math'; class NewRoomPage extends StatefulWidget { @@ -28,208 +29,226 @@ class _NewRoomPageState extends State { @override Widget build(BuildContext context) { final textTheme = Theme.of(context) - .textTheme - .apply(displayColor: Theme.of(context).colorScheme.onSurface); + .textTheme + .apply(displayColor: Theme.of(context).colorScheme.onSurface); double width = MediaQuery.of(context).size.width; double height = MediaQuery.of(context).size.height; double smallest = min(min(width, height), 400); return showSpinner - ? Scaffold( - body: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - Text('Loading', style: textTheme.titleLarge), - ]))) - : Scaffold( - appBar: AppBar( - title: const Text('New Room'), - ), - body: SingleChildScrollView( - child: Center( - child: Padding( - padding: const EdgeInsets.all(14), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: SvgPicture.asset( - _ctrIcon.img, - width: smallest * 0.3, - height: smallest * 0.3, - ), - tooltip: 'Change room icon', - onPressed: () { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text( - 'Choose a room Icon'), - actions: const [], - content: SizedBox( - width: smallest * 0.3 * 3, - height: smallest * 0.3 * 3, - child: GridView.count( - crossAxisCount: 3, - children: RoomIcon.list() - .map((icon) { - return GridTile( - child: IconButton( - icon: SvgPicture - .asset( - icon.img, - width: - smallest * - 0.3, - height: - smallest * - 0.3, - ), - tooltip: - icon.text, - onPressed: () { - setState(() { - _ctrIcon = - icon; - }); - Navigator.of( - context) - .pop(); - })); - }).toList())), - )); - }, - ), - Padding( - padding: const EdgeInsets.all(8), - child: TextField( - controller: _ctrID, - keyboardType: TextInputType.emailAddress, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.fact_check), - labelText: 'Room ID', - hintText: 'Unique room id', - helperText: - 'the room id and server tag allow the room to be identified', - border: OutlineInputBorder(), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8), - child: TextField( - controller: _ctrName, - keyboardType: TextInputType.name, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.badge), - labelText: 'Room Name', - hintText: 'Give your room a name', - helperText: - 'Easily identify a room with a human readable name', - border: OutlineInputBorder(), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8), - child: TextField( - controller: _ctrDescription, - keyboardType: TextInputType.text, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.dns), - labelText: 'Room Description', - hintText: 'Briefly describe your Room', - helperText: - 'Make it easier for other to know what this room is used for', - border: OutlineInputBorder(), - ), - ), - ), - Text('Visibility', style: textTheme.labelLarge), - Text('Specify who has access to your room', - style: textTheme.bodySmall), - SegmentedButton( - showSelectedIcon: true, - multiSelectionEnabled: false, - emptySelectionAllowed: false, - segments: RoomVisibility.list().map((vis) { - return ButtonSegment( - value: vis, - label: Text(vis.text), - icon: Icon(vis.icon)); - }).toList(), - onSelectionChanged: ((vset) { - setState(() { - _ctrVis = vset.single; - }); - }), - selected: {_ctrVis}, - selectedIcon: Icon(_ctrVis.icon), - ), - ], - ))))), - floatingActionButton: FloatingActionButton.extended( - onPressed: () async { - final scaffMgr = ScaffoldMessenger.of(context); - final router = GoRouter.of(context); + ? Scaffold( + body: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + Text(AppLocalizations.of(context)!.loading, + style: textTheme.titleLarge), + ]))) + : Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.newRoom), + ), + body: SingleChildScrollView( + child: Center( + child: Padding( + padding: const EdgeInsets.all(14), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: SvgPicture.asset( + _ctrIcon.img, + width: smallest * 0.3, + height: smallest * 0.3, + ), + tooltip: AppLocalizations.of(context)! + .changeRoomIcon, + onPressed: () { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text( + AppLocalizations.of(context)! + .chooseRoomIcon), + actions: const [], + content: SizedBox( + width: smallest * 0.3 * 3, + height: smallest * 0.3 * 3, + child: GridView.count( + crossAxisCount: 3, + children: RoomIcon.list() + .map((icon) { + return GridTile( + child: IconButton( + icon: SvgPicture + .asset( + icon.img, + width: + smallest * + 0.3, + height: + smallest * + 0.3, + ), + // do not display tooltip for now + // as it is hard to translate + // and the tooltip prevented the click event, + // when clicked on the tooltip bar + // tooltip:icon.text, + onPressed: () { + setState(() { + _ctrIcon = + icon; + }); + Navigator.of( + context) + .pop(); + })); + }).toList())), + )); + }, + ), + Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: _ctrID, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.fact_check), + labelText: AppLocalizations.of(context)! + .inputRoomIdLabel, + hintText: AppLocalizations.of(context)! + .inputRoomIdHint, + helperText: AppLocalizations.of(context)! + .inputRoomIdHelp, + border: const OutlineInputBorder(), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: _ctrName, + keyboardType: TextInputType.name, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.badge), + labelText: AppLocalizations.of(context)! + .inputRoomNameLabel, + hintText: AppLocalizations.of(context)! + .inputRoomNameHint, + helperText: AppLocalizations.of(context)! + .inputRoomNameHelp, + border: const OutlineInputBorder(), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: _ctrDescription, + keyboardType: TextInputType.text, + decoration: InputDecoration( + labelText: AppLocalizations.of(context)! + .inputRoomDescriptionLabel, + hintText: AppLocalizations.of(context)! + .inputRoomDescriptionHint, + helperText: AppLocalizations.of(context)! + .inputRoomDescriptionHelp, + prefixIcon: const Icon(Icons.dns), + border: const OutlineInputBorder(), + ), + ), + ), + Text( + AppLocalizations.of(context)! + .roomVisibilityTitle, + style: textTheme.labelLarge), + Text( + AppLocalizations.of(context)! + .roomVisibilitySubtitle, + style: textTheme.bodySmall), + SegmentedButton( + showSelectedIcon: true, + multiSelectionEnabled: false, + emptySelectionAllowed: false, + segments: RoomVisibility.list().map((vis) { + return ButtonSegment( + value: vis, + label: Text(vis.text(context)), + icon: Icon(vis.icon)); + }).toList(), + onSelectionChanged: ((vset) { + setState(() { + _ctrVis = vset.single; + }); + }), + selected: {_ctrVis}, + selectedIcon: Icon(_ctrVis.icon), + ), + ], + ))))), + floatingActionButton: FloatingActionButton.extended( + onPressed: () async { + final scaffMgr = ScaffoldMessenger.of(context); + final router = GoRouter.of(context); + final trans = AppLocalizations.of(context); - // ID should be at least three characters long - if (_ctrID.text.length < 3) { - showSimpleSnackbar(scaffMgr, - text: _ctrID.text.isEmpty - ? 'Please specify a Room ID' - : 'Room ID has to be at least three characters long', - action: 'OK'); + // ID should be at least three characters long + if (_ctrID.text.length < 3) { + showSimpleSnackbar(scaffMgr, + text: _ctrID.text.isEmpty + ? trans!.errorNoRoomId + : trans!.errorRoomIdLength, + action: trans.ok); - return; - } + return; + } - // name may not be empty - if (_ctrName.text.isEmpty) { - showSimpleSnackbar(scaffMgr, - text: 'Please specify a room name', action: 'OK'); + // name may not be empty + if (_ctrName.text.isEmpty) { + showSimpleSnackbar(scaffMgr, + text: trans!.errorNoRoomName, action: trans.ok); - return; - } + return; + } - final user = context.read(); - final room = Room( - id: _ctrID.text, - serverTag: user.server.tag, - name: _ctrName.text, - description: _ctrDescription.text, - icon: _ctrIcon, - visibility: _ctrVis); + final user = context.read(); + final room = Room( + id: _ctrID.text, + serverTag: user.server.tag, + name: _ctrName.text, + description: _ctrDescription.text, + icon: _ctrIcon, + visibility: _ctrVis); - doNetworkRequest(scaffMgr, - req: () => postWithCreadentials( - target: user.server, - credentials: user, - path: 'createRoom', - body: { - 'room': room.id, - 'title': room.name, - 'description': room.description, - 'icon': room.icon?.type, - 'visibility': room.visibility?.type - }), - onOK: (_) async { - // room was created - // save room - await room.toDisk(); - // move to home page - router.pushReplacementNamed('home'); - }); - }, - label: const Text('Create'), - icon: const Icon(Icons.add)), - ); + doNetworkRequest(scaffMgr, + req: () => postWithCreadentials( + target: user.server, + credentials: user, + path: 'createRoom', + body: { + 'room': room.id, + 'title': room.name, + 'description': room.description, + 'icon': room.icon?.type, + 'visibility': room.visibility?.type + }), + onOK: (_) async { + // room was created + // save room + await room.toDisk(); + // move to home page + router.pushReplacementNamed('home'); + }); + }, + label: Text(AppLocalizations.of(context)!.createRoomShort), + icon: const Icon(Icons.add)), + ); } } diff --git a/lib/screens/room/pages/about.dart b/lib/screens/room/pages/about.dart index 60a3168..e84e803 100644 --- a/lib/screens/room/pages/about.dart +++ b/lib/screens/room/pages/about.dart @@ -8,6 +8,7 @@ import 'package:outbag_app/backend/user.dart'; import 'dart:math'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class AboutRoomPage extends StatefulWidget { final RoomInfo? info; @@ -67,7 +68,7 @@ class _AboutRoomPageState extends State { segments: RoomVisibility.list().map((vis) { return ButtonSegment( value: vis.type, - label: Text(vis.text), + label: Text(vis.text(context)), icon: Icon(vis.icon)); }).toList(), onSelectionChanged: ((vset) { @@ -90,16 +91,14 @@ class _AboutRoomPageState extends State { showDialog( context: context, builder: (ctx) => AlertDialog( - title: - const Text('Change room visibility'), - content: Text( - 'Do you really want to change the room visibility to: ${vis.text}'), + title: Text(AppLocalizations.of(context)!.changeRoomVisibilityTitle), + content: Text(AppLocalizations.of(context)!.changeRoomVisibilitySubtitle(vis.text(context))), actions: [ TextButton( onPressed: () { context.pop(); }, - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), FilledButton( onPressed: () async { @@ -128,7 +127,7 @@ class _AboutRoomPageState extends State { nav.pop(); }); }, - child: const Text('Ok'), + child: Text(AppLocalizations.of(context)!.ok), ) ], )); @@ -156,9 +155,8 @@ class _AboutRoomPageState extends State { ? [ ListTile( trailing: const Icon(Icons.chevron_right), - title: const Text('Edit Metadata'), - subtitle: const Text( - 'Change the rooms name, description and icon'), + title: Text(AppLocalizations.of(context)!.editRoomMetadata), + subtitle: Text(AppLocalizations.of(context)!.editRoomMetadataSubtitle), onTap: () { // show edit room screen context.goNamed('edit-room', params: { @@ -172,8 +170,8 @@ class _AboutRoomPageState extends State { // open members view ListTile( trailing: const Icon(Icons.chevron_right), - title: const Text('Members'), - subtitle: const Text('Show Member list'), + title: Text(AppLocalizations.of(context)!.showRoomMembers), + subtitle: Text(AppLocalizations.of(context)!.showRoomMembersSubtitle), onTap: () { // open member view screen context.goNamed('room-members', params: { @@ -192,9 +190,8 @@ class _AboutRoomPageState extends State { ? [ ListTile( trailing: const Icon(Icons.chevron_right), - title: const Text('Edit Permissions'), - subtitle: const Text( - 'Change the default permission-set for all members'), + title: Text(AppLocalizations.of(context)!.editRoomPermissions), + subtitle: Text(AppLocalizations.of(context)!.editRoomPermissionsSubtitle), onTap: () { // show checkbox screen context.goNamed('room-permissions', params: { @@ -213,8 +210,8 @@ class _AboutRoomPageState extends State { ? [ ListTile( trailing: const Icon(Icons.chevron_right), - title: const Text('OTA'), - subtitle: const Text('Add and delete OTAs'), + title: Text(AppLocalizations.of(context)!.manageRoomOTA), + subtitle: Text(AppLocalizations.of(context)!.manageRoomOTASubtitle), onTap: () { // show manage ota screen context.goNamed('room-ota', params: { @@ -225,8 +222,8 @@ class _AboutRoomPageState extends State { ), ListTile( trailing: const Icon(Icons.chevron_right), - title: const Text('Invites'), - subtitle: const Text('Invite people to this room'), + title: Text(AppLocalizations.of(context)!.manageRoomInvites), + subtitle: Text(AppLocalizations.of(context)!.manageRoomInvitesSubtitle), onTap: () { // show manage ota screen context.goNamed('room-invite', params: { @@ -246,25 +243,26 @@ class _AboutRoomPageState extends State { padding: const EdgeInsets.all(8), child: FilledButton.tonal( child: Text(((widget.info?.isOwner)!) - ? 'Delete Room' - : 'Leave Room'), + ? AppLocalizations.of(context)!.deleteRoom + : AppLocalizations.of(context)!.leaveRoom), onPressed: () { // show confirm dialog showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(((widget.info?.isOwner)!) - ? 'Delete Room' - : 'Leave Room'), - content: Text( - 'Do you really want to ${((widget.info?.isOwner)!) ? "delete" : "leave"} the room?'), + ? AppLocalizations.of(context)!.deleteRoom + : AppLocalizations.of(context)!.leaveRoom), + content: Text(((widget.info?.isOwner)!) + ? AppLocalizations.of(context)!.deleteRoomConfirm + : AppLocalizations.of(context)!.leaveRoomConfirm), actions: [ TextButton( onPressed: () { // close popup Navigator.of(ctx).pop(); }, - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), FilledButton( onPressed: () async { @@ -302,8 +300,8 @@ class _AboutRoomPageState extends State { }); }, child: Text(((widget.info?.isOwner)!) - ? 'Delete' - : 'Leave'), + ? AppLocalizations.of(context)!.deleteRoomShort + : AppLocalizations.of(context)!.leaveRoomShort), ) ], )); diff --git a/lib/screens/room/permissions.dart b/lib/screens/room/permissions.dart index c502495..d3c6878 100644 --- a/lib/screens/room/permissions.dart +++ b/lib/screens/room/permissions.dart @@ -1,5 +1,5 @@ import 'dart:math'; - +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:outbag_app/backend/permissions.dart'; @@ -31,10 +31,10 @@ class _EditRoomPermissionSetPageState extends State { doNetworkRequest( sm, req: () => postWithCreadentials( - path: 'getRoomInfo', - credentials: user, - target: user.server, - body: {'room': widget.tag, 'server': widget.server}), + path: 'getRoomInfo', + credentials: user, + target: user.server, + body: {'room': widget.tag, 'server': widget.server}), onAnyErr: () { // user should not be here // close screen @@ -44,7 +44,7 @@ class _EditRoomPermissionSetPageState extends State { onOK: (body) async { final info = RoomInfo.fromJSON(body['data']); setState(() { - permissions = info.permissions; + permissions = info.permissions; }); return true; }, @@ -62,15 +62,7 @@ class _EditRoomPermissionSetPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Default permissions'), - leading: IconButton( - onPressed: () { - // go back - Navigator.of(context).pop(); - }, - icon: const Icon(Icons.arrow_back), - tooltip: "Go back", - ), + title: Text(AppLocalizations.of(context)!.roomDefaultPermissions), ), body: ListView.builder( itemCount: items.length, @@ -80,20 +72,20 @@ class _EditRoomPermissionSetPageState extends State { final int col = pow(2, index + 1) as int; return SwitchListTile( - title: Text(RoomPermission.name(item)), - subtitle: Text(RoomPermission.describe(item)), - onChanged: (state) { - setState(() { + title: Text(RoomPermission.name(item, context)), + subtitle: Text(RoomPermission.describe(item, context)), + onChanged: (state) { + setState(() { permissions += (state ? 1 : -1) * col; - }); - }, - value: permissions & col != 0); + }); + }, + value: permissions & col != 0); }, ), floatingActionButton: FloatingActionButton.extended( icon: const Icon(Icons.edit), - tooltip: "Update default permission set", - label: const Text('Edit'), + tooltip: AppLocalizations.of(context)!.updateRoomPermissionsHint, + label: Text(AppLocalizations.of(context)!.updateRoomPermissions), onPressed: () { final router = GoRouter.of(context); final sm = ScaffoldMessenger.of(context); @@ -101,18 +93,18 @@ class _EditRoomPermissionSetPageState extends State { // update permissions doNetworkRequest(sm, - req: () => postWithCreadentials( - path: 'setRoomRight', - credentials: user, - target: user.server, - body: { - 'room': widget.tag, - 'server': widget.server, - 'rights': permissions - }), - onOK: (_) { - router.pop(); - }); + req: () => postWithCreadentials( + path: 'setRoomRight', + credentials: user, + target: user.server, + body: { + 'room': widget.tag, + 'server': widget.server, + 'rights': permissions + }), + onOK: (_) { + router.pop(); + }); }, ), ); diff --git a/lib/screens/settings/dialogs/password.dart b/lib/screens/settings/dialogs/password.dart index 65be0f0..16fc982 100644 --- a/lib/screens/settings/dialogs/password.dart +++ b/lib/screens/settings/dialogs/password.dart @@ -4,6 +4,7 @@ import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/snackbar.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; class ChangePasswordDialog extends StatefulWidget { @@ -21,59 +22,57 @@ class _ChangePasswordDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text('Change Password'), + title: Text(AppLocalizations.of(context)!.changeThemeTitle), icon: const Icon(Icons.password), content: SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: TextField( - controller: _ctrOldPassword, - keyboardType: TextInputType.visiblePassword, - obscureText: true, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.lock), - labelText: 'Old Password', - hintText: 'Your current password', - helperText: - 'For safety, you have to type your current passwort', - border: OutlineInputBorder(), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: _ctrOldPassword, + keyboardType: TextInputType.visiblePassword, + obscureText: true, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.lock), + labelText: AppLocalizations.of(context)!.inputOldPasswordLabel, + hintText: AppLocalizations.of(context)!.inputOldPasswordHint, + helperText:AppLocalizations.of(context)!.inputOldPasswordHelp, + border: const OutlineInputBorder(), + ), ), ), - ), - Padding( - padding: const EdgeInsets.all(8), - child: TextField( - controller: _ctrNewPassword, - keyboardType: TextInputType.visiblePassword, - obscureText: true, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.lock), - labelText: 'New Password', - hintText: 'Your new password', - helperText: 'Password have to be at least six characters long', - border: OutlineInputBorder(), + Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: _ctrNewPassword, + keyboardType: TextInputType.visiblePassword, + obscureText: true, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.lock), + labelText: AppLocalizations.of(context)!.inputNewPasswordLabel, + hintText: AppLocalizations.of(context)!.inputNewPasswordHint, + helperText:AppLocalizations.of(context)!.inputNewPasswordHelp, + border: const OutlineInputBorder(), + ), ), ), - ), - Padding( - padding: const EdgeInsets.all(8), - child: TextField( - controller: _ctrNewPasswordRepeat, - keyboardType: TextInputType.visiblePassword, - obscureText: true, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.lock), - labelText: 'Repeat new Password', - hintText: 'Type your new password again', - helperText: - 'Type your new password again, to make sure you know it', - border: OutlineInputBorder(), + Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: _ctrNewPasswordRepeat, + keyboardType: TextInputType.visiblePassword, + obscureText: true, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.lock), + labelText: AppLocalizations.of(context)!.inputNewPasswordRepeatLabel, + hintText: AppLocalizations.of(context)!.inputNewPasswordRepeatHint, + helperText:AppLocalizations.of(context)!.inputNewPasswordRepeatHelp, + border: const OutlineInputBorder(), + ), ), ), - ), - ], + ], )), actions: [ TextButton( @@ -81,20 +80,21 @@ class _ChangePasswordDialogState extends State { // close popup Navigator.of(context).pop(); }, - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), FilledButton( onPressed: () async { final scaffMgr = ScaffoldMessenger.of(context); final nav = Navigator.of(context); final user = context.read(); + final trans = AppLocalizations.of(context); // validate password if (_ctrNewPassword.text.length < 6) { // password has to be at least 6 characters long showSimpleSnackbar(scaffMgr, - text: 'Password has to be at least 6 characters longs', - action: 'Dismiss'); + text: trans!.errorPasswordLength, + action: trans.dismiss); _ctrNewPasswordRepeat.clear(); return; @@ -102,7 +102,7 @@ class _ChangePasswordDialogState extends State { if (_ctrNewPassword.text != _ctrNewPasswordRepeat.text) { // new passwords do not match showSimpleSnackbar(scaffMgr, - text: 'New passwords do not match', action: 'Dismiss'); + text: trans!.errorPasswordsDoNotMatch, action: trans.dismiss); _ctrNewPasswordRepeat.clear(); return; @@ -110,7 +110,7 @@ class _ChangePasswordDialogState extends State { if (hashPassword(_ctrOldPassword.text) != user.password) { // current password wrong showSimpleSnackbar(scaffMgr, - text: 'Old password is wrong', action: 'Dismiss'); + text: trans!.errorOldPasswordWrong, action: trans.dismiss); _ctrOldPassword.clear(); return; @@ -120,25 +120,25 @@ class _ChangePasswordDialogState extends State { // send request doNetworkRequest(scaffMgr, - req: () => postWithCreadentials( - path: 'changePassword', - target: user.server, - body: {'accountKey': password}, - credentials: user), - onOK: (_) async { - // update local user struct - final updatedUser = User( - username: user.username, - password: password, - server: user.server); - await updatedUser.toDisk(); - }, - after: () { - // close popup - nav.pop(); - }); + req: () => postWithCreadentials( + path: 'changePassword', + target: user.server, + body: {'accountKey': password}, + credentials: user), + onOK: (_) async { + // update local user struct + final updatedUser = User( + username: user.username, + password: password, + server: user.server); + await updatedUser.toDisk(); + }, + after: () { + // close popup + nav.pop(); + }); }, - child: const Text('Change password'), + child: Text(AppLocalizations.of(context)!.changeThemeTitle), ) ], ); diff --git a/lib/screens/settings/main.dart b/lib/screens/settings/main.dart index 7531ed5..ac2f390 100644 --- a/lib/screens/settings/main.dart +++ b/lib/screens/settings/main.dart @@ -6,6 +6,7 @@ import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/screens/settings/dialogs/password.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -26,7 +27,7 @@ class _SettingsPageState extends State { return Scaffold( appBar: AppBar( - title: const Text('Settings'), + title: Text(AppLocalizations.of(context)!.settings), ), body: SingleChildScrollView( child: Center( @@ -44,51 +45,58 @@ class _SettingsPageState extends State { child: Text(user.humanReadable, style: textTheme.titleLarge)), ListTile( - title: const Text('Room count limit:'), - subtitle: const Text( - 'How many rooms you are allowed to own'), + title: Text( + AppLocalizations.of(context)!.limitRoomCount), + subtitle: Text(AppLocalizations.of(context)! + .limitRoomCountSubtitle), trailing: Text('${meta?.maxRoomCount ?? ""}'), ), ListTile( - title: const Text('Room size limit:'), - subtitle: const Text( - 'How many items/products/categories each room may contain'), + title: Text( + AppLocalizations.of(context)!.limitRoomSize), + subtitle: Text(AppLocalizations.of(context)! + .limitRoomSizeSubtitle), trailing: Text('${meta?.maxRoomSize ?? ""}'), ), ListTile( - title: const Text('Room member limit:'), - subtitle: const Text( - 'How many members each of your rooms may have'), + title: Text(AppLocalizations.of(context)! + .limitRoomMemberCount), + subtitle: Text(AppLocalizations.of(context)! + .limitRoomMemberCountSubtitle), trailing: Text('${meta?.maxRoomMemberCount ?? ""}')), ListTile( - title: const Text('Discoverable'), - subtitle: const Text( - 'Determines if your account can be discovered by users from other servers'), + title: Text(AppLocalizations.of(context)! + .userDiscoverable), + subtitle: Text(AppLocalizations.of(context)! + .userDiscoverableSubtitle), trailing: Checkbox( tristate: true, value: meta?.discoverable, - onChanged: (_) {}, + onChanged: (_) { + // TODO: implement changeVisibility + }, )) ], )))), // change theme button ListTile( - title: const Text('Change Theme'), - subtitle: const Text( - 'You can change between a light theme, a dark theme and automatic theme selection'), + title: Text(AppLocalizations.of(context)!.changeThemeTitle), + subtitle: Text(AppLocalizations.of(context)!.changeThemeSubtitle), trailing: const Icon(Icons.chevron_right), onTap: () { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Change Theme'), + title: Text( + AppLocalizations.of(context)!.changeThemeTitle), content: SingleChildScrollView( child: Column(children: [ - const Padding( - padding: EdgeInsets.all(8), - child: Text('Choose your preferred theme'), + Padding( + padding: const EdgeInsets.all(8), + child: Text(AppLocalizations.of(context)! + .changeThemeSubtitle), ), SegmentedButton( selected: {context.watch()}, @@ -100,7 +108,7 @@ class _SettingsPageState extends State { return ButtonSegment( value: item, icon: Icon(item.icon), - label: Text(item.name)); + label: Text(item.name(context))); }).toList(), onSelectionChanged: (item) async { try { @@ -111,9 +119,9 @@ class _SettingsPageState extends State { ])), actions: [ FilledButton( - child: const Text('Close'), + child: Text(AppLocalizations.of(context)!.close), onPressed: () { - Navigator.of(context).pop(); + context.pop(); }, ) ], @@ -123,8 +131,9 @@ class _SettingsPageState extends State { // change password button ListTile( - title: const Text('Change password'), - subtitle: const Text('Choose a new password for your account'), + title: Text(AppLocalizations.of(context)!.changePasswordTitle), + subtitle: + Text(AppLocalizations.of(context)!.changePasswordSubtitle), onTap: () { showDialog( context: context, @@ -135,8 +144,8 @@ class _SettingsPageState extends State { // export account to json ListTile( - title: const Text('Export account'), - subtitle: const Text('Export account data'), + title: Text(AppLocalizations.of(context)!.exportAccountTitle), + subtitle: Text(AppLocalizations.of(context)!.exportAccountSubtitle), onTap: () { // TODO: show confirm dialog // NOTE: json dump the localstore @@ -148,8 +157,8 @@ class _SettingsPageState extends State { // delete account button ListTile( - title: const Text('Delete account'), - subtitle: const Text('Delete your account from your homeserver'), + title: Text(AppLocalizations.of(context)!.deleteAccountTitle), + subtitle: Text(AppLocalizations.of(context)!.deleteAccountSubtitle), onTap: () { // show confirm dialog // NOTE: same as logout @@ -158,16 +167,17 @@ class _SettingsPageState extends State { showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Delete account'), - content: const Text( - 'Do you really want to delete your account?'), + title: Text( + AppLocalizations.of(context)!.deleteAccountTitle), + content: Text( + AppLocalizations.of(context)!.deleteAccountConfirm), actions: [ TextButton( onPressed: () { // close popup Navigator.of(ctx).pop(); }, - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), FilledButton( onPressed: () async { @@ -198,7 +208,7 @@ class _SettingsPageState extends State { nav.pop(); }); }, - child: const Text('Delete Account'), + child: Text(AppLocalizations.of(context)!.yes), ) ], )); @@ -210,22 +220,23 @@ class _SettingsPageState extends State { Padding( padding: const EdgeInsets.all(8), child: FilledButton.tonal( - child: const Text('Log out'), + child: Text(AppLocalizations.of(context)!.logOut), onPressed: () { // show confirm dialog showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Log out'), - content: - const Text('Do you really want to log out?'), + title: Text(AppLocalizations.of(context)!.logOut), + content: Text( + AppLocalizations.of(context)!.logOutConfirm), actions: [ TextButton( onPressed: () { // close popup Navigator.of(ctx).pop(); }, - child: const Text('Cancel'), + child: + Text(AppLocalizations.of(context)!.cancel), ), FilledButton( onPressed: () async { @@ -242,7 +253,7 @@ class _SettingsPageState extends State { // go back home router.pushReplacementNamed('home'); }, - child: const Text('Log out'), + child: Text(AppLocalizations.of(context)!.yes), ) ], )); diff --git a/lib/screens/welcome.dart b/lib/screens/welcome.dart index d593a73..9c0fc21 100644 --- a/lib/screens/welcome.dart +++ b/lib/screens/welcome.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:outbag_app/tools/assets.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'dart:math'; class WelcomePage extends StatefulWidget { @@ -33,17 +34,17 @@ class _WelcomePageState extends State { .textTheme .apply(displayColor: Theme.of(context).colorScheme.onSurface); - String fabText = "Next"; + String fabText = AppLocalizations.of(context)!.next; if (_currentPage == 0) { - fabText = "Let's go"; + fabText = AppLocalizations.of(context)!.letsGo; } else if (_currentPage == 4 - 1) { - fabText = "Sign up"; + fabText = AppLocalizations.of(context)!.signUp; } - String fabTooltip = "Continue Tour"; + String fabTooltip = AppLocalizations.of(context)!.continueTour; if (_currentPage == 0) { - fabTooltip = "Take Tour"; + fabTooltip = AppLocalizations.of(context)!.takeTour; } else if (_currentPage == 4 - 1) { - fabTooltip = "Create an account"; + fabTooltip = AppLocalizations.of(context)!.createNewAccount; } double width = MediaQuery.of(context).size.width; @@ -66,10 +67,13 @@ class _WelcomePageState extends State { width: smallest * 0.5, height: smallest * 0.5), Text( - 'Welcome to Outbag', + AppLocalizations.of(context)!.welcomeTitle, style: textTheme.displaySmall, ), - Text('Shopping lists made easy', style: textTheme.bodyMedium) + Text( + AppLocalizations.of(context)!.welcomeSubtitle, + style: textTheme.bodyMedium + ) ], ), Column( @@ -81,10 +85,11 @@ class _WelcomePageState extends State { width: smallest * 0.5, height: smallest * 0.5), Text( - 'Open. Decentralized', + AppLocalizations.of(context)!.page2Title, style: textTheme.displaySmall, ), - Text('One account, multiple servers', + Text( + AppLocalizations.of(context)!.page2Subtitle, style: textTheme.bodyMedium) ], ), @@ -97,10 +102,11 @@ class _WelcomePageState extends State { width: smallest * 0.5, height: smallest * 0.5), Text( - 'Made to share', + AppLocalizations.of(context)!.page3Title, style: textTheme.displaySmall, ), - Text('Collaborate on your shopping list in real time', + Text( + AppLocalizations.of(context)!.page3Subtitle, style: textTheme.bodyMedium) ], ), @@ -113,10 +119,11 @@ class _WelcomePageState extends State { width: smallest * 0.5, height: smallest * 0.5), Text( - 'Pocket-sized', + AppLocalizations.of(context)!.page4Title, style: textTheme.displaySmall, ), - Text('Always have your shopping list with you', + Text( + AppLocalizations.of(context)!.page4Subtitle, style: textTheme.bodyMedium) ], ), @@ -126,7 +133,8 @@ class _WelcomePageState extends State { onPressed: () { context.goNamed('signin'); }, - child: const Text('I already have an account'), + child: Text(AppLocalizations.of(context)!.userHasAnAccount + ), ) ], ), diff --git a/lib/tools/fetch_wrapper.dart b/lib/tools/fetch_wrapper.dart index 2897ad2..4aed109 100644 --- a/lib/tools/fetch_wrapper.dart +++ b/lib/tools/fetch_wrapper.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:outbag_app/backend/errors.dart'; import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/tools/snackbar.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; void doNetworkRequest(ScaffoldMessengerState? sm, {required Future Function() req, @@ -11,6 +12,8 @@ void doNetworkRequest(ScaffoldMessengerState? sm, Function()? onAnyErr, Function()? after, bool Function(Map)? onServerErr}) async { + AppLocalizations? trans = (sm!=null)?AppLocalizations.of(sm.context):null; + Response res; try { res = await req(); @@ -23,7 +26,7 @@ void doNetworkRequest(ScaffoldMessengerState? sm, } if (showBar && sm != null) { - showSimpleSnackbar(sm, text: 'Network Error', action: 'Dismiss'); + showSimpleSnackbar(sm, text: trans!.errorNetwork, action: trans.dismiss); } if (onAnyErr != null) { onAnyErr(); @@ -47,7 +50,7 @@ void doNetworkRequest(ScaffoldMessengerState? sm, showBar = onServerErr(res.body); } if (showBar && sm != null) { - showSimpleSnackbar(sm, text: errorAsString(res.body), action: 'OK'); + showSimpleSnackbar(sm, text: errorAsString(res.body, trans!), action: trans.ok); } if (onAnyErr != null) { onAnyErr(); @@ -61,7 +64,7 @@ void doNetworkRequest(ScaffoldMessengerState? sm, } if (showBar && sm != null) { - showSimpleSnackbar(sm, text: 'Unknown Error', action: 'OK'); + showSimpleSnackbar(sm, text: trans!.errorUnknown, action: trans.ok); } } diff --git a/pubspec.lock b/pubspec.lock index ca4d5e1..742eaa1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -134,6 +134,11 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_svg: dependency: "direct main" description: @@ -184,6 +189,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.15" + intl: + dependency: "direct main" + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2c0fee9..07e8e6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,9 @@ dependencies: crypto: ^3.0.2 provider: ^6.0.5 go_router: ^6.5.0 + flutter_localizations: + sdk: flutter + intl: any dev_dependencies: flutter_test: @@ -54,7 +57,8 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - + # needed for l10n localizations + generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class.