From 78756dda7f73d43ec40ce7d8b8ac0d064e7c2e9e Mon Sep 17 00:00:00 2001 From: Sarah Sporck Date: Fri, 21 Jul 2023 11:22:27 +0200 Subject: [PATCH 01/60] 1039: add mime type text/csv for ff ubuntu --- administration/src/bp-modules/cards/ImportCardsInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administration/src/bp-modules/cards/ImportCardsInput.tsx b/administration/src/bp-modules/cards/ImportCardsInput.tsx index 1014fda25..8eff30334 100644 --- a/administration/src/bp-modules/cards/ImportCardsInput.tsx +++ b/administration/src/bp-modules/cards/ImportCardsInput.tsx @@ -123,7 +123,7 @@ const ImportCardsInput = ({ setCardBlueprints, lineToBlueprint, headers }: Impor description={} action={ - + } /> From 4b82a2e353abb2e9c43f86d1b4b9afac8f392485 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 5 Oct 2023 09:56:24 +0200 Subject: [PATCH 02/60] release-3.2.1: update version --- administration/package.json | 2 +- frontend/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/administration/package.json b/administration/package.json index 2219f2eb8..65c1a43ec 100644 --- a/administration/package.json +++ b/administration/package.json @@ -1,6 +1,6 @@ { "name": "administration", - "version": "3.2.0", + "version": "3.2.1", "private": true, "dependenciesComments": { "typescript": "Keeping on 5.0.x because of current incompatibility of 5.1.x with @typescript-eslint" diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 051c8566c..a17540dac 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 3.2.0+113 +version: 3.2.1+114 environment: sdk: 3.0.2 From 46281e75b38a634981051c3319249a590fc64116 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 5 Oct 2023 09:58:13 +0200 Subject: [PATCH 03/60] release-3.2.1: update changelog --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b57ac6f4d..552dc522d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed ### Removed +# [3.2.1] 2023-10-05 + +### Added +* 1102: Add hint when application deleted by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1107 +* 1067 Implement sentry by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1111 +* 1114: Sort applications by status by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1117 +* 975: Show more user information by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1120 +* 1123: Add oberallgaeu by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1124 +* 1075: Create card from application by @sarahsporck in https://github.com/digitalfabrik/entitlementcard/pull/1091 + +### fixed +* 1108: Prevent auto install entitlementcard by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1109 +* 998: Improve color api switch by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1110 +* [fix]: add partial index for admin email by @sarahsporck in https://github.com/digitalfabrik/entitlementcard/pull/1116 +* 1115: Trim search text by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1118 + +### changed +* 1100: Use single quotes by @steffenkleinle in https://github.com/digitalfabrik/entitlementcard/pull/1101 +* build(deps): bump activesupport from 6.1.7.3 to 6.1.7.6 in /frontend/ios by @dependabot in https://github.com/digitalfabrik/entitlementcard/pull/1099 +* Extract linting rules into separate file by @sarahsporck in https://github.com/digitalfabrik/entitlementcard/pull/1104 +* Upgrade backend dependencies by @michael-markl in https://github.com/digitalfabrik/entitlementcard/pull/1112 +* 1044: Only digital card default checked by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1119 +* build(deps): bump graphql from 16.7.1 to 16.8.1 in /administration by @dependabot in https://github.com/digitalfabrik/entitlementcard/pull/1125 + # [3.2.0] 2023-08-23 ### Added From 514197e348ab51f7a72c5f272ec850944ef12242 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 6 Oct 2023 13:00:21 +0200 Subject: [PATCH 04/60] 1084: add userCodesModel & store, add carousel, add delete card dialig, adjust weiteren Aktionen --- frontend/build-configs/bayern/index.ts | 1 + frontend/build-configs/bayern/localization.ts | 10 +- frontend/build-configs/nuernberg/index.ts | 1 + .../build-configs/nuernberg/localization.ts | 10 +- frontend/build-configs/types.ts | 7 ++ frontend/lib/about/backend_switch_dialog.dart | 6 +- frontend/lib/about/dev_settings_view.dart | 31 +++--- frontend/lib/app.dart | 4 +- .../activation_code_scanner_page.dart | 16 +++- .../activation_existing_card_dialog.dart | 38 ++++++++ .../card_detail_view/card_carousel.dart | 60 ++++++++++++ .../card_detail_view/card_detail_view.dart | 29 +++--- .../card_detail_view/more_actions_dialog.dart | 11 +++ .../card_detail_view/self_verify_card.dart | 18 ++-- .../verification_code_view.dart | 4 +- .../identification/identification_page.dart | 57 ++++++++--- .../lib/identification/user_code_model.dart | 3 +- .../lib/identification/user_code_store.dart | 3 +- .../lib/identification/user_codes_model.dart | 60 ++++++++++++ .../lib/identification/user_codes_store.dart | 34 +++++++ .../remove_card_confirmation_dialog.dart | 94 +++++++++++++++++++ .../verification_qr_scanner_page.dart | 7 +- frontend/pubspec.lock | 8 ++ frontend/pubspec.yaml | 1 + 24 files changed, 442 insertions(+), 71 deletions(-) create mode 100644 frontend/lib/identification/activation_workflow/activation_existing_card_dialog.dart create mode 100644 frontend/lib/identification/card_detail_view/card_carousel.dart create mode 100644 frontend/lib/identification/user_codes_model.dart create mode 100644 frontend/lib/identification/user_codes_store.dart create mode 100644 frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart diff --git a/frontend/build-configs/bayern/index.ts b/frontend/build-configs/bayern/index.ts index 220e7954b..3765b0069 100644 --- a/frontend/build-configs/bayern/index.ts +++ b/frontend/build-configs/bayern/index.ts @@ -92,6 +92,7 @@ export const bayernCommon: CommonBuildConfigType = { publisherText, disclaimerText, localization, + maxCardAmount: 1 } let bayern: BuildConfigType = { diff --git a/frontend/build-configs/bayern/localization.ts b/frontend/build-configs/bayern/localization.ts index 5d0d91f22..f66614044 100644 --- a/frontend/build-configs/bayern/localization.ts +++ b/frontend/build-configs/bayern/localization.ts @@ -24,11 +24,17 @@ const localization: LocalizationType = { moreActions: { applyForAnotherCardTitle: "Ehrenamtskarte beantragen oder verlängern", applyForAnotherCardDescription: "Ihre hinterlegte Karte bleibt erhalten.", - activateAnotherCardTitle: "Anderen Aktivierungscode einscannen", - activateAnotherCardDescription: "Dadurch wird die hinterlegte Karte vom Gerät gelöscht.", + activateAnotherCardTitle: "Weitere Ehrenamtskarte hinzufügen", + activateAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten. Sie können diesen manuell entfernen", verifyTitle: "Eine digitale Ehrenamtskarte prüfen", verifyDescription: "Prüfen Sie die Gültigkeit einer digitalen Ehrenamtskarte.", + removeCardTitle: "Diese Ehrenamtskarte löschen", + removeCardDescription: "Nach der Auswahl wird diese Ehrenamtskarte vom Gerät gelöscht", }, + removeCardDialog: { + title: "Diese Karte löschen?", + description: "Wenn diese Karte gelöscht wird, muss sie vor der Wiederverwendung neu aktiviert werden.", + } }, } diff --git a/frontend/build-configs/nuernberg/index.ts b/frontend/build-configs/nuernberg/index.ts index 11a2393d7..ec809221e 100644 --- a/frontend/build-configs/nuernberg/index.ts +++ b/frontend/build-configs/nuernberg/index.ts @@ -94,6 +94,7 @@ export const nuernbergCommon: CommonBuildConfigType = { publisherText, disclaimerText, localization, + maxCardAmount: 1 } let nuernberg: BuildConfigType = { diff --git a/frontend/build-configs/nuernberg/localization.ts b/frontend/build-configs/nuernberg/localization.ts index a04732caa..b99c25b7b 100644 --- a/frontend/build-configs/nuernberg/localization.ts +++ b/frontend/build-configs/nuernberg/localization.ts @@ -24,11 +24,17 @@ const localization: LocalizationType = { moreActions: { applyForAnotherCardTitle: "Nürnberg-Pass beantragen oder verlängern", applyForAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten.", - activateAnotherCardTitle: "Anderen Aktivierungscode einscannen", - activateAnotherCardDescription: "Dadurch wird der hinterlegte Pass vom Gerät gelöscht.", + activateAnotherCardTitle: "Weiteren Nürnberg-Pass hinzufügen", + activateAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten. Sie können diesen manuell entfernen", verifyTitle: "Einen Nürnberg-Pass prüfen", verifyDescription: "Prüfen Sie die Gültigkeit eines Nürnberg-Passes.", + removeCardTitle: "Diesen Nürnberg-Pass löschen", + removeCardDescription: "Nach der Auswahl wird der hinterlegte Pass vom Gerät gelöscht", }, + removeCardDialog: { + title: "Diese Karte löschen?", + description: "Wenn diese Karte gelöscht wird, muss sie vor der Wiederverwendung neu aktiviert werden.", + } }, } diff --git a/frontend/build-configs/types.ts b/frontend/build-configs/types.ts index 186e92b72..bcb3e7c42 100644 --- a/frontend/build-configs/types.ts +++ b/frontend/build-configs/types.ts @@ -44,6 +44,12 @@ export type LocalizationType = { activateAnotherCardDescription: string verifyTitle: string verifyDescription: string + removeCardTitle: string + removeCardDescription: string + } + removeCardDialog: { + title: string + description: string } } } @@ -117,6 +123,7 @@ export type CommonBuildConfigType = { publisherText: string disclaimerText: string localization: LocalizationType + maxCardAmount: number } export type AndroidBuildConfigType = CommonBuildConfigType & { diff --git a/frontend/lib/about/backend_switch_dialog.dart b/frontend/lib/about/backend_switch_dialog.dart index 60603ea62..9ad0da626 100644 --- a/frontend/lib/about/backend_switch_dialog.dart +++ b/frontend/lib/about/backend_switch_dialog.dart @@ -1,6 +1,6 @@ import 'package:ehrenamtskarte/configuration/configuration.dart'; import 'package:ehrenamtskarte/configuration/settings_model.dart'; -import 'package:ehrenamtskarte/identification/user_code_model.dart'; +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -84,8 +84,8 @@ class BackendSwitchDialogState extends State { void clearData() { final settings = Provider.of(context, listen: false); - final card = Provider.of(context, listen: false); + final userCodes = Provider.of(context, listen: false); settings.clearSettings(); - card.removeCode(); + userCodes.removeCodes(); } } diff --git a/frontend/lib/about/dev_settings_view.dart b/frontend/lib/about/dev_settings_view.dart index 8612fb78c..e60d7a104 100644 --- a/frontend/lib/about/dev_settings_view.dart +++ b/frontend/lib/about/dev_settings_view.dart @@ -9,10 +9,9 @@ import 'package:ehrenamtskarte/graphql/graphql_api.graphql.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activate_code.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activation_code_parser.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activation_exception.dart'; -import 'package:ehrenamtskarte/identification/card_detail_view/self_verify_card.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_processor.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_parsing_error_dialog.dart'; -import 'package:ehrenamtskarte/identification/user_code_model.dart'; +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; import 'package:ehrenamtskarte/identification/util/card_info_utils.dart'; import 'package:ehrenamtskarte/intro_slides/intro_screen.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; @@ -22,6 +21,8 @@ import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; +import '../identification/card_detail_view/self_verify_card.dart'; + // this data includes a Base32 encoded random key created with openssl // for testing, so this is intended final sampleActivationCodeBavaria = DynamicUserCode() @@ -53,14 +54,14 @@ class DevSettingsView extends StatelessWidget { Widget build(BuildContext context) { final settings = Provider.of(context); final client = GraphQLProvider.of(context).value; - final userCodeModel = Provider.of(context); + final userCodesModel = Provider.of(context); return Padding( padding: const EdgeInsets.all(15.0), child: Column( children: [ ListTile( - title: const Text('Reset card'), - onTap: () => _resetEakData(context), + title: const Text('Reset cards'), + onTap: () => _resetEakData(context, userCodesModel), ), ListTile( title: const Text('Set (invalid) sample card'), @@ -79,9 +80,11 @@ class DevSettingsView extends StatelessWidget { onTap: () => _setExpiredLastVerification(context), ), ListTile( - title: const Text('Trigger self-verification'), - onTap: () => selfVerifyCard(userCodeModel, Configuration.of(context).projectId, client), - ), + title: const Text('Trigger self-verification'), + onTap: () => { + userCodesModel.userCodes! + .map((code) => selfVerifyCard(context, code, Configuration.of(context).projectId, client)) + }), ListTile( title: const Text('Log sample exception'), onTap: () => log('Sample exception.', error: Exception('Sample exception...')), @@ -101,8 +104,8 @@ class DevSettingsView extends StatelessWidget { ); } - Future _resetEakData(BuildContext context) async { - Provider.of(context, listen: false).removeCode(); + Future _resetEakData(BuildContext context, UserCodesModel userCodes) async { + userCodes.removeCodes(); } DynamicUserCode _determineUserCode(String projectId) { @@ -123,7 +126,7 @@ class DevSettingsView extends StatelessWidget { } Future _setSampleCard(BuildContext context) async { - Provider.of(context, listen: false).setCode(_determineUserCode(buildConfig.projectId.local)); + Provider.of(context, listen: false).setCode(_determineUserCode(buildConfig.projectId.local)); } Future _showRawCardInput(BuildContext context) async { @@ -168,7 +171,7 @@ class DevSettingsView extends StatelessWidget { Future _activateCard(BuildContext context, String base64qrcode) async { final messengerState = ScaffoldMessenger.of(context); - final provider = Provider.of(context, listen: false); + final provider = Provider.of(context, listen: false); final client = GraphQLProvider.of(context).value; final projectId = Configuration.of(context).projectId; try { @@ -235,8 +238,8 @@ class DevSettingsView extends StatelessWidget { // This is used to check the invalidation of a card because the verification with the backend couldn't be done lately (1 week plus UTC tolerance) void _setExpiredLastVerification(BuildContext context) { - final provider = Provider.of(context, listen: false); - final DynamicUserCode userCode = provider.userCode!; + final provider = Provider.of(context, listen: false); + final DynamicUserCode userCode = provider.userCodes!.first; final CardVerification cardVerification = CardVerification() ..verificationTimeStamp = secondsSinceEpoch(DateTime.now().toUtc().subtract(Duration(seconds: cardValidationExpireSeconds + 3600))) diff --git a/frontend/lib/app.dart b/frontend/lib/app.dart index fb60773e3..2d3968c15 100644 --- a/frontend/lib/app.dart +++ b/frontend/lib/app.dart @@ -3,7 +3,7 @@ import 'package:ehrenamtskarte/configuration/configuration.dart'; import 'package:ehrenamtskarte/configuration/definitions.dart'; import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/graphql/configured_graphql_provider.dart'; -import 'package:ehrenamtskarte/identification/user_code_model.dart'; +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; import 'package:ehrenamtskarte/intro_slides/intro_screen.dart'; import 'package:ehrenamtskarte/themes.dart'; import 'package:flutter/foundation.dart'; @@ -60,7 +60,7 @@ class App extends StatelessWidget { showDevSettings: kDebugMode, child: ConfiguredGraphQlProvider( child: ChangeNotifierProvider( - create: (context) => UserCodeModel()..initialize(), + create: (context) => UserCodesModel()..initialize(), child: MaterialApp( theme: lightTheme, darkTheme: darkTheme, diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 2232874c9..c8575573a 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -7,12 +7,13 @@ import 'package:ehrenamtskarte/graphql/graphql_api.graphql.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activate_code.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activation_code_parser.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activation_exception.dart'; +import 'package:ehrenamtskarte/identification/activation_workflow/activation_existing_card_dialog.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activation_overwrite_existing_dialog.dart'; import 'package:ehrenamtskarte/identification/connection_failed_dialog.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_processor.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_scanner_page.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_parsing_error_dialog.dart'; -import 'package:ehrenamtskarte/identification/user_code_model.dart'; +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; import 'package:ehrenamtskarte/identification/util/card_info_utils.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/verification_qr_code_processor.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; @@ -86,7 +87,7 @@ class ActivationCodeScannerPage extends StatelessWidget { ]) async { final client = GraphQLProvider.of(context).value; final projectId = Configuration.of(context).projectId; - final provider = Provider.of(context, listen: false); + final provider = Provider.of(context, listen: false); final activationSecretBase64 = const Base64Encoder().convert(activationCode.activationSecret); final cardInfoBase64 = activationCode.info.hash(activationCode.pepper); @@ -107,15 +108,20 @@ class ActivationCodeScannerPage extends StatelessWidget { throw const ActivationInvalidTotpSecretException(); } final totpSecret = const Base64Decoder().convert(activationResult.totpSecret!); - debugPrint('Card Activation: Successfully activated.'); - provider.setCode(DynamicUserCode() + DynamicUserCode userCode = DynamicUserCode() ..info = activationCode.info ..pepper = activationCode.pepper ..totpSecret = totpSecret ..cardVerification = (CardVerification() ..cardValid = true - ..verificationTimeStamp = secondsSinceEpoch(DateTime.parse(activationResult.activationTimeStamp)))); + ..verificationTimeStamp = secondsSinceEpoch(DateTime.parse(activationResult.activationTimeStamp))); + if (provider.userCodes != null && provider.isAlreadyInList(provider.userCodes!, userCode)) { + await ActivationExistingCardDialog.showExistingCardDialog(context); + } + provider.setCode(userCode); + debugPrint('Card Activation: Successfully activated.'); + break; case ActivationState.failed: await QrParsingErrorDialog.showErrorDialog( diff --git a/frontend/lib/identification/activation_workflow/activation_existing_card_dialog.dart b/frontend/lib/identification/activation_workflow/activation_existing_card_dialog.dart new file mode 100644 index 000000000..630dd5521 --- /dev/null +++ b/frontend/lib/identification/activation_workflow/activation_existing_card_dialog.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class ActivationExistingCardDialog extends StatelessWidget { + const ActivationExistingCardDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Diese Karte existiert bereits', style: TextStyle(fontSize: 18)), + content: SingleChildScrollView( + child: ListBody( + children: const [ + Text( + 'Diese Karte ist bereits auf ihrem Gerät aktiv.', + ), + ], + ), + ), + actions: [ + TextButton( + child: const Text('Ok'), + onPressed: () { + Navigator.of(context).pop(false); + }, + ) + ], + ); + } + + /// Returns true, if the user wants to activate an existing card + static Future showExistingCardDialog(BuildContext context) async { + return await showDialog( + context: context, + builder: (context) => ActivationExistingCardDialog(), + ) ?? + false; + } +} diff --git a/frontend/lib/identification/card_detail_view/card_carousel.dart b/frontend/lib/identification/card_detail_view/card_carousel.dart new file mode 100644 index 000000000..b6cf1ee72 --- /dev/null +++ b/frontend/lib/identification/card_detail_view/card_carousel.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:carousel_slider/carousel_slider.dart'; + +class CardCarousel extends StatefulWidget { + final List userCards; + final int cardIndex; + final Function(int index) updateIndex; + + const CardCarousel({super.key, required this.userCards, required this.cardIndex, required this.updateIndex}); + + @override + CardCarouselState createState() => CardCarouselState(); +} + +class CardCarouselState extends State { + final CarouselController _controller = CarouselController(); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Column( + children: [ + CarouselSlider( + items: widget.userCards, + carouselController: _controller, + options: CarouselOptions( + enlargeCenterPage: true, + enableInfiniteScroll: false, + viewportFraction: 0.98, + aspectRatio: 9 / 16, + onPageChanged: (index, reason) { + setState(() { + widget.updateIndex(index); + }); + }), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: widget.userCards.asMap().entries.map((entry) { + return GestureDetector( + onTap: () => _controller.animateToPage(entry.key), + child: Container( + width: 12.0, + height: 12.0, + margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black) + .withOpacity(widget.cardIndex == entry.key ? 0.9 : 0.4)), + ), + ); + }).toList(), + ), + ), + ], + )); + } +} diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index 7724e34c2..ecaccee18 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -3,13 +3,13 @@ import 'package:ehrenamtskarte/graphql/graphql_api.dart'; import 'package:ehrenamtskarte/identification/card_detail_view/more_actions_dialog.dart'; import 'package:ehrenamtskarte/identification/card_detail_view/self_verify_card.dart'; import 'package:ehrenamtskarte/identification/id_card/id_card.dart'; +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; import 'package:ehrenamtskarte/identification/util/card_info_utils.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; -import '../user_code_model.dart'; import 'verification_code_view.dart'; class CardDetailView extends StatefulWidget { @@ -17,14 +17,15 @@ class CardDetailView extends StatefulWidget { final VoidCallback startActivation; final VoidCallback startVerification; final VoidCallback startApplication; + final VoidCallback removeCard; - const CardDetailView({ - super.key, - required this.userCode, - required this.startActivation, - required this.startVerification, - required this.startApplication, - }); + const CardDetailView( + {super.key, + required this.userCode, + required this.startActivation, + required this.startVerification, + required this.startApplication, + required this.removeCard}); @override State createState() => _CardDetailViewState(); @@ -49,10 +50,10 @@ class _CardDetailViewState extends State { } Future _selfVerifyCard() async { - final userCodeModel = Provider.of(context, listen: false); + final userCodesModel = Provider.of(context, listen: false); final projectId = Configuration.of(context).projectId; final client = GraphQLProvider.of(context).value; - selfVerifyCard(userCodeModel, projectId, client); + userCodesModel.userCodes!.map((code) => selfVerifyCard(context, code, projectId, client)); } @override @@ -129,10 +130,10 @@ class _CardDetailViewState extends State { showDialog( context: context, builder: (context) => MoreActionsDialog( - startActivation: widget.startActivation, - startApplication: widget.startApplication, - startVerification: widget.startVerification, - ), + startActivation: widget.startActivation, + startApplication: widget.startApplication, + startVerification: widget.startVerification, + removeCard: widget.removeCard), ); } } diff --git a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart index 241671b2a..91e3c0377 100644 --- a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart +++ b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart @@ -5,12 +5,14 @@ class MoreActionsDialog extends StatelessWidget { final VoidCallback startActivation; final VoidCallback startVerification; final VoidCallback startApplication; + final VoidCallback removeCard; const MoreActionsDialog({ super.key, required this.startActivation, required this.startVerification, required this.startApplication, + required this.removeCard, }); @override @@ -50,6 +52,15 @@ class MoreActionsDialog extends StatelessWidget { startVerification(); }, ), + ListTile( + title: Text(localization.removeCardTitle), + subtitle: Text(localization.removeCardDescription), + leading: const Icon(Icons.delete, size: 36), + onTap: () { + Navigator.pop(context); + removeCard(); + }, + ), ], ), ), diff --git a/frontend/lib/identification/card_detail_view/self_verify_card.dart b/frontend/lib/identification/card_detail_view/self_verify_card.dart index 1aee9b3eb..0efbfd498 100644 --- a/frontend/lib/identification/card_detail_view/self_verify_card.dart +++ b/frontend/lib/identification/card_detail_view/self_verify_card.dart @@ -1,14 +1,16 @@ +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; import 'package:flutter/widgets.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; +import 'package:provider/provider.dart'; import '../../proto/card.pb.dart'; import '../../util/date_utils.dart'; import '../otp_generator.dart'; -import '../user_code_model.dart'; import '../verification_workflow/query_server_verification.dart'; -Future selfVerifyCard(UserCodeModel userCodeModel, String projectId, GraphQLClient client) async { - final userCode = userCodeModel.userCode; +Future selfVerifyCard( + BuildContext context, DynamicUserCode? userCode, String projectId, GraphQLClient client) async { + final userCodesModel = Provider.of(context, listen: false); if (userCode == null) { return; } @@ -25,14 +27,14 @@ Future selfVerifyCard(UserCodeModel userCodeModel, String projectId, Graph await queryDynamicServerVerification(client, projectId, qrCode); // If the user code has changed during the server request, we abort. - if (userCodeModel.userCode != userCode) { - debugPrint('Card Self-Verification: The user code has been changed during server request for the old user code.'); - return; - } + // if (userCodeModel.userCode != userCode) { + // debugPrint('Card Self-Verification: The user code has been changed during server request for the old user code.'); + // return; + // } debugPrint("Card Self-Verification: Persisting response. Card is ${cardVerification.valid ? "valid." : "INVALID."}"); - userCodeModel.setCode(DynamicUserCode() + userCodesModel.setCode(DynamicUserCode() ..info = userCode.info ..ecSignature = userCode.ecSignature ..pepper = userCode.pepper diff --git a/frontend/lib/identification/card_detail_view/verification_code_view.dart b/frontend/lib/identification/card_detail_view/verification_code_view.dart index 91ca85761..3372cd225 100644 --- a/frontend/lib/identification/card_detail_view/verification_code_view.dart +++ b/frontend/lib/identification/card_detail_view/verification_code_view.dart @@ -4,7 +4,7 @@ import 'dart:math'; import 'package:ehrenamtskarte/identification/card_detail_view/animated_progressbar.dart'; import 'package:ehrenamtskarte/identification/otp_generator.dart'; import 'package:ehrenamtskarte/identification/qr_content_parser.dart'; -import 'package:ehrenamtskarte/identification/user_code_model.dart'; +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -56,7 +56,7 @@ class VerificationCodeViewState extends State { return LayoutBuilder( builder: (context, constraints) { final padding = min(constraints.maxWidth, constraints.maxHeight) < 400 ? 12.0 : 24.0; - return Consumer( + return Consumer( builder: (context, cardDetailsModel, child) { final qrCode = qr.QrCode.fromUint8List( data: createDynamicVerificationQrCodeData(userCode, otpCode.code), diff --git a/frontend/lib/identification/identification_page.dart b/frontend/lib/identification/identification_page.dart index 6dc0c0d66..8fc189460 100644 --- a/frontend/lib/identification/identification_page.dart +++ b/frontend/lib/identification/identification_page.dart @@ -1,10 +1,12 @@ import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activation_code_scanner_page.dart'; +import 'package:ehrenamtskarte/identification/card_detail_view/card_carousel.dart'; import 'package:ehrenamtskarte/identification/card_detail_view/card_detail_view.dart'; import 'package:ehrenamtskarte/identification/no_card_view.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart'; -import 'package:ehrenamtskarte/identification/user_code_model.dart'; +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/verification_workflow.dart'; import 'package:ehrenamtskarte/routing.dart'; import 'package:flutter/material.dart'; @@ -12,29 +14,41 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; -class IdentificationPage extends StatelessWidget { +class IdentificationPage extends StatefulWidget { final String title; - const IdentificationPage({super.key, required this.title}); + @override + IdentificationPageState createState() => IdentificationPageState(); +} + +class IdentificationPageState extends State { + int cardIndex = 0; @override Widget build(BuildContext context) { final settings = Provider.of(context); - return Consumer( - builder: (context, userCodeModel, child) { - if (!userCodeModel.isInitialized) { + return Consumer( + builder: (context, userCodesModel, child) { + if (!userCodesModel.isInitialized) { return Container(); } - final userCode = userCodeModel.userCode; - if (userCode != null) { - return CardDetailView( - userCode: userCode, - startVerification: () => _showVerificationDialog(context, settings), - startActivation: () => _startActivation(context), - startApplication: _startApplication, - ); + if (userCodesModel.userCodes != null && userCodesModel.userCodes!.isNotEmpty) { + final List cards = []; + userCodesModel.userCodes?.forEach((element) { + cards.add(CardDetailView( + userCode: element, + startVerification: () => _showVerificationDialog(context, settings), + startActivation: () => _startActivation(context), + startApplication: _startApplication, + removeCard: () => _removeCard(context), + )); + }); + + return Column(children: [ + CardCarousel(userCards: cards, cardIndex: cardIndex, updateIndex: _updateCardIndex), + ]); } return NoCardView( @@ -72,4 +86,19 @@ class IdentificationPage extends StatelessWidget { mode: LaunchMode.externalApplication, ); } + + Future _removeCard(BuildContext context) async { + final userCodesModel = Provider.of(context, listen: false); + + await RemoveCardConfirmationDialog.show( + context: context, + userCode: userCodesModel.userCodes![cardIndex], + isLastItem: userCodesModel.userCodes!.length == 1); + } + + Future _updateCardIndex(int index) async { + setState(() { + cardIndex = index; + }); + } } diff --git a/frontend/lib/identification/user_code_model.dart b/frontend/lib/identification/user_code_model.dart index 66c2577ce..0909fe52a 100644 --- a/frontend/lib/identification/user_code_model.dart +++ b/frontend/lib/identification/user_code_model.dart @@ -3,7 +3,8 @@ import 'dart:developer'; import 'package:ehrenamtskarte/identification/user_code_store.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/foundation.dart'; - +// TODO remove +@Deprecated('is needed to load old card data to new structure') class UserCodeModel extends ChangeNotifier { DynamicUserCode? _userCode; bool _isInitialized = false; diff --git a/frontend/lib/identification/user_code_store.dart b/frontend/lib/identification/user_code_store.dart index 78cc3c11b..e4eac1dd5 100644 --- a/frontend/lib/identification/user_code_store.dart +++ b/frontend/lib/identification/user_code_store.dart @@ -4,7 +4,8 @@ import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; const userCodeBase64Key = 'userCodeBase64'; - +// TODO remove when import is finished +@Deprecated('is needed to load old card data to new structure') class UserCodeStore { const UserCodeStore(); diff --git a/frontend/lib/identification/user_codes_model.dart b/frontend/lib/identification/user_codes_model.dart new file mode 100644 index 000000000..26f935d01 --- /dev/null +++ b/frontend/lib/identification/user_codes_model.dart @@ -0,0 +1,60 @@ +import 'dart:developer'; + +import 'package:ehrenamtskarte/identification/user_codes_store.dart'; +import 'package:ehrenamtskarte/proto/card.pb.dart'; +import 'package:flutter/foundation.dart'; + +class UserCodesModel extends ChangeNotifier { + List? _userCodes = []; + bool _isInitialized = false; + + List? get userCodes { + return _userCodes; + } + + bool get isInitialized { + return _isInitialized; + } + + Future initialize() async { + if (_isInitialized) { + return; + } + try { + _userCodes = await const UserCodesStore().load(); + } on Exception catch (e) { + log('Failed to initialize activation code from secure storage of codes.', error: e); + } finally { + _isInitialized = true; + notifyListeners(); + } + } + + void setCode(DynamicUserCode code) { + List userCodes = _userCodes!; + if (UserCodesModel().isAlreadyInList(userCodes, code)) return; + userCodes.add(code); + const UserCodesStore().store(userCodes); + _userCodes = userCodes; + notifyListeners(); + } + + // + bool isAlreadyInList(List userCodes, DynamicUserCode code) { + return userCodes.map((userCode) => userCode.info).contains(code.info); + } + + void removeCode(DynamicUserCode code) { + List userCodes = _userCodes!; + userCodes.remove(code); + const UserCodesStore().store(userCodes); + _userCodes = userCodes; + notifyListeners(); + } + + void removeCodes() { + const UserCodesStore().remove(); + _userCodes = []; + notifyListeners(); + } +} diff --git a/frontend/lib/identification/user_codes_store.dart b/frontend/lib/identification/user_codes_store.dart new file mode 100644 index 000000000..281226ec6 --- /dev/null +++ b/frontend/lib/identification/user_codes_store.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; + +import 'package:ehrenamtskarte/proto/card.pb.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +const userCodeBase64Key = 'userCodeBase64'; + +class UserCodesStore { + const UserCodesStore(); + + static const _userCodesBase64Key = 'userCodesBase64'; + + Future store(List userCodes) async { + const storage = FlutterSecureStorage(); + Iterable userCodeString = userCodes.map((code) => const Base64Encoder().convert(code.writeToBuffer())); + + await storage.write(key: _userCodesBase64Key, value: userCodeString.join(',')); + } + + Future remove() async { + const storage = FlutterSecureStorage(); + await storage.delete(key: _userCodesBase64Key); + } + + Future?> load() async { + const storage = FlutterSecureStorage(); + final String? userCodesBase64 = await storage.read(key: _userCodesBase64Key); + if (userCodesBase64 == null) return null; + return userCodesBase64 + .split(',') + .map((code) => DynamicUserCode.fromBuffer(const Base64Decoder().convert(code))) + .toList(); + } +} diff --git a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart new file mode 100644 index 000000000..47e7688c8 --- /dev/null +++ b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart @@ -0,0 +1,94 @@ +import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; +import 'package:ehrenamtskarte/configuration/configuration.dart'; +import 'package:ehrenamtskarte/graphql/graphql_api.dart'; +import 'package:ehrenamtskarte/identification/id_card/id_card.dart'; +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/proto/card.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; +import 'package:provider/provider.dart'; + +class RemoveCardConfirmationDialog extends StatefulWidget { + final DynamicUserCode userCode; + final bool isLastItem; + + const RemoveCardConfirmationDialog({super.key, required this.userCode, required this.isLastItem}); + + static Future show({ + required BuildContext context, + required DynamicUserCode userCode, + required bool isLastItem, + }) => + showDialog( + context: context, + builder: (_) => RemoveCardConfirmationDialog(userCode: userCode, isLastItem: isLastItem), + ); + + @override + RemoveCardConfirmationDialogState createState() => RemoveCardConfirmationDialogState(); +} + +class RemoveCardConfirmationDialogState extends State { + @override + Widget build(BuildContext context) { + final localization = buildConfig.localization.identification.removeCardDialog; + final projectId = Configuration.of(context).projectId; + final regionsQuery = GetRegionsByIdQuery( + variables: GetRegionsByIdArguments( + project: projectId, + ids: [widget.userCode.info.extensions.extensionRegion.regionId], + ), + ); + + return Query( + options: QueryOptions(document: regionsQuery.document, variables: regionsQuery.getVariablesMap()), + builder: (result, {refetch, fetchMore}) { + final data = result.data; + final theme = Theme.of(context); + final region = result.isConcrete && data != null ? regionsQuery.parse(data).regionsByIdInProject[0] : null; + return AlertDialog( + title: ListTile( + leading: Icon(Icons.warning, color: theme.colorScheme.primaryContainer, size: 30), + title: Text(localization.title, style: TextStyle(fontSize: 18)), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 20), + child: Text(localization.description, style: TextStyle(fontSize: 12))), + IdCard( + cardInfo: widget.userCode.info, + region: region != null ? Region(region.prefix, region.name) : null, + // We trust the backend to have checked for expiration. + isExpired: false, + isNotYetValid: false, + ), + ], + ), + ), + actions: [ + TextButton( + child: const Text('Abbrechen'), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + TextButton( + child: const Text('Löschen'), + onPressed: () { + final provider = Provider.of(context, listen: false); + if (widget.isLastItem) { + provider.removeCodes(); + } else { + provider.removeCode(widget.userCode); + } + Navigator.of(context).pop(false); + }, + ), + ], + ); + }, + ); + } +} diff --git a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart index cb5884a43..d7b80ffce 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart @@ -8,7 +8,7 @@ import 'package:ehrenamtskarte/identification/otp_generator.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_processor.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_scanner_page.dart'; import 'package:ehrenamtskarte/identification/qr_content_parser.dart'; -import 'package:ehrenamtskarte/identification/user_code_model.dart'; +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/verification_info_dialog.dart'; @@ -50,8 +50,9 @@ class VerificationQrScannerPage extends StatelessWidget { if (config.showDevSettings) TextButton( onPressed: () async { - final provider = Provider.of(context, listen: false); - final userCode = provider.userCode!; + final provider = Provider.of(context, listen: false); + // TODO use current card instead of first + final userCode = provider.userCodes!.first; final otp = OTPGenerator(userCode.totpSecret).generateOTP().code; final verificationQrCode = QrCode() ..dynamicVerificationCode = (DynamicVerificationCode() diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index f1e950b49..e816439c6 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.6.0" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: "9c695cc963bf1d04a47bd6021f68befce8970bcd61d24938e1fb0918cf5d9c42" + url: "https://pub.dev" + source: hosted + version: "4.2.1" characters: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 051c8566c..14749a6be 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: shared_preferences: ^2.0.18 tinycolor2: ^3.0.1 sentry_flutter: ^7.9.0 + carousel_slider: ^4.2.1 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. From 6d8ed100b9429325d9b6870994628ef91de80523 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 6 Oct 2023 15:30:34 +0200 Subject: [PATCH 05/60] 1084: resetCardIndex after delete, fix card carousel position issue, added max card limit --- frontend/build-configs/bayern/localization.ts | 4 +-- frontend/build-configs/nuernberg/index.ts | 2 +- .../build-configs/nuernberg/localization.ts | 4 +-- .../activation_code_scanner_page.dart | 2 +- .../card_detail_view/card_carousel.dart | 1 - .../card_detail_view/more_actions_dialog.dart | 25 ++++++++++++------- .../identification/identification_page.dart | 10 ++++---- .../lib/identification/user_code_model.dart | 3 ++- .../lib/identification/user_code_store.dart | 1 + .../lib/identification/user_codes_model.dart | 16 +++++++----- .../remove_card_confirmation_dialog.dart | 12 ++++----- 11 files changed, 46 insertions(+), 34 deletions(-) diff --git a/frontend/build-configs/bayern/localization.ts b/frontend/build-configs/bayern/localization.ts index f66614044..6c4ecdfc8 100644 --- a/frontend/build-configs/bayern/localization.ts +++ b/frontend/build-configs/bayern/localization.ts @@ -25,11 +25,11 @@ const localization: LocalizationType = { applyForAnotherCardTitle: "Ehrenamtskarte beantragen oder verlängern", applyForAnotherCardDescription: "Ihre hinterlegte Karte bleibt erhalten.", activateAnotherCardTitle: "Weitere Ehrenamtskarte hinzufügen", - activateAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten. Sie können diesen manuell entfernen", + activateAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten. Sie können diesen manuell entfernen.", verifyTitle: "Eine digitale Ehrenamtskarte prüfen", verifyDescription: "Prüfen Sie die Gültigkeit einer digitalen Ehrenamtskarte.", removeCardTitle: "Diese Ehrenamtskarte löschen", - removeCardDescription: "Nach der Auswahl wird diese Ehrenamtskarte vom Gerät gelöscht", + removeCardDescription: "Nach der Auswahl wird diese Ehrenamtskarte vom Gerät gelöscht.", }, removeCardDialog: { title: "Diese Karte löschen?", diff --git a/frontend/build-configs/nuernberg/index.ts b/frontend/build-configs/nuernberg/index.ts index ec809221e..ac2eb9db1 100644 --- a/frontend/build-configs/nuernberg/index.ts +++ b/frontend/build-configs/nuernberg/index.ts @@ -94,7 +94,7 @@ export const nuernbergCommon: CommonBuildConfigType = { publisherText, disclaimerText, localization, - maxCardAmount: 1 + maxCardAmount: 5, } let nuernberg: BuildConfigType = { diff --git a/frontend/build-configs/nuernberg/localization.ts b/frontend/build-configs/nuernberg/localization.ts index b99c25b7b..db24dcfc4 100644 --- a/frontend/build-configs/nuernberg/localization.ts +++ b/frontend/build-configs/nuernberg/localization.ts @@ -25,11 +25,11 @@ const localization: LocalizationType = { applyForAnotherCardTitle: "Nürnberg-Pass beantragen oder verlängern", applyForAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten.", activateAnotherCardTitle: "Weiteren Nürnberg-Pass hinzufügen", - activateAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten. Sie können diesen manuell entfernen", + activateAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten. Sie können diesen manuell entfernen.", verifyTitle: "Einen Nürnberg-Pass prüfen", verifyDescription: "Prüfen Sie die Gültigkeit eines Nürnberg-Passes.", removeCardTitle: "Diesen Nürnberg-Pass löschen", - removeCardDescription: "Nach der Auswahl wird der hinterlegte Pass vom Gerät gelöscht", + removeCardDescription: "Nach der Auswahl wird der hinterlegte Pass vom Gerät gelöscht.", }, removeCardDialog: { title: "Diese Karte löschen?", diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index c8575573a..0004bcd46 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -116,7 +116,7 @@ class ActivationCodeScannerPage extends StatelessWidget { ..cardVerification = (CardVerification() ..cardValid = true ..verificationTimeStamp = secondsSinceEpoch(DateTime.parse(activationResult.activationTimeStamp))); - if (provider.userCodes != null && provider.isAlreadyInList(provider.userCodes!, userCode)) { + if (provider.userCodes != null && isAlreadyInList(provider.userCodes!, userCode)) { await ActivationExistingCardDialog.showExistingCardDialog(context); } provider.setCode(userCode); diff --git a/frontend/lib/identification/card_detail_view/card_carousel.dart b/frontend/lib/identification/card_detail_view/card_carousel.dart index b6cf1ee72..57e51e853 100644 --- a/frontend/lib/identification/card_detail_view/card_carousel.dart +++ b/frontend/lib/identification/card_detail_view/card_carousel.dart @@ -24,7 +24,6 @@ class CardCarouselState extends State { items: widget.userCards, carouselController: _controller, options: CarouselOptions( - enlargeCenterPage: true, enableInfiniteScroll: false, viewportFraction: 0.98, aspectRatio: 9 / 16, diff --git a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart index 91e3c0377..ae5caff7a 100644 --- a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart +++ b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart @@ -1,5 +1,7 @@ import 'package:ehrenamtskarte/build_config/build_config.dart'; +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class MoreActionsDialog extends StatelessWidget { final VoidCallback startActivation; @@ -18,6 +20,9 @@ class MoreActionsDialog extends StatelessWidget { @override Widget build(BuildContext context) { final localization = buildConfig.localization.identification.moreActions; + final userCodesModel = Provider.of(context, listen: false); + final String availableCards = (buildConfig.maxCardAmount - userCodesModel.userCodes!.length).toString(); + final String maxCardAmount = buildConfig.maxCardAmount.toString(); return AlertDialog( contentPadding: const EdgeInsets.only(top: 12), title: const Text('Weitere Aktionen'), @@ -34,15 +39,6 @@ class MoreActionsDialog extends StatelessWidget { startApplication(); }, ), - ListTile( - title: Text(localization.activateAnotherCardTitle), - subtitle: Text(localization.activateAnotherCardDescription), - leading: const Icon(Icons.add_card, size: 36), - onTap: () { - Navigator.pop(context); - startActivation(); - }, - ), ListTile( title: Text(localization.verifyTitle), subtitle: Text(localization.verifyDescription), @@ -52,6 +48,17 @@ class MoreActionsDialog extends StatelessWidget { startVerification(); }, ), + if (!hasReachedCardLimit(userCodesModel.userCodes!)) + ListTile( + title: Text(localization.activateAnotherCardTitle), + subtitle: Text( + '${localization.activateAnotherCardDescription}\n(Noch $availableCards von $maxCardAmount frei)'), + leading: const Icon(Icons.add_card, size: 36), + onTap: () { + Navigator.pop(context); + startActivation(); + }, + ), ListTile( title: Text(localization.removeCardTitle), subtitle: Text(localization.removeCardDescription), diff --git a/frontend/lib/identification/identification_page.dart b/frontend/lib/identification/identification_page.dart index 8fc189460..5088d1e9c 100644 --- a/frontend/lib/identification/identification_page.dart +++ b/frontend/lib/identification/identification_page.dart @@ -74,6 +74,7 @@ class IdentificationPageState extends State { Future _startActivation(BuildContext context) async { if (await Permission.camera.request().isGranted) { + // TODO update card index to last when card was activated Navigator.push(context, AppRoute(builder: (context) => const ActivationCodeScannerPage())); return; } @@ -89,11 +90,10 @@ class IdentificationPageState extends State { Future _removeCard(BuildContext context) async { final userCodesModel = Provider.of(context, listen: false); - - await RemoveCardConfirmationDialog.show( - context: context, - userCode: userCodesModel.userCodes![cardIndex], - isLastItem: userCodesModel.userCodes!.length == 1); + await RemoveCardConfirmationDialog.show(context: context, userCode: userCodesModel.userCodes![cardIndex]); + if (cardIndex > 0) { + _updateCardIndex(cardIndex - 1); + } } Future _updateCardIndex(int index) async { diff --git a/frontend/lib/identification/user_code_model.dart b/frontend/lib/identification/user_code_model.dart index 0909fe52a..998a1cb1d 100644 --- a/frontend/lib/identification/user_code_model.dart +++ b/frontend/lib/identification/user_code_model.dart @@ -3,7 +3,8 @@ import 'dart:developer'; import 'package:ehrenamtskarte/identification/user_code_store.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/foundation.dart'; -// TODO remove + +// TODO remove this file @Deprecated('is needed to load old card data to new structure') class UserCodeModel extends ChangeNotifier { DynamicUserCode? _userCode; diff --git a/frontend/lib/identification/user_code_store.dart b/frontend/lib/identification/user_code_store.dart index e4eac1dd5..11eeb1fae 100644 --- a/frontend/lib/identification/user_code_store.dart +++ b/frontend/lib/identification/user_code_store.dart @@ -4,6 +4,7 @@ import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; const userCodeBase64Key = 'userCodeBase64'; + // TODO remove when import is finished @Deprecated('is needed to load old card data to new structure') class UserCodeStore { diff --git a/frontend/lib/identification/user_codes_model.dart b/frontend/lib/identification/user_codes_model.dart index 26f935d01..826899a79 100644 --- a/frontend/lib/identification/user_codes_model.dart +++ b/frontend/lib/identification/user_codes_model.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:ehrenamtskarte/build_config/build_config.dart'; import 'package:ehrenamtskarte/identification/user_codes_store.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/foundation.dart'; @@ -32,18 +33,13 @@ class UserCodesModel extends ChangeNotifier { void setCode(DynamicUserCode code) { List userCodes = _userCodes!; - if (UserCodesModel().isAlreadyInList(userCodes, code)) return; + if (isAlreadyInList(userCodes, code)) return; userCodes.add(code); const UserCodesStore().store(userCodes); _userCodes = userCodes; notifyListeners(); } - // - bool isAlreadyInList(List userCodes, DynamicUserCode code) { - return userCodes.map((userCode) => userCode.info).contains(code.info); - } - void removeCode(DynamicUserCode code) { List userCodes = _userCodes!; userCodes.remove(code); @@ -58,3 +54,11 @@ class UserCodesModel extends ChangeNotifier { notifyListeners(); } } + +bool isAlreadyInList(List userCodes, DynamicUserCode code) { + return userCodes.map((userCode) => userCode.info).contains(code.info); +} + +bool hasReachedCardLimit(List userCodes) { + return userCodes.length >= buildConfig.maxCardAmount; +} diff --git a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart index 47e7688c8..3cd3cc8e9 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart @@ -10,18 +10,16 @@ import 'package:provider/provider.dart'; class RemoveCardConfirmationDialog extends StatefulWidget { final DynamicUserCode userCode; - final bool isLastItem; - const RemoveCardConfirmationDialog({super.key, required this.userCode, required this.isLastItem}); + const RemoveCardConfirmationDialog({super.key, required this.userCode}); static Future show({ required BuildContext context, required DynamicUserCode userCode, - required bool isLastItem, }) => showDialog( context: context, - builder: (_) => RemoveCardConfirmationDialog(userCode: userCode, isLastItem: isLastItem), + builder: (_) => RemoveCardConfirmationDialog(userCode: userCode), ); @override @@ -78,12 +76,14 @@ class RemoveCardConfirmationDialogState extends State(context, listen: false); - if (widget.isLastItem) { + if (provider.userCodes!.length == 1) { + print("removeCodes"); provider.removeCodes(); } else { + print("removeCode"); provider.removeCode(widget.userCode); } - Navigator.of(context).pop(false); + Navigator.of(context).pop(true); }, ), ], From d5d2aee601c48429103132bf6836d6dca34c188d Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 6 Oct 2023 20:29:24 +0200 Subject: [PATCH 06/60] 1084: jump to added card, adjust RemoveDialog --- .../activation_code_scanner_page.dart | 4 ++- .../card_detail_view/card_carousel.dart | 16 ++++++---- .../identification/identification_page.dart | 29 ++++++++++++------- .../remove_card_confirmation_dialog.dart | 5 ++-- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 0004bcd46..67a3e1483 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -26,7 +26,8 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; class ActivationCodeScannerPage extends StatelessWidget { - const ActivationCodeScannerPage({super.key}); + final VoidCallback moveToLastCard; + const ActivationCodeScannerPage({super.key, required this.moveToLastCard}); @override Widget build(BuildContext context) { @@ -120,6 +121,7 @@ class ActivationCodeScannerPage extends StatelessWidget { await ActivationExistingCardDialog.showExistingCardDialog(context); } provider.setCode(userCode); + moveToLastCard(); debugPrint('Card Activation: Successfully activated.'); break; diff --git a/frontend/lib/identification/card_detail_view/card_carousel.dart b/frontend/lib/identification/card_detail_view/card_carousel.dart index 57e51e853..ec2bff735 100644 --- a/frontend/lib/identification/card_detail_view/card_carousel.dart +++ b/frontend/lib/identification/card_detail_view/card_carousel.dart @@ -5,16 +5,20 @@ class CardCarousel extends StatefulWidget { final List userCards; final int cardIndex; final Function(int index) updateIndex; + final CarouselController carouselController; - const CardCarousel({super.key, required this.userCards, required this.cardIndex, required this.updateIndex}); + const CardCarousel( + {super.key, + required this.userCards, + required this.cardIndex, + required this.updateIndex, + required this.carouselController}); @override CardCarouselState createState() => CardCarouselState(); } class CardCarouselState extends State { - final CarouselController _controller = CarouselController(); - @override Widget build(BuildContext context) { return Expanded( @@ -22,10 +26,10 @@ class CardCarouselState extends State { children: [ CarouselSlider( items: widget.userCards, - carouselController: _controller, + carouselController: widget.carouselController, options: CarouselOptions( enableInfiniteScroll: false, - viewportFraction: 0.98, + viewportFraction: 0.96, aspectRatio: 9 / 16, onPageChanged: (index, reason) { setState(() { @@ -39,7 +43,7 @@ class CardCarouselState extends State { mainAxisAlignment: MainAxisAlignment.center, children: widget.userCards.asMap().entries.map((entry) { return GestureDetector( - onTap: () => _controller.animateToPage(entry.key), + onTap: () => widget.carouselController.animateToPage(entry.key), child: Container( width: 12.0, height: 12.0, diff --git a/frontend/lib/identification/identification_page.dart b/frontend/lib/identification/identification_page.dart index 5088d1e9c..fd9f9397b 100644 --- a/frontend/lib/identification/identification_page.dart +++ b/frontend/lib/identification/identification_page.dart @@ -1,3 +1,4 @@ +import 'package:carousel_slider/carousel_controller.dart'; import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activation_code_scanner_page.dart'; @@ -22,6 +23,7 @@ class IdentificationPage extends StatefulWidget { } class IdentificationPageState extends State { + CarouselController carouselController = CarouselController(); int cardIndex = 0; @override @@ -47,7 +49,11 @@ class IdentificationPageState extends State { }); return Column(children: [ - CardCarousel(userCards: cards, cardIndex: cardIndex, updateIndex: _updateCardIndex), + CardCarousel( + userCards: cards, + cardIndex: cardIndex, + updateIndex: _updateCardIndex, + carouselController: carouselController), ]); } @@ -72,10 +78,16 @@ class IdentificationPageState extends State { handleDeniedCameraPermission(context); } + Future _updateCardIndex(int index) async { + setState(() { + cardIndex = index; + }); + } + Future _startActivation(BuildContext context) async { if (await Permission.camera.request().isGranted) { - // TODO update card index to last when card was activated - Navigator.push(context, AppRoute(builder: (context) => const ActivationCodeScannerPage())); + Navigator.push(context, + AppRoute(builder: (context) => ActivationCodeScannerPage(moveToLastCard: _moveCarouselToLastPosition))); return; } handleDeniedCameraPermission(context); @@ -91,14 +103,11 @@ class IdentificationPageState extends State { Future _removeCard(BuildContext context) async { final userCodesModel = Provider.of(context, listen: false); await RemoveCardConfirmationDialog.show(context: context, userCode: userCodesModel.userCodes![cardIndex]); - if (cardIndex > 0) { - _updateCardIndex(cardIndex - 1); - } + carouselController.previousPage(duration: Duration(milliseconds: 500), curve: Curves.linear); } - Future _updateCardIndex(int index) async { - setState(() { - cardIndex = index; - }); + void _moveCarouselToLastPosition() { + final userCodesModel = Provider.of(context, listen: false); + carouselController.jumpToPage(userCodesModel.userCodes!.length); } } diff --git a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart index 3cd3cc8e9..f56aa805f 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart @@ -45,6 +45,9 @@ class RemoveCardConfirmationDialogState extends State(context, listen: false); if (provider.userCodes!.length == 1) { - print("removeCodes"); provider.removeCodes(); } else { - print("removeCode"); provider.removeCode(widget.userCode); } Navigator.of(context).pop(true); From 451a5d5a36e1d03c9f516e1eec71e3a48481a0e8 Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 7 Oct 2023 15:54:45 +0200 Subject: [PATCH 07/60] 1084: import legacy cards --- frontend/lib/identification/user_codes_model.dart | 1 + frontend/lib/identification/user_codes_store.dart | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/frontend/lib/identification/user_codes_model.dart b/frontend/lib/identification/user_codes_model.dart index 826899a79..9c8d0ba12 100644 --- a/frontend/lib/identification/user_codes_model.dart +++ b/frontend/lib/identification/user_codes_model.dart @@ -22,6 +22,7 @@ class UserCodesModel extends ChangeNotifier { return; } try { + await UserCodesStore().importLegacyCard(); _userCodes = await const UserCodesStore().load(); } on Exception catch (e) { log('Failed to initialize activation code from secure storage of codes.', error: e); diff --git a/frontend/lib/identification/user_codes_store.dart b/frontend/lib/identification/user_codes_store.dart index 281226ec6..69b425b39 100644 --- a/frontend/lib/identification/user_codes_store.dart +++ b/frontend/lib/identification/user_codes_store.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:ehrenamtskarte/identification/user_codes_model.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -9,6 +10,8 @@ class UserCodesStore { const UserCodesStore(); static const _userCodesBase64Key = 'userCodesBase64'; + // legacy key for single card + static const _userCodeBase64Key = 'userCodeBase64'; Future store(List userCodes) async { const storage = FlutterSecureStorage(); @@ -31,4 +34,14 @@ class UserCodesStore { .map((code) => DynamicUserCode.fromBuffer(const Base64Decoder().convert(code))) .toList(); } + + // legacy import for single cards to keep compatible after update + Future importLegacyCard() async { + const storage = FlutterSecureStorage(); + final String? userCodeBase64 = await storage.read(key: _userCodeBase64Key); + if (userCodeBase64 == null) return; + DynamicUserCode importedLegacyCard = DynamicUserCode.fromBuffer(const Base64Decoder().convert(userCodeBase64)); + UserCodesModel().setCode(importedLegacyCard); + await storage.delete(key: _userCodesBase64Key); + } } From 7e973e85a2fb2eec1879932e708bbf50c4326931 Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 7 Oct 2023 17:38:51 +0200 Subject: [PATCH 08/60] 1084: fix store key, remove unneeded files, return empty list instead of null --- frontend/lib/about/backend_switch_dialog.dart | 4 +- frontend/lib/about/dev_settings_view.dart | 19 +++--- frontend/lib/app.dart | 4 +- .../activation_code_scanner_page.dart | 6 +- .../card_detail_view/card_detail_view.dart | 6 +- .../card_detail_view/more_actions_dialog.dart | 8 +-- .../card_detail_view/self_verify_card.dart | 8 +-- .../verification_code_view.dart | 4 +- .../identification/identification_page.dart | 24 +++---- .../lib/identification/user_code_model.dart | 41 ++++++++---- .../lib/identification/user_code_store.dart | 37 +++++++---- .../lib/identification/user_codes_model.dart | 65 ------------------- .../lib/identification/user_codes_store.dart | 47 -------------- .../remove_card_confirmation_dialog.dart | 6 +- .../verification_qr_scanner_page.dart | 6 +- 15 files changed, 102 insertions(+), 183 deletions(-) delete mode 100644 frontend/lib/identification/user_codes_model.dart delete mode 100644 frontend/lib/identification/user_codes_store.dart diff --git a/frontend/lib/about/backend_switch_dialog.dart b/frontend/lib/about/backend_switch_dialog.dart index 9ad0da626..212f53544 100644 --- a/frontend/lib/about/backend_switch_dialog.dart +++ b/frontend/lib/about/backend_switch_dialog.dart @@ -1,6 +1,6 @@ import 'package:ehrenamtskarte/configuration/configuration.dart'; import 'package:ehrenamtskarte/configuration/settings_model.dart'; -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -84,7 +84,7 @@ class BackendSwitchDialogState extends State { void clearData() { final settings = Provider.of(context, listen: false); - final userCodes = Provider.of(context, listen: false); + final userCodes = Provider.of(context, listen: false); settings.clearSettings(); userCodes.removeCodes(); } diff --git a/frontend/lib/about/dev_settings_view.dart b/frontend/lib/about/dev_settings_view.dart index e60d7a104..a12d29a54 100644 --- a/frontend/lib/about/dev_settings_view.dart +++ b/frontend/lib/about/dev_settings_view.dart @@ -11,7 +11,7 @@ import 'package:ehrenamtskarte/identification/activation_workflow/activation_cod import 'package:ehrenamtskarte/identification/activation_workflow/activation_exception.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_processor.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_parsing_error_dialog.dart'; -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/identification/util/card_info_utils.dart'; import 'package:ehrenamtskarte/intro_slides/intro_screen.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; @@ -54,14 +54,14 @@ class DevSettingsView extends StatelessWidget { Widget build(BuildContext context) { final settings = Provider.of(context); final client = GraphQLProvider.of(context).value; - final userCodesModel = Provider.of(context); + final userCodeModel = Provider.of(context); return Padding( padding: const EdgeInsets.all(15.0), child: Column( children: [ ListTile( title: const Text('Reset cards'), - onTap: () => _resetEakData(context, userCodesModel), + onTap: () => _resetEakData(context, userCodeModel), ), ListTile( title: const Text('Set (invalid) sample card'), @@ -82,7 +82,7 @@ class DevSettingsView extends StatelessWidget { ListTile( title: const Text('Trigger self-verification'), onTap: () => { - userCodesModel.userCodes! + userCodeModel.userCodes .map((code) => selfVerifyCard(context, code, Configuration.of(context).projectId, client)) }), ListTile( @@ -104,7 +104,7 @@ class DevSettingsView extends StatelessWidget { ); } - Future _resetEakData(BuildContext context, UserCodesModel userCodes) async { + Future _resetEakData(BuildContext context, UserCodeModel userCodes) async { userCodes.removeCodes(); } @@ -126,7 +126,7 @@ class DevSettingsView extends StatelessWidget { } Future _setSampleCard(BuildContext context) async { - Provider.of(context, listen: false).setCode(_determineUserCode(buildConfig.projectId.local)); + Provider.of(context, listen: false).setCode(_determineUserCode(buildConfig.projectId.local)); } Future _showRawCardInput(BuildContext context) async { @@ -171,7 +171,7 @@ class DevSettingsView extends StatelessWidget { Future _activateCard(BuildContext context, String base64qrcode) async { final messengerState = ScaffoldMessenger.of(context); - final provider = Provider.of(context, listen: false); + final provider = Provider.of(context, listen: false); final client = GraphQLProvider.of(context).value; final projectId = Configuration.of(context).projectId; try { @@ -238,8 +238,9 @@ class DevSettingsView extends StatelessWidget { // This is used to check the invalidation of a card because the verification with the backend couldn't be done lately (1 week plus UTC tolerance) void _setExpiredLastVerification(BuildContext context) { - final provider = Provider.of(context, listen: false); - final DynamicUserCode userCode = provider.userCodes!.first; + final provider = Provider.of(context, listen: false); +// TODO remove the first and add proper implementation + final DynamicUserCode userCode = provider.userCodes.first; final CardVerification cardVerification = CardVerification() ..verificationTimeStamp = secondsSinceEpoch(DateTime.now().toUtc().subtract(Duration(seconds: cardValidationExpireSeconds + 3600))) diff --git a/frontend/lib/app.dart b/frontend/lib/app.dart index 2d3968c15..fb60773e3 100644 --- a/frontend/lib/app.dart +++ b/frontend/lib/app.dart @@ -3,7 +3,7 @@ import 'package:ehrenamtskarte/configuration/configuration.dart'; import 'package:ehrenamtskarte/configuration/definitions.dart'; import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/graphql/configured_graphql_provider.dart'; -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/intro_slides/intro_screen.dart'; import 'package:ehrenamtskarte/themes.dart'; import 'package:flutter/foundation.dart'; @@ -60,7 +60,7 @@ class App extends StatelessWidget { showDevSettings: kDebugMode, child: ConfiguredGraphQlProvider( child: ChangeNotifierProvider( - create: (context) => UserCodesModel()..initialize(), + create: (context) => UserCodeModel()..initialize(), child: MaterialApp( theme: lightTheme, darkTheme: darkTheme, diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 67a3e1483..4576bb9f9 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -13,7 +13,7 @@ import 'package:ehrenamtskarte/identification/connection_failed_dialog.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_processor.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_scanner_page.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_parsing_error_dialog.dart'; -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/identification/util/card_info_utils.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/verification_qr_code_processor.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; @@ -88,7 +88,7 @@ class ActivationCodeScannerPage extends StatelessWidget { ]) async { final client = GraphQLProvider.of(context).value; final projectId = Configuration.of(context).projectId; - final provider = Provider.of(context, listen: false); + final provider = Provider.of(context, listen: false); final activationSecretBase64 = const Base64Encoder().convert(activationCode.activationSecret); final cardInfoBase64 = activationCode.info.hash(activationCode.pepper); @@ -117,7 +117,7 @@ class ActivationCodeScannerPage extends StatelessWidget { ..cardVerification = (CardVerification() ..cardValid = true ..verificationTimeStamp = secondsSinceEpoch(DateTime.parse(activationResult.activationTimeStamp))); - if (provider.userCodes != null && isAlreadyInList(provider.userCodes!, userCode)) { + if (isAlreadyInList(provider.userCodes, userCode)) { await ActivationExistingCardDialog.showExistingCardDialog(context); } provider.setCode(userCode); diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index ecaccee18..0d1d87513 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -3,7 +3,7 @@ import 'package:ehrenamtskarte/graphql/graphql_api.dart'; import 'package:ehrenamtskarte/identification/card_detail_view/more_actions_dialog.dart'; import 'package:ehrenamtskarte/identification/card_detail_view/self_verify_card.dart'; import 'package:ehrenamtskarte/identification/id_card/id_card.dart'; -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/identification/util/card_info_utils.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; @@ -50,10 +50,10 @@ class _CardDetailViewState extends State { } Future _selfVerifyCard() async { - final userCodesModel = Provider.of(context, listen: false); + final userCodeModel = Provider.of(context, listen: false); final projectId = Configuration.of(context).projectId; final client = GraphQLProvider.of(context).value; - userCodesModel.userCodes!.map((code) => selfVerifyCard(context, code, projectId, client)); + userCodeModel.userCodes.map((code) => selfVerifyCard(context, code, projectId, client)); } @override diff --git a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart index ae5caff7a..1d843283f 100644 --- a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart +++ b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart @@ -1,5 +1,5 @@ import 'package:ehrenamtskarte/build_config/build_config.dart'; -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -20,8 +20,8 @@ class MoreActionsDialog extends StatelessWidget { @override Widget build(BuildContext context) { final localization = buildConfig.localization.identification.moreActions; - final userCodesModel = Provider.of(context, listen: false); - final String availableCards = (buildConfig.maxCardAmount - userCodesModel.userCodes!.length).toString(); + final userCodeModel = Provider.of(context, listen: false); + final String availableCards = (buildConfig.maxCardAmount - userCodeModel.userCodes.length).toString(); final String maxCardAmount = buildConfig.maxCardAmount.toString(); return AlertDialog( contentPadding: const EdgeInsets.only(top: 12), @@ -48,7 +48,7 @@ class MoreActionsDialog extends StatelessWidget { startVerification(); }, ), - if (!hasReachedCardLimit(userCodesModel.userCodes!)) + if (!hasReachedCardLimit(userCodeModel.userCodes)) ListTile( title: Text(localization.activateAnotherCardTitle), subtitle: Text( diff --git a/frontend/lib/identification/card_detail_view/self_verify_card.dart b/frontend/lib/identification/card_detail_view/self_verify_card.dart index 0efbfd498..ad75fcd2a 100644 --- a/frontend/lib/identification/card_detail_view/self_verify_card.dart +++ b/frontend/lib/identification/card_detail_view/self_verify_card.dart @@ -1,4 +1,4 @@ -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:flutter/widgets.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; @@ -10,7 +10,7 @@ import '../verification_workflow/query_server_verification.dart'; Future selfVerifyCard( BuildContext context, DynamicUserCode? userCode, String projectId, GraphQLClient client) async { - final userCodesModel = Provider.of(context, listen: false); + final userCodeModel = Provider.of(context, listen: false); if (userCode == null) { return; } @@ -25,7 +25,7 @@ Future selfVerifyCard( final (outOfSync: outOfSync, result: cardVerification) = await queryDynamicServerVerification(client, projectId, qrCode); - +// TODO uncomment // If the user code has changed during the server request, we abort. // if (userCodeModel.userCode != userCode) { // debugPrint('Card Self-Verification: The user code has been changed during server request for the old user code.'); @@ -34,7 +34,7 @@ Future selfVerifyCard( debugPrint("Card Self-Verification: Persisting response. Card is ${cardVerification.valid ? "valid." : "INVALID."}"); - userCodesModel.setCode(DynamicUserCode() + userCodeModel.setCode(DynamicUserCode() ..info = userCode.info ..ecSignature = userCode.ecSignature ..pepper = userCode.pepper diff --git a/frontend/lib/identification/card_detail_view/verification_code_view.dart b/frontend/lib/identification/card_detail_view/verification_code_view.dart index 3372cd225..91ca85761 100644 --- a/frontend/lib/identification/card_detail_view/verification_code_view.dart +++ b/frontend/lib/identification/card_detail_view/verification_code_view.dart @@ -4,7 +4,7 @@ import 'dart:math'; import 'package:ehrenamtskarte/identification/card_detail_view/animated_progressbar.dart'; import 'package:ehrenamtskarte/identification/otp_generator.dart'; import 'package:ehrenamtskarte/identification/qr_content_parser.dart'; -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -56,7 +56,7 @@ class VerificationCodeViewState extends State { return LayoutBuilder( builder: (context, constraints) { final padding = min(constraints.maxWidth, constraints.maxHeight) < 400 ? 12.0 : 24.0; - return Consumer( + return Consumer( builder: (context, cardDetailsModel, child) { final qrCode = qr.QrCode.fromUint8List( data: createDynamicVerificationQrCodeData(userCode, otpCode.code), diff --git a/frontend/lib/identification/identification_page.dart b/frontend/lib/identification/identification_page.dart index fd9f9397b..4a249ecd3 100644 --- a/frontend/lib/identification/identification_page.dart +++ b/frontend/lib/identification/identification_page.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/identification/card_detail_view/card_carousel.dar import 'package:ehrenamtskarte/identification/card_detail_view/card_detail_view.dart'; import 'package:ehrenamtskarte/identification/no_card_view.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart'; -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/verification_workflow.dart'; import 'package:ehrenamtskarte/routing.dart'; @@ -30,23 +30,23 @@ class IdentificationPageState extends State { Widget build(BuildContext context) { final settings = Provider.of(context); - return Consumer( - builder: (context, userCodesModel, child) { - if (!userCodesModel.isInitialized) { + return Consumer( + builder: (context, userCodeModel, child) { + if (!userCodeModel.isInitialized) { return Container(); } - if (userCodesModel.userCodes != null && userCodesModel.userCodes!.isNotEmpty) { + if (userCodeModel.userCodes.isNotEmpty) { final List cards = []; - userCodesModel.userCodes?.forEach((element) { + for (var code in userCodeModel.userCodes) { cards.add(CardDetailView( - userCode: element, + userCode: code, startVerification: () => _showVerificationDialog(context, settings), startActivation: () => _startActivation(context), startApplication: _startApplication, removeCard: () => _removeCard(context), )); - }); + } return Column(children: [ CardCarousel( @@ -101,13 +101,13 @@ class IdentificationPageState extends State { } Future _removeCard(BuildContext context) async { - final userCodesModel = Provider.of(context, listen: false); - await RemoveCardConfirmationDialog.show(context: context, userCode: userCodesModel.userCodes![cardIndex]); + final userCodeModel = Provider.of(context, listen: false); + await RemoveCardConfirmationDialog.show(context: context, userCode: userCodeModel.userCodes[cardIndex]); carouselController.previousPage(duration: Duration(milliseconds: 500), curve: Curves.linear); } void _moveCarouselToLastPosition() { - final userCodesModel = Provider.of(context, listen: false); - carouselController.jumpToPage(userCodesModel.userCodes!.length); + final userCodeModel = Provider.of(context, listen: false); + carouselController.jumpToPage(userCodeModel.userCodes.length); } } diff --git a/frontend/lib/identification/user_code_model.dart b/frontend/lib/identification/user_code_model.dart index 998a1cb1d..da30de712 100644 --- a/frontend/lib/identification/user_code_model.dart +++ b/frontend/lib/identification/user_code_model.dart @@ -1,17 +1,16 @@ import 'dart:developer'; +import 'package:ehrenamtskarte/build_config/build_config.dart'; import 'package:ehrenamtskarte/identification/user_code_store.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/foundation.dart'; -// TODO remove this file -@Deprecated('is needed to load old card data to new structure') class UserCodeModel extends ChangeNotifier { - DynamicUserCode? _userCode; + List _userCodes = []; bool _isInitialized = false; - DynamicUserCode? get userCode { - return _userCode; + List get userCodes { + return _userCodes; } bool get isInitialized { @@ -23,9 +22,10 @@ class UserCodeModel extends ChangeNotifier { return; } try { - _userCode = await const UserCodeStore().load(); + await UserCodeStore().importLegacyCard(); + _userCodes = await const UserCodeStore().load(); } on Exception catch (e) { - log('Failed to initialize activation code from secure storage.', error: e); + log('Failed to initialize activation code from secure storage of codes.', error: e); } finally { _isInitialized = true; notifyListeners(); @@ -33,14 +33,33 @@ class UserCodeModel extends ChangeNotifier { } void setCode(DynamicUserCode code) { - const UserCodeStore().store(code); - _userCode = code; + List userCodes = _userCodes; + if (isAlreadyInList(userCodes, code)) return; + userCodes.add(code); + const UserCodeStore().store(userCodes); + _userCodes = userCodes; notifyListeners(); } - void removeCode() { + void removeCode(DynamicUserCode code) { + List userCodes = _userCodes; + userCodes.remove(code); + const UserCodeStore().store(userCodes); + _userCodes = userCodes; + notifyListeners(); + } + + void removeCodes() { const UserCodeStore().remove(); - _userCode = null; + _userCodes = []; notifyListeners(); } } + +bool isAlreadyInList(List userCodes, DynamicUserCode code) { + return userCodes.map((userCode) => userCode.info).contains(code.info); +} + +bool hasReachedCardLimit(List userCodes) { + return userCodes.length >= buildConfig.maxCardAmount; +} diff --git a/frontend/lib/identification/user_code_store.dart b/frontend/lib/identification/user_code_store.dart index 11eeb1fae..6254740ab 100644 --- a/frontend/lib/identification/user_code_store.dart +++ b/frontend/lib/identification/user_code_store.dart @@ -1,34 +1,45 @@ import 'dart:convert'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -const userCodeBase64Key = 'userCodeBase64'; - -// TODO remove when import is finished -@Deprecated('is needed to load old card data to new structure') class UserCodeStore { const UserCodeStore(); + static const _userCodesBase64Key = 'userCodesBase64'; + // legacy key for single card static const _userCodeBase64Key = 'userCodeBase64'; - Future store(DynamicUserCode userCode) async { + Future store(List userCodes) async { const storage = FlutterSecureStorage(); - await storage.write( - key: _userCodeBase64Key, - value: const Base64Encoder().convert(userCode.writeToBuffer()), - ); + Iterable userCodeString = userCodes.map((code) => const Base64Encoder().convert(code.writeToBuffer())); + + await storage.write(key: _userCodesBase64Key, value: userCodeString.join(',')); } Future remove() async { const storage = FlutterSecureStorage(); - await storage.delete(key: _userCodeBase64Key); + await storage.delete(key: _userCodesBase64Key); } - Future load() async { + Future> load() async { + const storage = FlutterSecureStorage(); + final String? userCodesBase64 = await storage.read(key: _userCodesBase64Key); + if (userCodesBase64 == null) return []; + return userCodesBase64 + .split(',') + .map((code) => DynamicUserCode.fromBuffer(const Base64Decoder().convert(code))) + .toList(); + } + + // legacy import for single cards to keep compatible after update + Future importLegacyCard() async { const storage = FlutterSecureStorage(); final String? userCodeBase64 = await storage.read(key: _userCodeBase64Key); - if (userCodeBase64 == null) return null; - return DynamicUserCode.fromBuffer(const Base64Decoder().convert(userCodeBase64)); + if (userCodeBase64 == null) return; + DynamicUserCode importedLegacyCard = DynamicUserCode.fromBuffer(const Base64Decoder().convert(userCodeBase64)); + UserCodeModel().setCode(importedLegacyCard); + await storage.delete(key: _userCodeBase64Key); } } diff --git a/frontend/lib/identification/user_codes_model.dart b/frontend/lib/identification/user_codes_model.dart deleted file mode 100644 index 9c8d0ba12..000000000 --- a/frontend/lib/identification/user_codes_model.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'dart:developer'; - -import 'package:ehrenamtskarte/build_config/build_config.dart'; -import 'package:ehrenamtskarte/identification/user_codes_store.dart'; -import 'package:ehrenamtskarte/proto/card.pb.dart'; -import 'package:flutter/foundation.dart'; - -class UserCodesModel extends ChangeNotifier { - List? _userCodes = []; - bool _isInitialized = false; - - List? get userCodes { - return _userCodes; - } - - bool get isInitialized { - return _isInitialized; - } - - Future initialize() async { - if (_isInitialized) { - return; - } - try { - await UserCodesStore().importLegacyCard(); - _userCodes = await const UserCodesStore().load(); - } on Exception catch (e) { - log('Failed to initialize activation code from secure storage of codes.', error: e); - } finally { - _isInitialized = true; - notifyListeners(); - } - } - - void setCode(DynamicUserCode code) { - List userCodes = _userCodes!; - if (isAlreadyInList(userCodes, code)) return; - userCodes.add(code); - const UserCodesStore().store(userCodes); - _userCodes = userCodes; - notifyListeners(); - } - - void removeCode(DynamicUserCode code) { - List userCodes = _userCodes!; - userCodes.remove(code); - const UserCodesStore().store(userCodes); - _userCodes = userCodes; - notifyListeners(); - } - - void removeCodes() { - const UserCodesStore().remove(); - _userCodes = []; - notifyListeners(); - } -} - -bool isAlreadyInList(List userCodes, DynamicUserCode code) { - return userCodes.map((userCode) => userCode.info).contains(code.info); -} - -bool hasReachedCardLimit(List userCodes) { - return userCodes.length >= buildConfig.maxCardAmount; -} diff --git a/frontend/lib/identification/user_codes_store.dart b/frontend/lib/identification/user_codes_store.dart deleted file mode 100644 index 69b425b39..000000000 --- a/frontend/lib/identification/user_codes_store.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'dart:convert'; - -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; -import 'package:ehrenamtskarte/proto/card.pb.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; - -const userCodeBase64Key = 'userCodeBase64'; - -class UserCodesStore { - const UserCodesStore(); - - static const _userCodesBase64Key = 'userCodesBase64'; - // legacy key for single card - static const _userCodeBase64Key = 'userCodeBase64'; - - Future store(List userCodes) async { - const storage = FlutterSecureStorage(); - Iterable userCodeString = userCodes.map((code) => const Base64Encoder().convert(code.writeToBuffer())); - - await storage.write(key: _userCodesBase64Key, value: userCodeString.join(',')); - } - - Future remove() async { - const storage = FlutterSecureStorage(); - await storage.delete(key: _userCodesBase64Key); - } - - Future?> load() async { - const storage = FlutterSecureStorage(); - final String? userCodesBase64 = await storage.read(key: _userCodesBase64Key); - if (userCodesBase64 == null) return null; - return userCodesBase64 - .split(',') - .map((code) => DynamicUserCode.fromBuffer(const Base64Decoder().convert(code))) - .toList(); - } - - // legacy import for single cards to keep compatible after update - Future importLegacyCard() async { - const storage = FlutterSecureStorage(); - final String? userCodeBase64 = await storage.read(key: _userCodeBase64Key); - if (userCodeBase64 == null) return; - DynamicUserCode importedLegacyCard = DynamicUserCode.fromBuffer(const Base64Decoder().convert(userCodeBase64)); - UserCodesModel().setCode(importedLegacyCard); - await storage.delete(key: _userCodesBase64Key); - } -} diff --git a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart index f56aa805f..f600d53bf 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart @@ -2,7 +2,7 @@ import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/configuration/configuration.dart'; import 'package:ehrenamtskarte/graphql/graphql_api.dart'; import 'package:ehrenamtskarte/identification/id_card/id_card.dart'; -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; @@ -78,8 +78,8 @@ class RemoveCardConfirmationDialogState extends State(context, listen: false); - if (provider.userCodes!.length == 1) { + final provider = Provider.of(context, listen: false); + if (provider.userCodes.length == 1) { provider.removeCodes(); } else { provider.removeCode(widget.userCode); diff --git a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart index d7b80ffce..2bdd76101 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart @@ -8,7 +8,7 @@ import 'package:ehrenamtskarte/identification/otp_generator.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_processor.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_scanner_page.dart'; import 'package:ehrenamtskarte/identification/qr_content_parser.dart'; -import 'package:ehrenamtskarte/identification/user_codes_model.dart'; +import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/verification_info_dialog.dart'; @@ -50,9 +50,9 @@ class VerificationQrScannerPage extends StatelessWidget { if (config.showDevSettings) TextButton( onPressed: () async { - final provider = Provider.of(context, listen: false); + final provider = Provider.of(context, listen: false); // TODO use current card instead of first - final userCode = provider.userCodes!.first; + final userCode = provider.userCodes.first; final otp = OTPGenerator(userCode.totpSecret).generateOTP().code; final verificationQrCode = QrCode() ..dynamicVerificationCode = (DynamicVerificationCode() From cc7b456f433fdcb00501e9fd43dee669837141e4 Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 7 Oct 2023 17:52:36 +0200 Subject: [PATCH 09/60] 1084: fix bug adding first card --- .../activation_workflow/activation_code_scanner_page.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 4576bb9f9..991cfd6bc 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -121,7 +121,9 @@ class ActivationCodeScannerPage extends StatelessWidget { await ActivationExistingCardDialog.showExistingCardDialog(context); } provider.setCode(userCode); - moveToLastCard(); + if (provider.userCodes.length > 1) { + moveToLastCard(); + } debugPrint('Card Activation: Successfully activated.'); break; From 2bfb76395fa9722f11264e8bc424632a7958381b Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 9 Oct 2023 12:05:56 +0200 Subject: [PATCH 10/60] 1084: hide slider indicator for one card, fix self verification --- frontend/lib/about/dev_settings_view.dart | 14 +++++-- .../card_detail_view/card_carousel.dart | 39 ++++++++++--------- .../card_detail_view/self_verify_card.dart | 29 +++++++------- .../identification/identification_page.dart | 5 ++- .../verification_qr_scanner_page.dart | 16 ++++---- .../verification_workflow.dart | 9 +++-- 6 files changed, 61 insertions(+), 51 deletions(-) diff --git a/frontend/lib/about/dev_settings_view.dart b/frontend/lib/about/dev_settings_view.dart index a12d29a54..7991863c1 100644 --- a/frontend/lib/about/dev_settings_view.dart +++ b/frontend/lib/about/dev_settings_view.dart @@ -77,7 +77,7 @@ class DevSettingsView extends StatelessWidget { ), ListTile( title: const Text('Set expired last card verification'), - onTap: () => _setExpiredLastVerification(context), + onTap: () => _setExpiredLastVerifications(context), ), ListTile( title: const Text('Trigger self-verification'), @@ -236,11 +236,17 @@ class DevSettingsView extends StatelessWidget { ); } + void _setExpiredLastVerifications(BuildContext context) { + final provider = Provider.of(context, listen: false); + if (provider.userCodes.isNotEmpty) { + List userCodes = provider.userCodes; + userCodes.map((code) => _setExpiredLastVerification(context, code)); + } + } + // This is used to check the invalidation of a card because the verification with the backend couldn't be done lately (1 week plus UTC tolerance) - void _setExpiredLastVerification(BuildContext context) { + void _setExpiredLastVerification(BuildContext context, DynamicUserCode userCode) { final provider = Provider.of(context, listen: false); -// TODO remove the first and add proper implementation - final DynamicUserCode userCode = provider.userCodes.first; final CardVerification cardVerification = CardVerification() ..verificationTimeStamp = secondsSinceEpoch(DateTime.now().toUtc().subtract(Duration(seconds: cardValidationExpireSeconds + 3600))) diff --git a/frontend/lib/identification/card_detail_view/card_carousel.dart b/frontend/lib/identification/card_detail_view/card_carousel.dart index ec2bff735..06d215438 100644 --- a/frontend/lib/identification/card_detail_view/card_carousel.dart +++ b/frontend/lib/identification/card_detail_view/card_carousel.dart @@ -37,26 +37,27 @@ class CardCarouselState extends State { }); }), ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: widget.userCards.asMap().entries.map((entry) { - return GestureDetector( - onTap: () => widget.carouselController.animateToPage(entry.key), - child: Container( - width: 12.0, - height: 12.0, - margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black) - .withOpacity(widget.cardIndex == entry.key ? 0.9 : 0.4)), - ), - ); - }).toList(), + if (widget.userCards.length > 1) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: widget.userCards.asMap().entries.map((entry) { + return GestureDetector( + onTap: () => widget.carouselController.animateToPage(entry.key), + child: Container( + width: 12.0, + height: 12.0, + margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black) + .withOpacity(widget.cardIndex == entry.key ? 0.9 : 0.4)), + ), + ); + }).toList(), + ), ), - ), ], )); } diff --git a/frontend/lib/identification/card_detail_view/self_verify_card.dart b/frontend/lib/identification/card_detail_view/self_verify_card.dart index ad75fcd2a..67f77af62 100644 --- a/frontend/lib/identification/card_detail_view/self_verify_card.dart +++ b/frontend/lib/identification/card_detail_view/self_verify_card.dart @@ -10,35 +10,36 @@ import '../verification_workflow/query_server_verification.dart'; Future selfVerifyCard( BuildContext context, DynamicUserCode? userCode, String projectId, GraphQLClient client) async { - final userCodeModel = Provider.of(context, listen: false); - if (userCode == null) { + final initialUserCode = userCode; + if (initialUserCode == null) { return; } - final otpCode = OTPGenerator(userCode.totpSecret).generateOTP(); + final otpCode = OTPGenerator(initialUserCode.totpSecret).generateOTP(); final DynamicVerificationCode qrCode = DynamicVerificationCode() - ..info = userCode.info - ..pepper = userCode.pepper + ..info = initialUserCode.info + ..pepper = initialUserCode.pepper ..otp = otpCode.code; debugPrint('Card Self-Verification: Requesting server'); final (outOfSync: outOfSync, result: cardVerification) = await queryDynamicServerVerification(client, projectId, qrCode); -// TODO uncomment + // If the user code has changed during the server request, we abort. - // if (userCodeModel.userCode != userCode) { - // debugPrint('Card Self-Verification: The user code has been changed during server request for the old user code.'); - // return; - // } + if (userCode != initialUserCode) { + debugPrint('Card Self-Verification: The user code has been changed during server request for the old user code.'); + return; + } debugPrint("Card Self-Verification: Persisting response. Card is ${cardVerification.valid ? "valid." : "INVALID."}"); + final userCodeModel = Provider.of(context, listen: false); userCodeModel.setCode(DynamicUserCode() - ..info = userCode.info - ..ecSignature = userCode.ecSignature - ..pepper = userCode.pepper - ..totpSecret = userCode.totpSecret + ..info = initialUserCode.info + ..ecSignature = initialUserCode.ecSignature + ..pepper = initialUserCode.pepper + ..totpSecret = initialUserCode.totpSecret ..cardVerification = (CardVerification() ..cardValid = cardVerification.valid ..verificationTimeStamp = secondsSinceEpoch(DateTime.parse(cardVerification.verificationTimeStamp)) diff --git a/frontend/lib/identification/identification_page.dart b/frontend/lib/identification/identification_page.dart index 4a249ecd3..c674cb5fe 100644 --- a/frontend/lib/identification/identification_page.dart +++ b/frontend/lib/identification/identification_page.dart @@ -9,6 +9,7 @@ import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_camera_per import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/verification_workflow.dart'; +import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:ehrenamtskarte/routing.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -72,7 +73,9 @@ class IdentificationPageState extends State { Future _showVerificationDialog(BuildContext context, SettingsModel settings) async { if (await Permission.camera.request().isGranted) { - await VerificationWorkflow.startWorkflow(context, settings); + final userCodeModel = Provider.of(context, listen: false); + DynamicUserCode? userCode = userCodeModel.userCodes.isNotEmpty ? userCodeModel.userCodes[cardIndex] : null; + await VerificationWorkflow.startWorkflow(context, settings, userCode); return; } handleDeniedCameraPermission(context); diff --git a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart index 2bdd76101..201dee420 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart @@ -8,7 +8,6 @@ import 'package:ehrenamtskarte/identification/otp_generator.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_processor.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_scanner_page.dart'; import 'package:ehrenamtskarte/identification/qr_content_parser.dart'; -import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/verification_info_dialog.dart'; @@ -21,12 +20,14 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; class VerificationQrScannerPage extends StatelessWidget { - const VerificationQrScannerPage({super.key}); + final DynamicUserCode? userCode; + const VerificationQrScannerPage({super.key, this.userCode}); @override Widget build(BuildContext context) { final config = Configuration.of(context); final settings = Provider.of(context); + final currentUserCode = userCode; return Column( children: [ CustomAppBar( @@ -47,17 +48,14 @@ class VerificationQrScannerPage extends StatelessWidget { onCodeScanned: (code) => _handleQrCode(context, code), ), ), - if (config.showDevSettings) + if (config.showDevSettings && currentUserCode != null) TextButton( onPressed: () async { - final provider = Provider.of(context, listen: false); - // TODO use current card instead of first - final userCode = provider.userCodes.first; - final otp = OTPGenerator(userCode.totpSecret).generateOTP().code; + final otp = OTPGenerator(currentUserCode.totpSecret).generateOTP().code; final verificationQrCode = QrCode() ..dynamicVerificationCode = (DynamicVerificationCode() - ..info = userCode.info - ..pepper = userCode.pepper + ..info = currentUserCode.info + ..pepper = currentUserCode.pepper ..otp = otp); final verificationCode = verificationQrCode.writeToBuffer(); _handleQrCode(context, verificationCode); diff --git a/frontend/lib/identification/verification_workflow/verification_workflow.dart b/frontend/lib/identification/verification_workflow/verification_workflow.dart index cf60b136d..2889ca37b 100644 --- a/frontend/lib/identification/verification_workflow/verification_workflow.dart +++ b/frontend/lib/identification/verification_workflow/verification_workflow.dart @@ -1,16 +1,17 @@ import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/verification_info_dialog.dart'; import 'package:ehrenamtskarte/identification/verification_workflow/verification_qr_scanner_page.dart'; +import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:ehrenamtskarte/routing.dart'; import 'package:flutter/material.dart'; class VerificationWorkflow { VerificationWorkflow._(); // hide the constructor - static Future startWorkflow(BuildContext context, SettingsModel settings) => - VerificationWorkflow._().showInfoAndQrScanner(context, settings); + static Future startWorkflow(BuildContext context, SettingsModel settings, DynamicUserCode? userCode) => + VerificationWorkflow._().showInfoAndQrScanner(context, settings, userCode); - Future showInfoAndQrScanner(BuildContext rootContext, SettingsModel settings) async { + Future showInfoAndQrScanner(BuildContext rootContext, SettingsModel settings, DynamicUserCode? userCode) async { if (settings.hideVerificationInfo != true) { // show info dialog and cancel if it is not accepted if (await VerificationInfoDialog.show(rootContext) != true) return; @@ -21,7 +22,7 @@ class VerificationWorkflow { rootContext, AppRoute( builder: (context) { - return const VerificationQrScannerPage(); + return VerificationQrScannerPage(userCode: userCode); }, ), ); From 8f95ac5cd894602f8c210fbe8051b12e0fad1394 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 9 Oct 2023 13:39:58 +0200 Subject: [PATCH 11/60] 1084: fix minor issues --- frontend/build-configs/bayern/localization.ts | 2 +- frontend/lib/about/dev_settings_view.dart | 11 +++++----- .../card_detail_view/card_carousel.dart | 10 ++++----- .../card_detail_view/card_detail_view.dart | 18 ++++++++------- .../card_detail_view/more_actions_dialog.dart | 6 ++--- .../identification/identification_page.dart | 22 +++++++++---------- .../lib/identification/user_code_model.dart | 2 +- .../lib/identification/user_code_store.dart | 9 ++++---- .../remove_card_confirmation_dialog.dart | 17 ++++++++------ 9 files changed, 51 insertions(+), 46 deletions(-) diff --git a/frontend/build-configs/bayern/localization.ts b/frontend/build-configs/bayern/localization.ts index 6c4ecdfc8..4e218b184 100644 --- a/frontend/build-configs/bayern/localization.ts +++ b/frontend/build-configs/bayern/localization.ts @@ -25,7 +25,7 @@ const localization: LocalizationType = { applyForAnotherCardTitle: "Ehrenamtskarte beantragen oder verlängern", applyForAnotherCardDescription: "Ihre hinterlegte Karte bleibt erhalten.", activateAnotherCardTitle: "Weitere Ehrenamtskarte hinzufügen", - activateAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten. Sie können diesen manuell entfernen.", + activateAnotherCardDescription: "Ihr hinterlegte Ehrenamtskarte bleibt erhalten. Sie können diesen manuell entfernen.", verifyTitle: "Eine digitale Ehrenamtskarte prüfen", verifyDescription: "Prüfen Sie die Gültigkeit einer digitalen Ehrenamtskarte.", removeCardTitle: "Diese Ehrenamtskarte löschen", diff --git a/frontend/lib/about/dev_settings_view.dart b/frontend/lib/about/dev_settings_view.dart index 7991863c1..ea01db470 100644 --- a/frontend/lib/about/dev_settings_view.dart +++ b/frontend/lib/about/dev_settings_view.dart @@ -9,6 +9,7 @@ import 'package:ehrenamtskarte/graphql/graphql_api.graphql.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activate_code.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activation_code_parser.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activation_exception.dart'; +import 'package:ehrenamtskarte/identification/card_detail_view/self_verify_card.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_processor.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_parsing_error_dialog.dart'; import 'package:ehrenamtskarte/identification/user_code_model.dart'; @@ -21,8 +22,6 @@ import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; -import '../identification/card_detail_view/self_verify_card.dart'; - // this data includes a Base32 encoded random key created with openssl // for testing, so this is intended final sampleActivationCodeBavaria = DynamicUserCode() @@ -82,8 +81,8 @@ class DevSettingsView extends StatelessWidget { ListTile( title: const Text('Trigger self-verification'), onTap: () => { - userCodeModel.userCodes - .map((code) => selfVerifyCard(context, code, Configuration.of(context).projectId, client)) + for (final userCode in userCodeModel.userCodes) + {() => selfVerifyCard(context, userCode, Configuration.of(context).projectId, client)} }), ListTile( title: const Text('Log sample exception'), @@ -240,7 +239,9 @@ class DevSettingsView extends StatelessWidget { final provider = Provider.of(context, listen: false); if (provider.userCodes.isNotEmpty) { List userCodes = provider.userCodes; - userCodes.map((code) => _setExpiredLastVerification(context, code)); + for (final userCode in userCodes) { + userCodes.map((code) => _setExpiredLastVerification(context, userCode)); + } } } diff --git a/frontend/lib/identification/card_detail_view/card_carousel.dart b/frontend/lib/identification/card_detail_view/card_carousel.dart index 06d215438..e6f1843ea 100644 --- a/frontend/lib/identification/card_detail_view/card_carousel.dart +++ b/frontend/lib/identification/card_detail_view/card_carousel.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; import 'package:carousel_slider/carousel_slider.dart'; class CardCarousel extends StatefulWidget { - final List userCards; + final List cards; final int cardIndex; final Function(int index) updateIndex; final CarouselController carouselController; const CardCarousel( {super.key, - required this.userCards, + required this.cards, required this.cardIndex, required this.updateIndex, required this.carouselController}); @@ -25,7 +25,7 @@ class CardCarouselState extends State { child: Column( children: [ CarouselSlider( - items: widget.userCards, + items: widget.cards, carouselController: widget.carouselController, options: CarouselOptions( enableInfiniteScroll: false, @@ -37,12 +37,12 @@ class CardCarouselState extends State { }); }), ), - if (widget.userCards.length > 1) + if (widget.cards.length > 1) Padding( padding: const EdgeInsets.only(top: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, - children: widget.userCards.asMap().entries.map((entry) { + children: widget.cards.asMap().entries.map((entry) { return GestureDetector( onTap: () => widget.carouselController.animateToPage(entry.key), child: Container( diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index 0d1d87513..3a71e88e8 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -17,7 +17,7 @@ class CardDetailView extends StatefulWidget { final VoidCallback startActivation; final VoidCallback startVerification; final VoidCallback startApplication; - final VoidCallback removeCard; + final VoidCallback openRemoveCardDialog; const CardDetailView( {super.key, @@ -25,7 +25,7 @@ class CardDetailView extends StatefulWidget { required this.startActivation, required this.startVerification, required this.startApplication, - required this.removeCard}); + required this.openRemoveCardDialog}); @override State createState() => _CardDetailViewState(); @@ -44,16 +44,18 @@ class _CardDetailViewState extends State { // - the card was activated on another device // - the card was revoked // - the card expired (on backend's system time) - _selfVerifyCard(); + _selfVerifyCards(); initiatedSelfVerification = true; } } - Future _selfVerifyCard() async { - final userCodeModel = Provider.of(context, listen: false); + Future _selfVerifyCards() async { + final userCodeModel = Provider.of(context, listen: false).userCodes; final projectId = Configuration.of(context).projectId; final client = GraphQLProvider.of(context).value; - userCodeModel.userCodes.map((code) => selfVerifyCard(context, code, projectId, client)); + for (final userCode in userCodeModel) { + selfVerifyCard(context, userCode, projectId, client); + } } @override @@ -88,7 +90,7 @@ class _CardDetailViewState extends State { final qrCodeAndStatus = QrCodeAndStatus( userCode: widget.userCode, onMoreActionsPressed: () => _onMoreActionsPressed(context), - onSelfVerifyPressed: _selfVerifyCard, + onSelfVerifyPressed: _selfVerifyCards, ); return orientation == Orientation.landscape @@ -133,7 +135,7 @@ class _CardDetailViewState extends State { startActivation: widget.startActivation, startApplication: widget.startApplication, startVerification: widget.startVerification, - removeCard: widget.removeCard), + openRemoveCardDialog: widget.openRemoveCardDialog), ); } } diff --git a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart index 1d843283f..9100a568d 100644 --- a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart +++ b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart @@ -7,14 +7,14 @@ class MoreActionsDialog extends StatelessWidget { final VoidCallback startActivation; final VoidCallback startVerification; final VoidCallback startApplication; - final VoidCallback removeCard; + final VoidCallback openRemoveCardDialog; const MoreActionsDialog({ super.key, required this.startActivation, required this.startVerification, required this.startApplication, - required this.removeCard, + required this.openRemoveCardDialog, }); @override @@ -65,7 +65,7 @@ class MoreActionsDialog extends StatelessWidget { leading: const Icon(Icons.delete, size: 36), onTap: () { Navigator.pop(context); - removeCard(); + openRemoveCardDialog(); }, ), ], diff --git a/frontend/lib/identification/identification_page.dart b/frontend/lib/identification/identification_page.dart index c674cb5fe..c4204f3b4 100644 --- a/frontend/lib/identification/identification_page.dart +++ b/frontend/lib/identification/identification_page.dart @@ -38,20 +38,20 @@ class IdentificationPageState extends State { } if (userCodeModel.userCodes.isNotEmpty) { - final List cards = []; + final List carouselCards = []; for (var code in userCodeModel.userCodes) { - cards.add(CardDetailView( + carouselCards.add(CardDetailView( userCode: code, - startVerification: () => _showVerificationDialog(context, settings), + startVerification: () => _showVerificationDialog(context, settings, userCodeModel), startActivation: () => _startActivation(context), startApplication: _startApplication, - removeCard: () => _removeCard(context), + openRemoveCardDialog: () => _openRemoveCardDialog(context), )); } return Column(children: [ CardCarousel( - userCards: cards, + cards: carouselCards, cardIndex: cardIndex, updateIndex: _updateCardIndex, carouselController: carouselController), @@ -59,7 +59,7 @@ class IdentificationPageState extends State { } return NoCardView( - startVerification: () => _showVerificationDialog(context, settings), + startVerification: () => _showVerificationDialog(context, settings, userCodeModel), startActivation: () => _startActivation(context), startApplication: _startApplication, ); @@ -71,9 +71,9 @@ class IdentificationPageState extends State { await QrCodeCameraPermissionDialog.showPermissionDialog(context); } - Future _showVerificationDialog(BuildContext context, SettingsModel settings) async { + Future _showVerificationDialog( + BuildContext context, SettingsModel settings, UserCodeModel userCodeModel) async { if (await Permission.camera.request().isGranted) { - final userCodeModel = Provider.of(context, listen: false); DynamicUserCode? userCode = userCodeModel.userCodes.isNotEmpty ? userCodeModel.userCodes[cardIndex] : null; await VerificationWorkflow.startWorkflow(context, settings, userCode); return; @@ -103,10 +103,10 @@ class IdentificationPageState extends State { ); } - Future _removeCard(BuildContext context) async { + Future _openRemoveCardDialog(BuildContext context) async { final userCodeModel = Provider.of(context, listen: false); - await RemoveCardConfirmationDialog.show(context: context, userCode: userCodeModel.userCodes[cardIndex]); - carouselController.previousPage(duration: Duration(milliseconds: 500), curve: Curves.linear); + await RemoveCardConfirmationDialog.show( + context: context, userCode: userCodeModel.userCodes[cardIndex], carouselController: carouselController); } void _moveCarouselToLastPosition() { diff --git a/frontend/lib/identification/user_code_model.dart b/frontend/lib/identification/user_code_model.dart index da30de712..4368b8ad3 100644 --- a/frontend/lib/identification/user_code_model.dart +++ b/frontend/lib/identification/user_code_model.dart @@ -25,7 +25,7 @@ class UserCodeModel extends ChangeNotifier { await UserCodeStore().importLegacyCard(); _userCodes = await const UserCodeStore().load(); } on Exception catch (e) { - log('Failed to initialize activation code from secure storage of codes.', error: e); + log('Failed to initialize activation code from secure storage.', error: e); } finally { _isInitialized = true; notifyListeners(); diff --git a/frontend/lib/identification/user_code_store.dart b/frontend/lib/identification/user_code_store.dart index 6254740ab..5e164ecef 100644 --- a/frontend/lib/identification/user_code_store.dart +++ b/frontend/lib/identification/user_code_store.dart @@ -10,12 +10,11 @@ class UserCodeStore { static const _userCodesBase64Key = 'userCodesBase64'; // legacy key for single card static const _userCodeBase64Key = 'userCodeBase64'; - + static const _storageDelimiter = ','; Future store(List userCodes) async { const storage = FlutterSecureStorage(); Iterable userCodeString = userCodes.map((code) => const Base64Encoder().convert(code.writeToBuffer())); - - await storage.write(key: _userCodesBase64Key, value: userCodeString.join(',')); + await storage.write(key: _userCodesBase64Key, value: userCodeString.join(_storageDelimiter)); } Future remove() async { @@ -28,12 +27,12 @@ class UserCodeStore { final String? userCodesBase64 = await storage.read(key: _userCodesBase64Key); if (userCodesBase64 == null) return []; return userCodesBase64 - .split(',') + .split(_storageDelimiter) .map((code) => DynamicUserCode.fromBuffer(const Base64Decoder().convert(code))) .toList(); } - // legacy import for single cards to keep compatible after update + // legacy import of existing to keep them in storage after update Future importLegacyCard() async { const storage = FlutterSecureStorage(); final String? userCodeBase64 = await storage.read(key: _userCodeBase64Key); diff --git a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart index f600d53bf..0bada3edb 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart @@ -1,3 +1,4 @@ +import 'package:carousel_slider/carousel_controller.dart'; import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/configuration/configuration.dart'; import 'package:ehrenamtskarte/graphql/graphql_api.dart'; @@ -10,16 +11,17 @@ import 'package:provider/provider.dart'; class RemoveCardConfirmationDialog extends StatefulWidget { final DynamicUserCode userCode; + final CarouselController carouselController; - const RemoveCardConfirmationDialog({super.key, required this.userCode}); + const RemoveCardConfirmationDialog({super.key, required this.userCode, required this.carouselController}); - static Future show({ - required BuildContext context, - required DynamicUserCode userCode, - }) => + static Future show( + {required BuildContext context, + required DynamicUserCode userCode, + required CarouselController carouselController}) => showDialog( context: context, - builder: (_) => RemoveCardConfirmationDialog(userCode: userCode), + builder: (_) => RemoveCardConfirmationDialog(userCode: userCode, carouselController: carouselController), ); @override @@ -57,7 +59,7 @@ class RemoveCardConfirmationDialogState extends State[ Padding( padding: EdgeInsets.only(bottom: 20), - child: Text(localization.description, style: TextStyle(fontSize: 12))), + child: Text(localization.description, style: TextStyle(fontSize: 14))), IdCard( cardInfo: widget.userCode.info, region: region != null ? Region(region.prefix, region.name) : null, @@ -83,6 +85,7 @@ class RemoveCardConfirmationDialogState extends State Date: Mon, 9 Oct 2023 14:06:33 +0200 Subject: [PATCH 12/60] 1084: fix carousel overflow and adjust indicator padding --- .../card_detail_view/card_carousel.dart | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/lib/identification/card_detail_view/card_carousel.dart b/frontend/lib/identification/card_detail_view/card_carousel.dart index e6f1843ea..59329ffb7 100644 --- a/frontend/lib/identification/card_detail_view/card_carousel.dart +++ b/frontend/lib/identification/card_detail_view/card_carousel.dart @@ -24,22 +24,24 @@ class CardCarouselState extends State { return Expanded( child: Column( children: [ - CarouselSlider( - items: widget.cards, - carouselController: widget.carouselController, - options: CarouselOptions( - enableInfiniteScroll: false, - viewportFraction: 0.96, - aspectRatio: 9 / 16, - onPageChanged: (index, reason) { - setState(() { - widget.updateIndex(index); - }); - }), + Expanded( + child: CarouselSlider( + items: widget.cards, + carouselController: widget.carouselController, + options: CarouselOptions( + enableInfiniteScroll: false, + viewportFraction: 0.96, + aspectRatio: 9 / 16, + onPageChanged: (index, reason) { + setState(() { + widget.updateIndex(index); + }); + }), + ), ), if (widget.cards.length > 1) Padding( - padding: const EdgeInsets.only(top: 16.0), + padding: const EdgeInsets.only(top: 12.0, bottom: 12.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: widget.cards.asMap().entries.map((entry) { From c0519cd8bb63000e7a4d0a337688a87bb3a2f788 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 9 Oct 2023 19:17:34 +0200 Subject: [PATCH 13/60] 1084: fix minor issues in dev_settings_view --- frontend/lib/about/dev_settings_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/lib/about/dev_settings_view.dart b/frontend/lib/about/dev_settings_view.dart index ea01db470..643e09fef 100644 --- a/frontend/lib/about/dev_settings_view.dart +++ b/frontend/lib/about/dev_settings_view.dart @@ -82,7 +82,7 @@ class DevSettingsView extends StatelessWidget { title: const Text('Trigger self-verification'), onTap: () => { for (final userCode in userCodeModel.userCodes) - {() => selfVerifyCard(context, userCode, Configuration.of(context).projectId, client)} + {selfVerifyCard(context, userCode, Configuration.of(context).projectId, client)} }), ListTile( title: const Text('Log sample exception'), @@ -240,7 +240,7 @@ class DevSettingsView extends StatelessWidget { if (provider.userCodes.isNotEmpty) { List userCodes = provider.userCodes; for (final userCode in userCodes) { - userCodes.map((code) => _setExpiredLastVerification(context, userCode)); + _setExpiredLastVerification(context, userCode); } } } From 346e60292adfeb733af6d1deddba92698d281ac8 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 10 Oct 2023 12:05:40 +0200 Subject: [PATCH 14/60] 1084: add scroll view and fixed footer to card carousel --- .../card_detail_view/card_carousel.dart | 85 ++++++++++--------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/frontend/lib/identification/card_detail_view/card_carousel.dart b/frontend/lib/identification/card_detail_view/card_carousel.dart index 59329ffb7..b9c7af654 100644 --- a/frontend/lib/identification/card_detail_view/card_carousel.dart +++ b/frontend/lib/identification/card_detail_view/card_carousel.dart @@ -18,49 +18,58 @@ class CardCarousel extends StatefulWidget { CardCarouselState createState() => CardCarouselState(); } +// Default bottomNavigationBarHeight in flutter +// https://api.flutter.dev/flutter/material/NavigationBar/height.html +final double bottomNavigationBarHeight = 80; +final double indicatorHeight = 28; + class CardCarouselState extends State { @override Widget build(BuildContext context) { return Expanded( - child: Column( - children: [ - Expanded( - child: CarouselSlider( - items: widget.cards, - carouselController: widget.carouselController, - options: CarouselOptions( - enableInfiniteScroll: false, - viewportFraction: 0.96, - aspectRatio: 9 / 16, - onPageChanged: (index, reason) { - setState(() { - widget.updateIndex(index); - }); - }), - ), - ), - if (widget.cards.length > 1) - Padding( - padding: const EdgeInsets.only(top: 12.0, bottom: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: widget.cards.asMap().entries.map((entry) { - return GestureDetector( - onTap: () => widget.carouselController.animateToPage(entry.key), - child: Container( - width: 12.0, - height: 12.0, - margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black) - .withOpacity(widget.cardIndex == entry.key ? 0.9 : 0.4)), - ), - ); - }).toList(), + child: Column( + children: [ + SingleChildScrollView( + child: CarouselSlider( + items: widget.cards, + carouselController: widget.carouselController, + options: CarouselOptions( + enableInfiniteScroll: false, + viewportFraction: 0.96, + height: MediaQuery.of(context).size.height - bottomNavigationBarHeight - indicatorHeight, + onPageChanged: (index, reason) { + setState(() { + widget.updateIndex(index); + }); + }), ), ), - ], - )); + if (widget.cards.length > 1) + Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Container( + height: indicatorHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: widget.cards.asMap().entries.map((entry) { + return GestureDetector( + onTap: () => widget.carouselController.animateToPage(entry.key), + child: Container( + width: 12.0, + height: 12, + margin: EdgeInsets.symmetric(horizontal: 4.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black) + .withOpacity(widget.cardIndex == entry.key ? 0.9 : 0.4)), + ), + ); + }).toList(), + ), + ), + ), + ], + ), + ); } } From a76138fe96e70bfd738250b07379106780eba322 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 10 Oct 2023 12:12:30 +0200 Subject: [PATCH 15/60] 1084: add scroll view and fixed footer to card carousel --- .../lib/identification/card_detail_view/card_carousel.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/lib/identification/card_detail_view/card_carousel.dart b/frontend/lib/identification/card_detail_view/card_carousel.dart index b9c7af654..f6b88bf31 100644 --- a/frontend/lib/identification/card_detail_view/card_carousel.dart +++ b/frontend/lib/identification/card_detail_view/card_carousel.dart @@ -21,11 +21,12 @@ class CardCarousel extends StatefulWidget { // Default bottomNavigationBarHeight in flutter // https://api.flutter.dev/flutter/material/NavigationBar/height.html final double bottomNavigationBarHeight = 80; -final double indicatorHeight = 28; class CardCarouselState extends State { @override Widget build(BuildContext context) { + final double indicatorHeight = widget.cards.length > 1 ? 28 : 0; + return Expanded( child: Column( children: [ From b8abaaf719761961e2c40792419153c439acf253 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 10 Oct 2023 16:08:57 +0200 Subject: [PATCH 16/60] 1084: fix carousel scroll container, adjust more actions view for card limit reached --- frontend/build-configs/bayern/localization.ts | 1 + .../build-configs/nuernberg/localization.ts | 1 + frontend/build-configs/types.ts | 1 + .../card_detail_view/card_carousel.dart | 59 ++++++++++--------- .../card_detail_view/more_actions_dialog.dart | 30 ++++++---- .../identification/identification_page.dart | 12 ++-- .../remove_card_confirmation_dialog.dart | 4 +- 7 files changed, 57 insertions(+), 51 deletions(-) diff --git a/frontend/build-configs/bayern/localization.ts b/frontend/build-configs/bayern/localization.ts index 4e218b184..4f9b69402 100644 --- a/frontend/build-configs/bayern/localization.ts +++ b/frontend/build-configs/bayern/localization.ts @@ -26,6 +26,7 @@ const localization: LocalizationType = { applyForAnotherCardDescription: "Ihre hinterlegte Karte bleibt erhalten.", activateAnotherCardTitle: "Weitere Ehrenamtskarte hinzufügen", activateAnotherCardDescription: "Ihr hinterlegte Ehrenamtskarte bleibt erhalten. Sie können diesen manuell entfernen.", + activationLimitDescription: "Um eine weitere Ehrenamtskarte hinzuzufügen, müssen Sie zuerst eine vorhandene Ehrenamtskarte löschen.", verifyTitle: "Eine digitale Ehrenamtskarte prüfen", verifyDescription: "Prüfen Sie die Gültigkeit einer digitalen Ehrenamtskarte.", removeCardTitle: "Diese Ehrenamtskarte löschen", diff --git a/frontend/build-configs/nuernberg/localization.ts b/frontend/build-configs/nuernberg/localization.ts index db24dcfc4..76adc28f6 100644 --- a/frontend/build-configs/nuernberg/localization.ts +++ b/frontend/build-configs/nuernberg/localization.ts @@ -26,6 +26,7 @@ const localization: LocalizationType = { applyForAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten.", activateAnotherCardTitle: "Weiteren Nürnberg-Pass hinzufügen", activateAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten. Sie können diesen manuell entfernen.", + activationLimitDescription: "Um einen weiteren Pass hinzuzufügen, müssen Sie zuerst einen der vorhandenen Pässe löschen.", verifyTitle: "Einen Nürnberg-Pass prüfen", verifyDescription: "Prüfen Sie die Gültigkeit eines Nürnberg-Passes.", removeCardTitle: "Diesen Nürnberg-Pass löschen", diff --git a/frontend/build-configs/types.ts b/frontend/build-configs/types.ts index bcb3e7c42..40e526fcd 100644 --- a/frontend/build-configs/types.ts +++ b/frontend/build-configs/types.ts @@ -42,6 +42,7 @@ export type LocalizationType = { applyForAnotherCardDescription: string activateAnotherCardTitle: string activateAnotherCardDescription: string + activationLimitDescription: string verifyTitle: string verifyDescription: string removeCardTitle: string diff --git a/frontend/lib/identification/card_detail_view/card_carousel.dart b/frontend/lib/identification/card_detail_view/card_carousel.dart index f6b88bf31..9647306b1 100644 --- a/frontend/lib/identification/card_detail_view/card_carousel.dart +++ b/frontend/lib/identification/card_detail_view/card_carousel.dart @@ -25,12 +25,13 @@ final double bottomNavigationBarHeight = 80; class CardCarouselState extends State { @override Widget build(BuildContext context) { - final double indicatorHeight = widget.cards.length > 1 ? 28 : 0; + final int cardAmount = widget.cards.length; + final double indicatorHeight = cardAmount > 1 ? 16 : 0; - return Expanded( - child: Column( - children: [ - SingleChildScrollView( + return Column( + children: [ + Expanded( + child: SingleChildScrollView( child: CarouselSlider( items: widget.cards, carouselController: widget.carouselController, @@ -45,32 +46,32 @@ class CardCarouselState extends State { }), ), ), - if (widget.cards.length > 1) - Padding( - padding: EdgeInsets.symmetric(vertical: 10), - child: Container( - height: indicatorHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: widget.cards.asMap().entries.map((entry) { - return GestureDetector( - onTap: () => widget.carouselController.animateToPage(entry.key), - child: Container( - width: 12.0, - height: 12, - margin: EdgeInsets.symmetric(horizontal: 4.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black) - .withOpacity(widget.cardIndex == entry.key ? 0.9 : 0.4)), - ), - ); - }).toList(), - ), + ), + if (cardAmount > 1) + Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Container( + height: indicatorHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: widget.cards.asMap().entries.map((entry) { + return GestureDetector( + onTap: () => widget.carouselController.animateToPage(entry.key), + child: Container( + width: 8.0, + height: 8.0, + margin: EdgeInsets.symmetric(horizontal: 4.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black) + .withOpacity(widget.cardIndex == entry.key ? 0.9 : 0.4)), + ), + ); + }).toList(), ), ), - ], - ), + ), + ], ); } } diff --git a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart index 9100a568d..bdaffdf1b 100644 --- a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart +++ b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart @@ -1,4 +1,4 @@ -import 'package:ehrenamtskarte/build_config/build_config.dart'; +import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -21,8 +21,10 @@ class MoreActionsDialog extends StatelessWidget { Widget build(BuildContext context) { final localization = buildConfig.localization.identification.moreActions; final userCodeModel = Provider.of(context, listen: false); - final String availableCards = (buildConfig.maxCardAmount - userCodeModel.userCodes.length).toString(); + final String cardsInUse = userCodeModel.userCodes.length.toString(); final String maxCardAmount = buildConfig.maxCardAmount.toString(); + final bool cardLimitIsReached = hasReachedCardLimit(userCodeModel.userCodes); + return AlertDialog( contentPadding: const EdgeInsets.only(top: 12), title: const Text('Weitere Aktionen'), @@ -48,17 +50,19 @@ class MoreActionsDialog extends StatelessWidget { startVerification(); }, ), - if (!hasReachedCardLimit(userCodeModel.userCodes)) - ListTile( - title: Text(localization.activateAnotherCardTitle), - subtitle: Text( - '${localization.activateAnotherCardDescription}\n(Noch $availableCards von $maxCardAmount frei)'), - leading: const Icon(Icons.add_card, size: 36), - onTap: () { - Navigator.pop(context); - startActivation(); - }, - ), + ListTile( + enabled: !cardLimitIsReached, + title: Text('${localization.activateAnotherCardTitle} ($cardsInUse/$maxCardAmount)', + style: TextStyle(color: Theme.of(context).colorScheme.onBackground)), + subtitle: Text(cardLimitIsReached + ? localization.activationLimitDescription + : localization.activateAnotherCardDescription), + leading: Icon(Icons.add_card, size: 36), + onTap: () { + Navigator.pop(context); + startActivation(); + }, + ), ListTile( title: Text(localization.removeCardTitle), subtitle: Text(localization.removeCardDescription), diff --git a/frontend/lib/identification/identification_page.dart b/frontend/lib/identification/identification_page.dart index c4204f3b4..24b55a3b1 100644 --- a/frontend/lib/identification/identification_page.dart +++ b/frontend/lib/identification/identification_page.dart @@ -49,13 +49,11 @@ class IdentificationPageState extends State { )); } - return Column(children: [ - CardCarousel( - cards: carouselCards, - cardIndex: cardIndex, - updateIndex: _updateCardIndex, - carouselController: carouselController), - ]); + return CardCarousel( + cards: carouselCards, + cardIndex: cardIndex, + updateIndex: _updateCardIndex, + carouselController: carouselController); } return NoCardView( diff --git a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart index 0bada3edb..4a23af32d 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart @@ -48,8 +48,8 @@ class RemoveCardConfirmationDialogState extends State Date: Tue, 10 Oct 2023 22:22:09 +0200 Subject: [PATCH 17/60] 1084: add method to update cards for self verification --- frontend/lib/about/dev_settings_view.dart | 6 +++--- .../activation_code_scanner_page.dart | 2 +- .../card_detail_view/self_verify_card.dart | 2 +- .../lib/identification/user_code_model.dart | 17 ++++++++++++++++- .../lib/identification/user_code_store.dart | 4 ++-- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/frontend/lib/about/dev_settings_view.dart b/frontend/lib/about/dev_settings_view.dart index 643e09fef..a61951ca3 100644 --- a/frontend/lib/about/dev_settings_view.dart +++ b/frontend/lib/about/dev_settings_view.dart @@ -125,7 +125,7 @@ class DevSettingsView extends StatelessWidget { } Future _setSampleCard(BuildContext context) async { - Provider.of(context, listen: false).setCode(_determineUserCode(buildConfig.projectId.local)); + Provider.of(context, listen: false).insertCode(_determineUserCode(buildConfig.projectId.local)); } Future _showRawCardInput(BuildContext context) async { @@ -195,7 +195,7 @@ class DevSettingsView extends StatelessWidget { ..info = activationCode.info ..pepper = activationCode.pepper ..totpSecret = totpSecret; - provider.setCode(userCode); + provider.insertCode(userCode); break; case ActivationState.failed: await QrParsingErrorDialog.showErrorDialog( @@ -252,7 +252,7 @@ class DevSettingsView extends StatelessWidget { ..verificationTimeStamp = secondsSinceEpoch(DateTime.now().toUtc().subtract(Duration(seconds: cardValidationExpireSeconds + 3600))) ..cardValid = true; - provider.setCode(DynamicUserCode() + provider.updateCode(DynamicUserCode() ..info = userCode.info ..ecSignature = userCode.ecSignature ..pepper = userCode.pepper diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 991cfd6bc..079c5ecb1 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -120,7 +120,7 @@ class ActivationCodeScannerPage extends StatelessWidget { if (isAlreadyInList(provider.userCodes, userCode)) { await ActivationExistingCardDialog.showExistingCardDialog(context); } - provider.setCode(userCode); + provider.insertCode(userCode); if (provider.userCodes.length > 1) { moveToLastCard(); } diff --git a/frontend/lib/identification/card_detail_view/self_verify_card.dart b/frontend/lib/identification/card_detail_view/self_verify_card.dart index 67f77af62..5b937266a 100644 --- a/frontend/lib/identification/card_detail_view/self_verify_card.dart +++ b/frontend/lib/identification/card_detail_view/self_verify_card.dart @@ -35,7 +35,7 @@ Future selfVerifyCard( debugPrint("Card Self-Verification: Persisting response. Card is ${cardVerification.valid ? "valid." : "INVALID."}"); final userCodeModel = Provider.of(context, listen: false); - userCodeModel.setCode(DynamicUserCode() + userCodeModel.updateCode(DynamicUserCode() ..info = initialUserCode.info ..ecSignature = initialUserCode.ecSignature ..pepper = initialUserCode.pepper diff --git a/frontend/lib/identification/user_code_model.dart b/frontend/lib/identification/user_code_model.dart index 4368b8ad3..bf2bfe53d 100644 --- a/frontend/lib/identification/user_code_model.dart +++ b/frontend/lib/identification/user_code_model.dart @@ -32,7 +32,7 @@ class UserCodeModel extends ChangeNotifier { } } - void setCode(DynamicUserCode code) { + void insertCode(DynamicUserCode code) { List userCodes = _userCodes; if (isAlreadyInList(userCodes, code)) return; userCodes.add(code); @@ -41,6 +41,16 @@ class UserCodeModel extends ChangeNotifier { notifyListeners(); } + void updateCode(DynamicUserCode code) { + List userCodes = _userCodes; + if (isAlreadyInList(userCodes, code)) { + userCodes = updateUserCode(userCodes, code); + const UserCodeStore().store(userCodes); + _userCodes = userCodes; + notifyListeners(); + } + } + void removeCode(DynamicUserCode code) { List userCodes = _userCodes; userCodes.remove(code); @@ -54,6 +64,11 @@ class UserCodeModel extends ChangeNotifier { _userCodes = []; notifyListeners(); } + + List updateUserCode(List userCodes, DynamicUserCode userCode) { + userCodes[userCodes.indexWhere((code) => code.info == userCode.info)] = userCode; + return userCodes; + } } bool isAlreadyInList(List userCodes, DynamicUserCode code) { diff --git a/frontend/lib/identification/user_code_store.dart b/frontend/lib/identification/user_code_store.dart index 5e164ecef..47379c593 100644 --- a/frontend/lib/identification/user_code_store.dart +++ b/frontend/lib/identification/user_code_store.dart @@ -32,13 +32,13 @@ class UserCodeStore { .toList(); } - // legacy import of existing to keep them in storage after update + // legacy import of existing card to keep them in storage after update Future importLegacyCard() async { const storage = FlutterSecureStorage(); final String? userCodeBase64 = await storage.read(key: _userCodeBase64Key); if (userCodeBase64 == null) return; DynamicUserCode importedLegacyCard = DynamicUserCode.fromBuffer(const Base64Decoder().convert(userCodeBase64)); - UserCodeModel().setCode(importedLegacyCard); + UserCodeModel().insertCode(importedLegacyCard); await storage.delete(key: _userCodeBase64Key); } } From 19dce6457573a6aab99ef4074624abcd099c1a41 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 10 Oct 2023 23:07:57 +0200 Subject: [PATCH 18/60] 1084: minor fixes --- frontend/lib/about/backend_switch_dialog.dart | 4 ++-- frontend/lib/about/dev_settings_view.dart | 18 +++++++++--------- .../activation_code_scanner_page.dart | 9 +++++---- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/frontend/lib/about/backend_switch_dialog.dart b/frontend/lib/about/backend_switch_dialog.dart index 212f53544..ba04b5143 100644 --- a/frontend/lib/about/backend_switch_dialog.dart +++ b/frontend/lib/about/backend_switch_dialog.dart @@ -84,8 +84,8 @@ class BackendSwitchDialogState extends State { void clearData() { final settings = Provider.of(context, listen: false); - final userCodes = Provider.of(context, listen: false); + final userCodesModel = Provider.of(context, listen: false); settings.clearSettings(); - userCodes.removeCodes(); + userCodesModel.removeCodes(); } } diff --git a/frontend/lib/about/dev_settings_view.dart b/frontend/lib/about/dev_settings_view.dart index a61951ca3..b984be0cc 100644 --- a/frontend/lib/about/dev_settings_view.dart +++ b/frontend/lib/about/dev_settings_view.dart @@ -103,8 +103,8 @@ class DevSettingsView extends StatelessWidget { ); } - Future _resetEakData(BuildContext context, UserCodeModel userCodes) async { - userCodes.removeCodes(); + Future _resetEakData(BuildContext context, UserCodeModel userCodesModel) async { + userCodesModel.removeCodes(); } DynamicUserCode _determineUserCode(String projectId) { @@ -170,7 +170,7 @@ class DevSettingsView extends StatelessWidget { Future _activateCard(BuildContext context, String base64qrcode) async { final messengerState = ScaffoldMessenger.of(context); - final provider = Provider.of(context, listen: false); + final userCodesModel = Provider.of(context, listen: false); final client = GraphQLProvider.of(context).value; final projectId = Configuration.of(context).projectId; try { @@ -195,7 +195,7 @@ class DevSettingsView extends StatelessWidget { ..info = activationCode.info ..pepper = activationCode.pepper ..totpSecret = totpSecret; - provider.insertCode(userCode); + userCodesModel.insertCode(userCode); break; case ActivationState.failed: await QrParsingErrorDialog.showErrorDialog( @@ -236,9 +236,9 @@ class DevSettingsView extends StatelessWidget { } void _setExpiredLastVerifications(BuildContext context) { - final provider = Provider.of(context, listen: false); - if (provider.userCodes.isNotEmpty) { - List userCodes = provider.userCodes; + final userCodesModel = Provider.of(context, listen: false); + if (userCodesModel.userCodes.isNotEmpty) { + List userCodes = userCodesModel.userCodes; for (final userCode in userCodes) { _setExpiredLastVerification(context, userCode); } @@ -247,12 +247,12 @@ class DevSettingsView extends StatelessWidget { // This is used to check the invalidation of a card because the verification with the backend couldn't be done lately (1 week plus UTC tolerance) void _setExpiredLastVerification(BuildContext context, DynamicUserCode userCode) { - final provider = Provider.of(context, listen: false); + final userCodesModel = Provider.of(context, listen: false); final CardVerification cardVerification = CardVerification() ..verificationTimeStamp = secondsSinceEpoch(DateTime.now().toUtc().subtract(Duration(seconds: cardValidationExpireSeconds + 3600))) ..cardValid = true; - provider.updateCode(DynamicUserCode() + userCodesModel.updateCode(DynamicUserCode() ..info = userCode.info ..ecSignature = userCode.ecSignature ..pepper = userCode.pepper diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 079c5ecb1..0f952d560 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -88,7 +88,7 @@ class ActivationCodeScannerPage extends StatelessWidget { ]) async { final client = GraphQLProvider.of(context).value; final projectId = Configuration.of(context).projectId; - final provider = Provider.of(context, listen: false); + final userCodesModel = Provider.of(context, listen: false); final activationSecretBase64 = const Base64Encoder().convert(activationCode.activationSecret); final cardInfoBase64 = activationCode.info.hash(activationCode.pepper); @@ -117,11 +117,12 @@ class ActivationCodeScannerPage extends StatelessWidget { ..cardVerification = (CardVerification() ..cardValid = true ..verificationTimeStamp = secondsSinceEpoch(DateTime.parse(activationResult.activationTimeStamp))); - if (isAlreadyInList(provider.userCodes, userCode)) { + if (isAlreadyInList(userCodesModel.userCodes, userCode)) { await ActivationExistingCardDialog.showExistingCardDialog(context); + break; } - provider.insertCode(userCode); - if (provider.userCodes.length > 1) { + userCodesModel.insertCode(userCode); + if (userCodesModel.userCodes.length > 1) { moveToLastCard(); } debugPrint('Card Activation: Successfully activated.'); From 3ff92fb557f020d71343b607d2d2e4e7f2857c3c Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 11 Oct 2023 17:08:27 +0200 Subject: [PATCH 19/60] 1084: only verify the particular card on each CardDetail widget --- .../card_detail_view/card_detail_view.dart | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index 3a71e88e8..20d99a540 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -3,12 +3,10 @@ import 'package:ehrenamtskarte/graphql/graphql_api.dart'; import 'package:ehrenamtskarte/identification/card_detail_view/more_actions_dialog.dart'; import 'package:ehrenamtskarte/identification/card_detail_view/self_verify_card.dart'; import 'package:ehrenamtskarte/identification/id_card/id_card.dart'; -import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/identification/util/card_info_utils.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; -import 'package:provider/provider.dart'; import 'verification_code_view.dart'; @@ -44,18 +42,15 @@ class _CardDetailViewState extends State { // - the card was activated on another device // - the card was revoked // - the card expired (on backend's system time) - _selfVerifyCards(); + _selfVerifyCard(widget.userCode); initiatedSelfVerification = true; } } - Future _selfVerifyCards() async { - final userCodeModel = Provider.of(context, listen: false).userCodes; + Future _selfVerifyCard(DynamicUserCode userCode) async { final projectId = Configuration.of(context).projectId; final client = GraphQLProvider.of(context).value; - for (final userCode in userCodeModel) { - selfVerifyCard(context, userCode, projectId, client); - } + selfVerifyCard(context, userCode, projectId, client); } @override @@ -90,7 +85,7 @@ class _CardDetailViewState extends State { final qrCodeAndStatus = QrCodeAndStatus( userCode: widget.userCode, onMoreActionsPressed: () => _onMoreActionsPressed(context), - onSelfVerifyPressed: _selfVerifyCards, + onSelfVerifyPressed: () => _selfVerifyCard(widget.userCode), ); return orientation == Orientation.landscape From 035fd39d33c2b8ce6715582ed7dd15365a8946ee Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 12 Oct 2023 09:56:34 +0200 Subject: [PATCH 20/60] 1150: fix typo --- .../src/bp-modules/applications/ApplicationCard.tsx | 2 +- .../ApplicationVerificationController.tsx | 6 +++--- .../main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt | 4 ++-- docs/artefacts/beantragung_zusammenfassung.md | 2 +- docs/artefacts/beantragungsprozess_entwurf.drawio.svg | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/administration/src/bp-modules/applications/ApplicationCard.tsx b/administration/src/bp-modules/applications/ApplicationCard.tsx index e39df26e5..f12940586 100644 --- a/administration/src/bp-modules/applications/ApplicationCard.tsx +++ b/administration/src/bp-modules/applications/ApplicationCard.tsx @@ -150,7 +150,7 @@ const ApplicationCard = ({ {withdrawalDate && ( - Der Antrag wurde vom Antragssteller am {formatDateWithTimezone(withdrawalDate, config.timezone)}{' '} + Der Antrag wurde vom Antragsteller am {formatDateWithTimezone(withdrawalDate, config.timezone)}{' '} zurückgezogen.
Bitte löschen Sie den Antrag zeitnah.
diff --git a/administration/src/mui-modules/application-verification/ApplicationVerificationController.tsx b/administration/src/mui-modules/application-verification/ApplicationVerificationController.tsx index faf6580fc..5d023bb6f 100644 --- a/administration/src/mui-modules/application-verification/ApplicationVerificationController.tsx +++ b/administration/src/mui-modules/application-verification/ApplicationVerificationController.tsx @@ -83,7 +83,7 @@ const ApplicationVerification = ({ applicationVerificationAccessKey }: Applicati if (application.withdrawalDate) return (
- Sie wurden gebeten, die Angaben eines Antrags auf Ehrenamtskarte zu bestätigen. Die Antragsstellerin oder der - Antragssteller hat Sie als Kontaktperson der Organisation {verification.organizationName} angegeben. Im + Sie wurden gebeten, die Angaben eines Antrags auf Ehrenamtskarte zu bestätigen. Die Antragstellerin oder der + Antragsteller hat Sie als Kontaktperson der Organisation {verification.organizationName} angegeben. Im Folgenden können Sie den zugehörigen Antrag einsehen. Wir bitten Sie, die enthaltenen Angaben, welche die Organisation {verification.organizationName} betreffen, zu bestätigen. Falls Sie denken, die Angaben wurden fälschlicherweise gemacht, bitten wir Sie, den Angaben zu widersprechen. diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt index b76f2878d..135b47c26 100644 --- a/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt @@ -134,8 +134,8 @@ object Mailer { val message = """ Guten Tag ${applicationVerification.contactName}, - Sie wurden gebeten, die Angaben eines Antrags auf eine Ehrenamtskarte zu bestätigen. Die Antragsstellerin oder der - Antragssteller hat Sie als Kontaktperson der Organisation ${applicationVerification.organizationName} angegeben. + Sie wurden gebeten, die Angaben eines Antrags auf eine Ehrenamtskarte zu bestätigen. Die Antragstellerin oder der + Antragsteller hat Sie als Kontaktperson der Organisation ${applicationVerification.organizationName} angegeben. Sie können den Antrag unter folgendem Link einsehen und die Angaben bestätigen oder ihnen widersprechen: ${projectConfig.administrationBaseUrl}/antrag-verifizieren/${URLEncoder.encode(applicationVerification.accessKey, StandardCharsets.UTF_8)} diff --git a/docs/artefacts/beantragung_zusammenfassung.md b/docs/artefacts/beantragung_zusammenfassung.md index fe98d3ce2..61e4f7ac1 100644 --- a/docs/artefacts/beantragung_zusammenfassung.md +++ b/docs/artefacts/beantragung_zusammenfassung.md @@ -54,7 +54,7 @@ Auswahl aus #### Bestätigung der Organisation - Ort, Datum, Unterschrift/Stempel der Organistation -- AntragsstellerIn (Organisation/AntragsstellerIn) +- AntragstellerIn (Organisation/AntragstellerIn) - Organisation wird gefördert durch (Freitext, optional) ### 2. Juleica Inhaber (Jugendleiterkarte) diff --git a/docs/artefacts/beantragungsprozess_entwurf.drawio.svg b/docs/artefacts/beantragungsprozess_entwurf.drawio.svg index 34e57f397..aa7a96f36 100644 --- a/docs/artefacts/beantragungsprozess_entwurf.drawio.svg +++ b/docs/artefacts/beantragungsprozess_entwurf.drawio.svg @@ -1,3 +1,3 @@ -
Prozess Beantragung analoge Ehrenamtskarte
Prozess Beantragung analoge Ehrenamtskarte
Antragssteller (Engagierte/Organisationen)
Antragssteller (Engagierte/Organisationen)
Ja
Ja
Antrag
ausgefüllt
Antrag...
Absage erhalten
Absage erhalten
Antrag beendet
Ehrenamtskarte erhalten
Ehrenamtskarte e...
Sind Daten auf Karte korrekt?
Sind D...
Antrag beendet
Ehrenamtskarte erhalten
Ehrenamtskarte e...
Hier würde wir 
einen Code mitschicken
Hier...
Hier würden wir
einen Code mitschicken
Hier...
Nein
Nein
Kreisfreie Stadt/Landkreis
Kreisfreie Stadt/Landkreis
Antrag empfangen
Antrag empfangen
Nein
Nein
Antrag genehmigt?
Antrag...
Absage formulieren
Absage formulier...
Daten korrigieren
Daten korrigiere...
Ehrenamtskarte drucken
Ehrenamtskarte d...
Ehrenamtskarte per Post verschicken
Ehrenamtskarte p...
Karten in Brief
verpacken
Karten in Brief...
Ehrenamtskarte drucken
Ehrenamtskarte d...
Drucken auslagern?
Drucke...
Ja
Ja
Druckerei (Sozialministerium)
Druckerei (Sozialministerium)
Ehrenamtskarte per Post zurückschicken
Ehrenamtskarte p...
Ehrenamtskarte drucken
Ehrenamtskarte d...
Antrag per Post
Antrag per Post
Ja
Ja
Viewer does not support full SVG 1.1
\ No newline at end of file +
Prozess Beantragung analoge Ehrenamtskarte
Prozess Beantragung analoge Ehrenamtskarte
Antragsteller (Engagierte/Organisationen)
Antragssteller (Engagierte/Organisationen)
Ja
Ja
Antrag
ausgefüllt
Antrag...
Absage erhalten
Absage erhalten
Antrag beendet
Ehrenamtskarte erhalten
Ehrenamtskarte e...
Sind Daten auf Karte korrekt?
Sind D...
Antrag beendet
Ehrenamtskarte erhalten
Ehrenamtskarte e...
Hier würde wir 
einen Code mitschicken
Hier...
Hier würden wir
einen Code mitschicken
Hier...
Nein
Nein
Kreisfreie Stadt/Landkreis
Kreisfreie Stadt/Landkreis
Antrag empfangen
Antrag empfangen
Nein
Nein
Antrag genehmigt?
Antrag...
Absage formulieren
Absage formulier...
Daten korrigieren
Daten korrigiere...
Ehrenamtskarte drucken
Ehrenamtskarte d...
Ehrenamtskarte per Post verschicken
Ehrenamtskarte p...
Karten in Brief
verpacken
Karten in Brief...
Ehrenamtskarte drucken
Ehrenamtskarte d...
Drucken auslagern?
Drucke...
Ja
Ja
Druckerei (Sozialministerium)
Druckerei (Sozialministerium)
Ehrenamtskarte per Post zurückschicken
Ehrenamtskarte p...
Ehrenamtskarte drucken
Ehrenamtskarte d...
Antrag per Post
Antrag per Post
Ja
Ja
Viewer does not support full SVG 1.1
\ No newline at end of file From 5d63e045c38a1864f7c57d193eb59070cb608b70 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 12 Oct 2023 13:14:40 +0200 Subject: [PATCH 21/60] release-3.2.1: fix lowercase typo --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 552dc522d..1eaf5ea9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,13 +21,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * 1123: Add oberallgaeu by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1124 * 1075: Create card from application by @sarahsporck in https://github.com/digitalfabrik/entitlementcard/pull/1091 -### fixed +### Fixed * 1108: Prevent auto install entitlementcard by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1109 * 998: Improve color api switch by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1110 * [fix]: add partial index for admin email by @sarahsporck in https://github.com/digitalfabrik/entitlementcard/pull/1116 * 1115: Trim search text by @f1sh1918 in https://github.com/digitalfabrik/entitlementcard/pull/1118 -### changed +### Changed * 1100: Use single quotes by @steffenkleinle in https://github.com/digitalfabrik/entitlementcard/pull/1101 * build(deps): bump activesupport from 6.1.7.3 to 6.1.7.6 in /frontend/ios by @dependabot in https://github.com/digitalfabrik/entitlementcard/pull/1099 * Extract linting rules into separate file by @sarahsporck in https://github.com/digitalfabrik/entitlementcard/pull/1104 From d8ea36c4d1f8613552149e1e04ff8da86744d9e4 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 12 Oct 2023 14:03:30 +0200 Subject: [PATCH 22/60] fix bundle id for sozialpass, fix buildConfig --- frontend/build-configs/bayern-floss/index.ts | 4 ++-- frontend/build-configs/bayern/index.ts | 3 +-- frontend/build-configs/nuernberg/index.ts | 5 ++--- frontend/build-configs/types.ts | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/build-configs/bayern-floss/index.ts b/frontend/build-configs/bayern-floss/index.ts index bb03cc374..0b9bc95c1 100644 --- a/frontend/build-configs/bayern-floss/index.ts +++ b/frontend/build-configs/bayern-floss/index.ts @@ -12,8 +12,8 @@ let bayernFloss: BuildConfigType = { ...bayern.android, ...bayernFlossCommon, applicationId: "app.ehrenamtskarte.bayern.floss", - featureFlags: { - ...bayern.android.featureFlags, + buildFeatures: { + ...bayern.android.buildFeatures, excludeLocationPlayServices: true, excludeX86: true } diff --git a/frontend/build-configs/bayern/index.ts b/frontend/build-configs/bayern/index.ts index 220e7954b..f0fbaacad 100644 --- a/frontend/build-configs/bayern/index.ts +++ b/frontend/build-configs/bayern/index.ts @@ -99,8 +99,7 @@ let bayern: BuildConfigType = { android: { ...bayernCommon, applicationId: "de.nrw.it.giz.ehrensache.bayern.android", - featureFlags: { - ...bayernCommon.featureFlags, + buildFeatures: { excludeLocationPlayServices: false, excludeX86: false, }, diff --git a/frontend/build-configs/nuernberg/index.ts b/frontend/build-configs/nuernberg/index.ts index 11a2393d7..4afd9c369 100644 --- a/frontend/build-configs/nuernberg/index.ts +++ b/frontend/build-configs/nuernberg/index.ts @@ -101,15 +101,14 @@ let nuernberg: BuildConfigType = { android: { ...nuernbergCommon, applicationId: "app.entitlementcard.nuernberg", - featureFlags: { - ...nuernbergCommon.featureFlags, + buildFeatures: { excludeLocationPlayServices: false, excludeX86: false, }, }, ios: { ...nuernbergCommon, - bundleIdentifier: "de.nrw.it.ehrensachebayern", + bundleIdentifier: "app.entitlementcard.nuernberg", }, } diff --git a/frontend/build-configs/types.ts b/frontend/build-configs/types.ts index 186e92b72..61508d56a 100644 --- a/frontend/build-configs/types.ts +++ b/frontend/build-configs/types.ts @@ -122,7 +122,7 @@ export type CommonBuildConfigType = { export type AndroidBuildConfigType = CommonBuildConfigType & { // Shows the app icon as splash screen on app start. applicationId: string - featureFlags: { + buildFeatures: { excludeLocationPlayServices: boolean excludeX86: boolean } From c1da91ee05270a8e949929e66ebf8d5c8f6cd525 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 12 Oct 2023 14:54:21 +0200 Subject: [PATCH 23/60] replace variable in build.gradle --- frontend/android/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/android/app/build.gradle b/frontend/android/app/build.gradle index 304dcbc75..cb96822dc 100644 --- a/frontend/android/app/build.gradle +++ b/frontend/android/app/build.gradle @@ -49,13 +49,13 @@ ext.setupApplication = { ApplicationProductFlavor flavor, buildConfigName -> ext.setupVariant = { variant, buildConfigName -> var buildConfig = createBuildConfig(buildConfigName) - if (buildConfig.featureFlags.excludeX86) { + if (buildConfig.buildFeatures.excludeX86) { // Verify this using: // unzip -l frontend/build/app/outputs/apk/Bayern/debug/app-Bayern-debug.apk | grep x86 // unzip -l frontend/build/app/outputs/apk/Nuernberg/debug/app-Nuernberg-debug.apk | grep x86 variant.packaging.jniLibs.excludes.add('**/lib/x86**') } - if (buildConfig.excludeLocationPlayServices) { + if (buildConfig.buildFeatures.excludeLocationPlayServices) { // Verify this by opening the APKs in Android Studio/IntelliJ. Then select all the .dex files. // Then see if there are files located at com.android.gms (Cursive files are not present, only referenced) variant.compileConfiguration.exclude group: 'com.google.android.gms', module: 'play-services-location' From c733b8a0c7782dcc5b09536aabc132c5aaaeaef1 Mon Sep 17 00:00:00 2001 From: Andreas Fischer <37902063+f1sh1918@users.noreply.github.com> Date: Tue, 17 Oct 2023 09:49:36 +0200 Subject: [PATCH 24/60] Update frontend/build-configs/bayern/localization.ts Co-authored-by: Sarah --- frontend/build-configs/bayern/localization.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/build-configs/bayern/localization.ts b/frontend/build-configs/bayern/localization.ts index 4f9b69402..c25c6c03f 100644 --- a/frontend/build-configs/bayern/localization.ts +++ b/frontend/build-configs/bayern/localization.ts @@ -25,7 +25,7 @@ const localization: LocalizationType = { applyForAnotherCardTitle: "Ehrenamtskarte beantragen oder verlängern", applyForAnotherCardDescription: "Ihre hinterlegte Karte bleibt erhalten.", activateAnotherCardTitle: "Weitere Ehrenamtskarte hinzufügen", - activateAnotherCardDescription: "Ihr hinterlegte Ehrenamtskarte bleibt erhalten. Sie können diesen manuell entfernen.", + activateAnotherCardDescription: "Ihre hinterlegte Ehrenamtskarte bleibt erhalten. Sie können diese manuell entfernen.", activationLimitDescription: "Um eine weitere Ehrenamtskarte hinzuzufügen, müssen Sie zuerst eine vorhandene Ehrenamtskarte löschen.", verifyTitle: "Eine digitale Ehrenamtskarte prüfen", verifyDescription: "Prüfen Sie die Gültigkeit einer digitalen Ehrenamtskarte.", From 71548c59e75935cd500777ca1acc161ae7705239 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 17 Oct 2023 10:25:11 +0200 Subject: [PATCH 25/60] 1084: set card limit in dev settings, show already existing dialog before asking for override --- frontend/lib/about/dev_settings_view.dart | 3 ++- .../activation_code_scanner_page.dart | 9 +++++---- frontend/lib/identification/user_code_model.dart | 8 ++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/frontend/lib/about/dev_settings_view.dart b/frontend/lib/about/dev_settings_view.dart index b984be0cc..8b2c00945 100644 --- a/frontend/lib/about/dev_settings_view.dart +++ b/frontend/lib/about/dev_settings_view.dart @@ -67,7 +67,8 @@ class DevSettingsView extends StatelessWidget { onTap: () => _setSampleCard(context), ), ListTile( - title: const Text('Set base64 card'), + title: Text('Set base64 card (Limit: ${buildConfig.maxCardAmount})'), + enabled: !hasReachedCardLimit(userCodeModel.userCodes), onTap: () => _showRawCardInput(context), ), ListTile( diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 0f952d560..90df43b6e 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -117,10 +117,7 @@ class ActivationCodeScannerPage extends StatelessWidget { ..cardVerification = (CardVerification() ..cardValid = true ..verificationTimeStamp = secondsSinceEpoch(DateTime.parse(activationResult.activationTimeStamp))); - if (isAlreadyInList(userCodesModel.userCodes, userCode)) { - await ActivationExistingCardDialog.showExistingCardDialog(context); - break; - } + userCodesModel.insertCode(userCode); if (userCodesModel.userCodes.length > 1) { moveToLastCard(); @@ -138,6 +135,10 @@ class ActivationCodeScannerPage extends StatelessWidget { if (overwriteExisting) { throw const ActivationDidNotOverwriteExisting(); } + if (isAlreadyInList(userCodesModel.userCodes, activationCode.info)) { + await ActivationExistingCardDialog.showExistingCardDialog(context); + break; + } debugPrint( 'Card Activation: Card had been activated already and was not overwritten. Waiting for user feedback.'); if (await ActivationOverwriteExistingDialog.showActivationOverwriteExistingDialog(context)) { diff --git a/frontend/lib/identification/user_code_model.dart b/frontend/lib/identification/user_code_model.dart index bf2bfe53d..8b7a7bbdc 100644 --- a/frontend/lib/identification/user_code_model.dart +++ b/frontend/lib/identification/user_code_model.dart @@ -34,7 +34,7 @@ class UserCodeModel extends ChangeNotifier { void insertCode(DynamicUserCode code) { List userCodes = _userCodes; - if (isAlreadyInList(userCodes, code)) return; + if (isAlreadyInList(userCodes, code.info)) return; userCodes.add(code); const UserCodeStore().store(userCodes); _userCodes = userCodes; @@ -43,7 +43,7 @@ class UserCodeModel extends ChangeNotifier { void updateCode(DynamicUserCode code) { List userCodes = _userCodes; - if (isAlreadyInList(userCodes, code)) { + if (isAlreadyInList(userCodes, code.info)) { userCodes = updateUserCode(userCodes, code); const UserCodeStore().store(userCodes); _userCodes = userCodes; @@ -71,8 +71,8 @@ class UserCodeModel extends ChangeNotifier { } } -bool isAlreadyInList(List userCodes, DynamicUserCode code) { - return userCodes.map((userCode) => userCode.info).contains(code.info); +bool isAlreadyInList(List userCodes, CardInfo info) { + return userCodes.map((userCode) => userCode.info).contains(info); } bool hasReachedCardLimit(List userCodes) { From aed696f7eec1a4e84d2281a0d79b670ef6ff5a47 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Thu, 24 Aug 2023 15:00:12 +0200 Subject: [PATCH 26/60] 904: Start translating --- frontend/assets/bayern/translations/de.json | 58 ++++++++++++++ frontend/assets/bayern/translations/en.json | 10 +++ frontend/lib/about/about_page.dart | 27 ++++--- frontend/lib/about/license_page.dart | 7 +- frontend/lib/app.dart | 11 ++- frontend/lib/category_assets.dart | 76 +++++++++---------- frontend/lib/home/home_page.dart | 12 +-- .../activation_workflow/activate_code.dart | 2 +- .../activation_overwrite_existing_dialog.dart | 4 +- .../card_detail_view/card_detail_view.dart | 14 ++-- .../connection_failed_dialog.dart | 4 +- .../identification/id_card/card_content.dart | 14 ++-- .../identification/identification_page.dart | 4 +- frontend/lib/identification/info_dialog.dart | 3 +- .../qr_code_camera_permission_dialog.dart | 4 +- .../qr_code_scanner/qr_code_scanner.dart | 4 +- .../qr_code_scanner/qr_overlay_shape.dart | 2 +- .../qr_parsing_error_dialog.dart | 4 +- .../positive_verification_result_dialog.dart | 2 +- .../dialogs/verification_info_dialog.dart | 2 +- .../query_server_verification.dart | 2 +- frontend/lib/intro_slides/intro_screen.dart | 7 +- frontend/lib/location/determine_position.dart | 8 +- frontend/lib/location/dialogs.dart | 2 +- frontend/lib/location/location_ffi.dart | 4 +- frontend/lib/map/location_button.dart | 7 +- frontend/lib/map/map/attribution_dialog.dart | 10 +-- frontend/lib/map/map/map.dart | 2 +- frontend/lib/search/results_loader.dart | 4 +- .../store_widgets/detail/detail_app_bar.dart | 2 +- .../store_widgets/detail/detail_content.dart | 6 +- frontend/lib/util/i18n.dart | 4 + frontend/pubspec.lock | 24 ++++++ frontend/pubspec.yaml | 3 + 34 files changed, 234 insertions(+), 115 deletions(-) create mode 100644 frontend/assets/bayern/translations/de.json create mode 100644 frontend/assets/bayern/translations/en.json create mode 100644 frontend/lib/util/i18n.dart diff --git a/frontend/assets/bayern/translations/de.json b/frontend/assets/bayern/translations/de.json new file mode 100644 index 000000000..f7b1790e8 --- /dev/null +++ b/frontend/assets/bayern/translations/de.json @@ -0,0 +1,58 @@ +{ + "publisher": "Herausgeber", + "moreInformation": "Mehr Informationen", + "licenses": "Lizenzen", + "license": "Lizenz", + "numberLicenses0": "{number} Lizenzen", + "numberLicenses1": "{number} Lizenz", + "numberLicenses2": "{number} Lizenzen", + "privacyDeclaration": "Datenschutzerklärung", + "disclaimer": "Haftung, Haftungsausschluss und Impressum", + "dependencies": "Software-Bibliotheken", + "sourceCode": "Quellcode der App", + "developmentOptions": "Entwickleroptionen", + "locationAccessDeactivated": "Die Standortfreigabe ist deaktiviert.", + "checkSettings": "Prüfe Einstellungen...", + "connectionFailed": "Keine Verbindung möglich", + "ok": "OK", + "done": "Fertig", + "next": "Weiter", + "previous": "Zurück", + "settings": "Einstellungen", + "card": "Karte", + "search": "Suche", + "identification": "Ausweisen", + "about": "Über", + "categories": { + "mobilityLong": "Auto/Zweirad", + "mobility": "Mobilität", + "media": "Multimedia", + "healthLong": "Gesundheit/Sport/Wellness", + "health": "Gesundheit", + "cultureLong": "Bildung/Kultur/Unterhaltung", + "culture": "Kultur", + "servicesLong": "Dienstleistungen/Finanzen", + "services": "Dienstleistung", + "fashionLong": "Mode/Beauty", + "fashion": "Mode", + "livingLong": "Wohnen/Haus/Garten", + "living": "Einrichtung", + "leisureLong": "Freizeit/Reise/Unterkünfte", + "leisure": "Freizeit", + "foodLong": "Essen/Trinken/Gastronomie", + "food": "Gastronomie", + "other": "Anderes", + "lunchTables": "Mittagstische", + "clothingLong": "Kleidung/Gebrauchtes", + "clothing": "Kleidung", + "cultureLongAlternative": "Kultur/Museen/Freizeit", + "education": "Bildung", + "moviesLong": "Kinos/Theater/Konzerte", + "movies": "Schauspiel", + "pharmaciesLong": "Apotheken/Gesundheit", + "pharmacies": "Apotheken", + "digitalParticipation": "Digitale Teilhabe", + "sportsLong": "Sport/Bewegung/Tanz", + "sports": "Sport" + } +} diff --git a/frontend/assets/bayern/translations/en.json b/frontend/assets/bayern/translations/en.json new file mode 100644 index 000000000..504d7da7f --- /dev/null +++ b/frontend/assets/bayern/translations/en.json @@ -0,0 +1,10 @@ +{ +"publisher": "Publisher", +"moreInformation": "More Information", +"licenses-0": "Licenses", +"licenses-1": "License", +"licenses-2": "Licenses", +"number-licenses-0": "{number} Licenses", +"number-licenses-1": "{number} License", +"number-licenses-2": "{number} Licenses" +} diff --git a/frontend/lib/about/about_page.dart b/frontend/lib/about/about_page.dart index 1b14bdd24..52169ad4e 100644 --- a/frontend/lib/about/about_page.dart +++ b/frontend/lib/about/about_page.dart @@ -7,9 +7,12 @@ import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/configuration/configuration.dart'; import 'package:ehrenamtskarte/routing.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import '../util/i18n.dart'; + class AboutPage extends StatefulWidget { final countToEnableSwitch = 10; const AboutPage({super.key}); @@ -67,19 +70,21 @@ class AboutPageState extends State { child: Column( children: [ Center( - child: Text('Herausgeber', style: Theme.of(context).textTheme.titleSmall), + child: I18nText('publisher', child: Text('', style: Theme.of(context).textTheme.titleSmall)), ), Padding( padding: const EdgeInsets.only(left: 10, right: 10, top: 16, bottom: 16), child: Text(buildConfig.publisherAddress, style: Theme.of(context).textTheme.bodyLarge), ), - Text( - 'Mehr Informationen', + I18nText( + 'moreInformation', + child: Text('', style: Theme.of(context) .textTheme .bodyMedium ?.merge(TextStyle(color: Theme.of(context).colorScheme.secondary)), ), + ), ], ), ), @@ -87,7 +92,7 @@ class AboutPageState extends State { Navigator.push( context, AppRoute( - builder: (context) => ContentPage(title: 'Herausgeber', children: getPublisherText(context)), + builder: (context) => ContentPage(title: t(context, 'publisher'), children: getPublisherText(context)), ), ); }, @@ -97,20 +102,20 @@ class AboutPageState extends State { thickness: 1, ), const SizedBox(height: 20), - ContentTile(icon: Icons.copyright, title: 'Lizenz', children: getCopyrightText(context)), + ContentTile(icon: Icons.copyright, title: t(context, 'license'), children: getCopyrightText(context)), ListTile( leading: const Icon(Icons.privacy_tip_outlined), - title: const Text('Datenschutzerklärung'), + title: I18nText('privacyDeclaration'), onTap: () => launchUrlString(buildConfig.dataPrivacyPolicyUrl, mode: LaunchMode.externalApplication), ), ContentTile( icon: Icons.info_outline, - title: 'Haftung, Haftungsausschluss und Impressum', + title: t(context, 'disclaimer'), children: getDisclaimerText(context), ), ListTile( leading: const Icon(Icons.book_outlined), - title: const Text('Software-Bibliotheken'), + title: I18nText('dependencies'), onTap: () { Navigator.push( context, @@ -122,7 +127,7 @@ class AboutPageState extends State { ), ListTile( leading: const Icon(Icons.code_outlined), - title: const Text('Quellcode der App'), + title: I18nText('sourceCode'), onTap: () { launchUrlString( 'https://github.com/digitalfabrik/entitlementcard', @@ -133,11 +138,11 @@ class AboutPageState extends State { if (config.showDevSettings) ListTile( leading: const Icon(Icons.build), - title: const Text('Entwickleroptionen'), + title: I18nText('developmentOptions'), onTap: () => showDialog( context: context, builder: (context) => - const SimpleDialog(title: Text('Entwickleroptionen'), children: [DevSettingsView()]), + SimpleDialog(title: I18nText('developmentOptions'), children: [DevSettingsView()]), ), ) ]; diff --git a/frontend/lib/about/license_page.dart b/frontend/lib/about/license_page.dart index bc0837ca7..1a004eaf0 100644 --- a/frontend/lib/about/license_page.dart +++ b/frontend/lib/about/license_page.dart @@ -5,6 +5,9 @@ import 'package:ehrenamtskarte/widgets/error_message.dart'; import 'package:ehrenamtskarte/widgets/top_loading_spinner.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; + +import '../util/i18n.dart'; class CustomLicenseEntry { final String packageName; @@ -51,7 +54,7 @@ class CustomLicensePage extends StatelessWidget { return CustomScrollView( slivers: [ - const CustomSliverAppBar(title: 'Lizenzen'), + CustomSliverAppBar(title: t(context, 'licenses')), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { @@ -59,7 +62,7 @@ class CustomLicensePage extends StatelessWidget { final paragraphs = license.licenseParagraphs; return ListTile( title: Text(license.packageName), - subtitle: Text('${paragraphs.length} Lizenzen'), + subtitle: I18nPlural('numberLicenses', paragraphs.length), onTap: () { Navigator.push( context, diff --git a/frontend/lib/app.dart b/frontend/lib/app.dart index fb60773e3..64f324888 100644 --- a/frontend/lib/app.dart +++ b/frontend/lib/app.dart @@ -8,6 +8,7 @@ import 'package:ehrenamtskarte/intro_slides/intro_screen.dart'; import 'package:ehrenamtskarte/themes.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; @@ -66,13 +67,17 @@ class App extends StatelessWidget { darkTheme: darkTheme, themeMode: ThemeMode.system, debugShowCheckedModeBanner: false, - localizationsDelegates: const [ + localizationsDelegates: [ + FlutterI18nDelegate( + translationLoader: FileTranslationLoader(useCountryCode: false, fallbackFile: 'en', basePath: 'assets/bayern/translations'), + missingTranslationHandler: (key, locale) => + print('--- Missing Key: $key, languageCode: ${locale?.languageCode}'), + ), GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: const [Locale('de')], - locale: const Locale('de'), + supportedLocales: const [Locale('de'), Locale('en')], initialRoute: initialRoute, routes: routes, ), diff --git a/frontend/lib/category_assets.dart b/frontend/lib/category_assets.dart index c1a981110..455140581 100644 --- a/frontend/lib/category_assets.dart +++ b/frontend/lib/category_assets.dart @@ -33,152 +33,152 @@ class CategoryAsset { const List categoryAssets = [ CategoryAsset( id: 0, - name: 'Auto/Zweirad', - shortName: 'Mobilität', + name: 'categories.mobilityLong', + shortName: 'categories.mobility', icon: 'assets/category_icons/0.svg', detailIcon: 'assets/detail_headers/0_auto.svg', color: Color(0xffE89600), ), CategoryAsset( id: 1, - name: 'Multimedia', - shortName: 'Multimedia', + name: 'categories.media', + shortName: 'categories.media', icon: 'assets/category_icons/1.svg', detailIcon: 'assets/detail_headers/1_multimedia.svg', color: Color(0xFFFA0000), ), CategoryAsset( id: 2, - name: 'Gesundheit/Sport/Wellness', - shortName: 'Gesundheit', + name: 'categories.healthLong', + shortName: 'categories.health', icon: 'assets/category_icons/2.svg', detailIcon: 'assets/detail_headers/2_sport.svg', color: Color(0xFFE500D3), ), CategoryAsset( id: 3, - name: 'Bildung/Kultur/Unterhaltung', - shortName: 'Kultur', + name: 'categories.cultureLong', + shortName: 'categories.culture', icon: 'assets/category_icons/3.svg', detailIcon: 'assets/detail_headers/3_kultur.svg', color: Color(0xFF7500EB), ), CategoryAsset( id: 4, - name: 'Dienstleistungen/Finanzen', - shortName: 'Dienstleistung', + name: 'categories.servicesLong', + shortName: 'categories.services', icon: 'assets/category_icons/4.svg', detailIcon: 'assets/detail_headers/4_finanzen.svg', color: Color(0xFF515151), ), CategoryAsset( id: 5, - name: 'Mode/Beauty', - shortName: 'Mode', + name: 'categories.fashionLong', + shortName: 'categories.fashion', icon: 'assets/category_icons/5.svg', detailIcon: 'assets/detail_headers/5_mode.svg', color: Color(0xFF6EBE00), ), CategoryAsset( id: 6, - name: 'Wohnen/Haus/Garten', - shortName: 'Einrichtung', + name: 'categories.livingLong', + shortName: 'categories.living', icon: 'assets/category_icons/6.svg', detailIcon: 'assets/detail_headers/6_haus.svg', color: Color(0xFF00D0C7), ), CategoryAsset( id: 7, - name: 'Freizeit/Reise/Unterkünfte', - shortName: 'Freizeit', + name: 'categories.leisureLong', + shortName: 'categories.leisure', icon: 'assets/category_icons/7.svg', detailIcon: 'assets/detail_headers/7_freizeit.svg', color: Color(0xFF007CE8), ), CategoryAsset( id: 8, - name: 'Essen/Trinken/Gastronomie', - shortName: 'Gastronomie', + name: 'categories.foodLong', + shortName: 'categories.food', icon: 'assets/category_icons/8.svg', detailIcon: 'assets/detail_headers/8_essen.svg', color: Color(0xFF197489), ), CategoryAsset( id: 9, - name: 'Anderes', - shortName: 'Anderes', + name: 'categories.other', + shortName: 'categories.other', icon: 'assets/category_icons/9.svg', detailIcon: null, color: Color(0xFFc51162), ), CategoryAsset( id: 10, - name: 'Mittagstische', - shortName: 'Mittagstische', + name: 'categories.lunchTables', + shortName: 'categories.lunchTables', icon: 'assets/category_icons/10.svg', detailIcon: 'assets/detail_headers/10_mittagstische.svg', color: Color(0xFF197489), ), CategoryAsset( id: 11, - name: 'Kleidung/Gebrauchtes', - shortName: 'Kleidung', + name: 'categories.clothingLong', + shortName: 'categories.clothing', icon: 'assets/category_icons/11.svg', detailIcon: 'assets/detail_headers/11_kleidung.svg', color: Color(0xFF6EBE00), ), CategoryAsset( id: 12, - name: 'Kultur/Museen/Freizeit', - shortName: 'Kultur', + name: 'categories.cultureLongAlternative', + shortName: 'categories.culture', icon: 'assets/category_icons/12.svg', detailIcon: 'assets/detail_headers/12_kultur.svg', color: Color(0xFF7500EB), ), CategoryAsset( id: 13, - name: 'Bildung', - shortName: 'Bildung', + name: 'categories.education', + shortName: 'categories.education', icon: 'assets/category_icons/13.svg', detailIcon: null, color: Color(0xFFd100cc), ), CategoryAsset( id: 14, - name: 'Kinos/Theater/Konzerte', - shortName: 'Schauspiel', + name: 'categories.moviesLong', + shortName: 'categories.movies', icon: 'assets/category_icons/14.svg', detailIcon: null, color: Color(0xFFc51162), ), CategoryAsset( id: 15, - name: 'Apotheken/Gesundheit', - shortName: 'Apotheken', + name: 'categories.pharmaciesLong', + shortName: 'categories.pharmacies', icon: 'assets/category_icons/15.svg', detailIcon: null, color: Color(0xFF007be0), ), CategoryAsset( id: 16, - name: 'Digitale Teilhabe', - shortName: 'Digitale Teilhabe', + name: 'categories.digitalParticipation', + shortName: 'categories.digitalParticipation', icon: 'assets/category_icons/16.svg', detailIcon: 'assets/detail_headers/16_teilhabe.svg', color: Color(0xFFFA0000), ), CategoryAsset( id: 17, - name: 'Sport/Bewegung/Tanz', - shortName: 'Sport', + name: 'categories.sports', + shortName: 'categories.sports', icon: 'assets/category_icons/17.svg', detailIcon: 'assets/detail_headers/17_sport.svg', color: Color(0xFF00ccc6), ), CategoryAsset( id: 18, - name: 'Mobilität', - shortName: 'Mobilität', + name: 'categories.mobility', + shortName: 'categories.mobility', icon: 'assets/category_icons/18.svg', detailIcon: 'assets/detail_headers/18_mobilitaet.svg', color: Color(0xffE89600), diff --git a/frontend/lib/home/home_page.dart b/frontend/lib/home/home_page.dart index 7361cc199..a17ae1576 100644 --- a/frontend/lib/home/home_page.dart +++ b/frontend/lib/home/home_page.dart @@ -11,6 +11,8 @@ import 'package:ehrenamtskarte/search/search_page.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../util/i18n.dart'; + const mapTabIndex = 0; class HomePage extends StatefulWidget { @@ -37,23 +39,23 @@ class HomePageState extends State { selectAcceptingStore: (id) => setState(() => selectedAcceptingStoreId = id), ), Icons.map_outlined, - 'Karte', + t(context, 'card'), GlobalKey(debugLabel: 'Map tab key'), ), AppFlow( const SearchPage(), Icons.search_outlined, - 'Suche', + t(context, 'search'), GlobalKey(debugLabel: 'Search tab key'), ), if (buildConfig.featureFlags.verification) AppFlow( - const IdentificationPage(title: 'Ausweisen'), + IdentificationPage(), Icons.remove_red_eye_outlined, - 'Ausweisen', + t(context, 'identification'), GlobalKey(debugLabel: 'Auth tab key'), ), - AppFlow(const AboutPage(), Icons.info_outline, 'Über', GlobalKey(debugLabel: 'About tab key')), + AppFlow(const AboutPage(), Icons.info_outline, t(context, 'about'), GlobalKey(debugLabel: 'About tab key')), ]; } diff --git a/frontend/lib/identification/activation_workflow/activate_code.dart b/frontend/lib/identification/activation_workflow/activate_code.dart index 1b791b5a2..88db4aaff 100644 --- a/frontend/lib/identification/activation_workflow/activate_code.dart +++ b/frontend/lib/identification/activation_workflow/activate_code.dart @@ -49,6 +49,6 @@ class ServerCardActivationException implements Exception { @override String toString() { - return 'ServerCardActivationException{cause: $cause}'; + return "ServerCardActivationException{cause: $cause}"; } } diff --git a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart index ca322644e..86256b457 100644 --- a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart +++ b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart @@ -18,13 +18,13 @@ class ActivationOverwriteExistingDialog extends StatelessWidget { ), actions: [ TextButton( - child: const Text('Abbrechen'), + child: const Text("Abbrechen"), onPressed: () { Navigator.of(context).pop(false); }, ), TextButton( - child: const Text('Aktivieren'), + child: const Text("Aktivieren"), onPressed: () { Navigator.of(context).pop(true); }, diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index 7724e34c2..23bee3409 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -42,7 +42,7 @@ class _CardDetailViewState extends State { // in order to detect if one of the following events happened: // - the card was activated on another device // - the card was revoked - // - the card expired (on backend's system time) + // - the card expired (on backend"s system time) _selfVerifyCard(); initiatedSelfVerification = true; } @@ -144,7 +144,7 @@ enum CardStatus { notVerifiedLately, // The time of the device was out of sync with the server. timeOutOfSync, - // The validity period didn't start yet according to the clock of the local device + // The validity period didn"t start yet according to the clock of the local device notYetValid, // The card was verified lately by the server and it responded that the card is invalid. invalid, @@ -192,7 +192,7 @@ class QrCodeAndStatus extends StatelessWidget { ], CardStatus.notVerifiedLately => [ _PaddedText( - 'Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Verbindung mit dem Internet besteht und prüfen Sie erneut.'), + "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Verbindung mit dem Internet besteht und prüfen Sie erneut."), Flexible( child: TextButton.icon( icon: const Icon(Icons.refresh), @@ -203,7 +203,7 @@ class QrCodeAndStatus extends StatelessWidget { ], CardStatus.timeOutOfSync => [ _PaddedText( - 'Die Uhrzeit Ihres Geräts scheint nicht zu stimmen. Bitte synchronisieren Sie die Uhrzeit in den Systemeinstellungen.'), + "Die Uhrzeit Ihres Geräts scheint nicht zu stimmen. Bitte synchronisieren Sie die Uhrzeit in den Systemeinstellungen."), Flexible( child: TextButton.icon( icon: const Icon(Icons.refresh), @@ -213,14 +213,14 @@ class QrCodeAndStatus extends StatelessWidget { ], CardStatus.invalid => [ _PaddedText( - 'Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.') + "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.") ], CardStatus.valid => [ - _PaddedText('Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:'), + _PaddedText("Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:"), Flexible(child: VerificationCodeView(userCode: userCode)) ], CardStatus.notYetValid => [ - _PaddedText('Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen.'), + _PaddedText("Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen."), ] }, Container( diff --git a/frontend/lib/identification/connection_failed_dialog.dart b/frontend/lib/identification/connection_failed_dialog.dart index a53410e26..b6c5c6689 100644 --- a/frontend/lib/identification/connection_failed_dialog.dart +++ b/frontend/lib/identification/connection_failed_dialog.dart @@ -1,6 +1,8 @@ import 'package:ehrenamtskarte/identification/info_dialog.dart'; import 'package:flutter/material.dart'; +import '../util/i18n.dart'; + class ConnectionFailedDialog extends StatelessWidget { final String reason; @@ -9,7 +11,7 @@ class ConnectionFailedDialog extends StatelessWidget { @override Widget build(BuildContext context) { return InfoDialog( - title: 'Keine Verbindung möglich', + title: t(context, 'connectionFailed'), icon: Icons.signal_cellular_connected_no_internet_4_bar, iconColor: Theme.of(context).colorScheme.onBackground, child: Text(reason), diff --git a/frontend/lib/identification/id_card/card_content.dart b/frontend/lib/identification/id_card/card_content.dart index 836ce65cb..42d7f9f93 100644 --- a/frontend/lib/identification/id_card/card_content.dart +++ b/frontend/lib/identification/id_card/card_content.dart @@ -49,14 +49,14 @@ class CardContent extends StatelessWidget { String get _formattedExpirationDate { final expirationDay = cardInfo.hasExpirationDay() ? cardInfo.expirationDay : null; return expirationDay != null - ? DateFormat('dd.MM.yyyy').format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: expirationDay))) - : 'unbegrenzt'; + ? DateFormat("dd.MM.yyyy").format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: expirationDay))) + : "unbegrenzt"; } String? get _formattedBirthday { final birthday = cardInfo.extensions.hasExtensionBirthday() ? cardInfo.extensions.extensionBirthday.birthday : null; return birthday != null - ? DateFormat('dd.MM.yyyy').format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: birthday))) + ? DateFormat("dd.MM.yyyy").format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: birthday))) : null; } @@ -69,12 +69,12 @@ class CardContent extends StatelessWidget { String? get _formattedStartDate { final startDay = cardInfo.extensions.hasExtensionStartDay() ? cardInfo.extensions.extensionStartDay.startDay : null; return startDay != null - ? DateFormat('dd.MM.yyyy').format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: startDay))) + ? DateFormat("dd.MM.yyyy").format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: startDay))) : null; } String _getCardValidityDate(String? startDate, String expirationDate) { - return startDate != null ? 'Gültig: $startDate bis $expirationDate' : 'Gültig bis: $expirationDate'; + return startDate != null ? "Gültig: $startDate bis $expirationDate" : "Gültig bis: $expirationDate"; } @override @@ -90,7 +90,7 @@ class CardContent extends StatelessWidget { final scaleFactor = constraints.maxWidth / 300; final currentRegion = region; final headerLeftTitle = buildConfig.cardBranding.headerTitleLeft.isEmpty && currentRegion != null - ? '${currentRegion.prefix} ${currentRegion.name}' + ? "${currentRegion.prefix} ${currentRegion.name}" : buildConfig.cardBranding.headerTitleLeft; return Column( mainAxisAlignment: MainAxisAlignment.start, @@ -163,7 +163,7 @@ class CardContent extends StatelessWidget { AspectRatio( aspectRatio: 6 / 1.5, child: Align( - alignment: buildConfig.cardBranding.bodyLogoPosition == 'center' + alignment: buildConfig.cardBranding.bodyLogoPosition == "center" ? Alignment.center : Alignment.centerRight, child: Image( diff --git a/frontend/lib/identification/identification_page.dart b/frontend/lib/identification/identification_page.dart index 6dc0c0d66..3982deb4d 100644 --- a/frontend/lib/identification/identification_page.dart +++ b/frontend/lib/identification/identification_page.dart @@ -13,9 +13,7 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; class IdentificationPage extends StatelessWidget { - final String title; - - const IdentificationPage({super.key, required this.title}); + const IdentificationPage({super.key}); @override Widget build(BuildContext context) { diff --git a/frontend/lib/identification/info_dialog.dart b/frontend/lib/identification/info_dialog.dart index d4f9d98af..6c03f652a 100644 --- a/frontend/lib/identification/info_dialog.dart +++ b/frontend/lib/identification/info_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_i18n/widgets/I18nText.dart'; class InfoDialog extends StatelessWidget { final Widget child; @@ -23,7 +24,7 @@ class InfoDialog extends StatelessWidget { title: Text(title, style: theme.textTheme.headlineSmall), ), content: child, - actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))], + actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: I18nText('ok'))], ); } } diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart index 94db41714..f1177a981 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart @@ -19,13 +19,13 @@ class QrCodeCameraPermissionDialog extends StatelessWidget { ), actions: [ TextButton( - child: const Text('Abbrechen'), + child: const Text("Abbrechen"), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( - child: const Text('Einstellungen öffnen'), + child: const Text("Einstellungen öffnen"), onPressed: () { openAppSettings(); }, diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart index dfd893137..d07dcb2d1 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart @@ -24,7 +24,7 @@ class _QRViewState extends State { formats: [BarcodeFormat.qrCode], returnImage: false, ); - final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + final GlobalKey qrKey = GlobalKey(debugLabel: "QR"); // Determines whether a code is currently processed by the onCodeScanned callback // During this time, we do not re-trigger the callback. @@ -75,7 +75,7 @@ class _QRViewState extends State { children: [ Container( margin: const EdgeInsets.all(8), - child: const Text('Halten Sie die Kamera auf den QR Code.'), + child: const Text("Halten Sie die Kamera auf den QR Code."), ), QrCodeScannerControls(controller: controller) ], diff --git a/frontend/lib/identification/qr_code_scanner/qr_overlay_shape.dart b/frontend/lib/identification/qr_code_scanner/qr_overlay_shape.dart index 48c44dfda..ceefa2c13 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_overlay_shape.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_overlay_shape.dart @@ -25,7 +25,7 @@ class QrScannerOverlayShape extends ShapeBorder { assert( (cutOutWidth == null && cutOutHeight == null) || (cutOutSize == null && cutOutWidth != null && cutOutHeight != null), - 'Use only cutOutWidth and cutOutHeight or only cutOutSize', + "Use only cutOutWidth and cutOutHeight or only cutOutSize", ); } diff --git a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart index d3e9fdc1a..2118b3672 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart @@ -8,7 +8,7 @@ class QrParsingErrorDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text('Fehler beim Lesen des Codes'), + title: const Text("Fehler beim Lesen des Codes"), content: SingleChildScrollView( child: ListBody( children: [ @@ -18,7 +18,7 @@ class QrParsingErrorDialog extends StatelessWidget { ), actions: [ TextButton( - child: const Text('Ok'), + child: const Text("Ok"), onPressed: () { Navigator.of(context).pop(); }, diff --git a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart index 01b709dc0..3cd79fe39 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart @@ -75,7 +75,7 @@ class PositiveVerificationResultDialogState extends State onDonePress(context), - renderDoneBtn: const Text('Fertig'), - renderNextBtn: const Text('Weiter'), - renderPrevBtn: const Text('Zurück'), + renderDoneBtn: I18nText('done'), + renderNextBtn: I18nText('next'), + renderPrevBtn: I18nText('previous'), doneButtonStyle: Theme.of(context).textButtonTheme.style, indicatorConfig: IndicatorConfig( colorActiveIndicator: theme.colorScheme.primary, diff --git a/frontend/lib/location/determine_position.dart b/frontend/lib/location/determine_position.dart index 1ffe1190e..2255216f1 100644 --- a/frontend/lib/location/determine_position.dart +++ b/frontend/lib/location/determine_position.dart @@ -11,16 +11,16 @@ enum LocationStatus { /// request for permission another time. denied, - /// Permission to access the device's location is permenantly denied. When + /// Permission to access the device"s location is permenantly denied. When /// requestiong permissions the permission dialog will not been shown until /// the user updates the permission in the App settings. deniedForever, - /// Permission to access the device's location is allowed only while + /// Permission to access the device"s location is allowed only while /// the App is in use. whileInUse, - /// Permission to access the device's location is allowed even when the + /// Permission to access the device"s location is allowed even when the /// App is running in the background. always, @@ -140,7 +140,7 @@ Future checkAndRequestLocationPermission( if (requestResult == LocationPermission.denied) { // Permissions are denied, next time you could try // requesting permissions again (this is also where - // Android's shouldShowRequestPermissionRationale + // Android"s shouldShowRequestPermissionRationale // returned true. According to Android guidelines // your App should show an explanatory UI now. diff --git a/frontend/lib/location/dialogs.dart b/frontend/lib/location/dialogs.dart index 1abb34165..3d4ac88a7 100644 --- a/frontend/lib/location/dialogs.dart +++ b/frontend/lib/location/dialogs.dart @@ -28,7 +28,7 @@ class RationaleDialog extends StatelessWidget { content: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: [Text(_rationale), const Text('Soll nocheinmal nach der Berechtigung gefragt werden?')], + children: [Text(_rationale), const Text("Soll nocheinmal nach der Berechtigung gefragt werden?")], ), actions: [ TextButton(child: const Text('Berechtigung erteilen'), onPressed: () => Navigator.of(context).pop(true)), diff --git a/frontend/lib/location/location_ffi.dart b/frontend/lib/location/location_ffi.dart index 622fbbe83..7ef4c0afe 100644 --- a/frontend/lib/location/location_ffi.dart +++ b/frontend/lib/location/location_ffi.dart @@ -1,12 +1,12 @@ import 'package:flutter/services.dart'; -const platform = MethodChannel('app.ehrenamtskarte/location'); +const platform = MethodChannel("app.ehrenamtskarte/location"); /// Checks whether location services are enabled /// without using Google Play Services Future isNonGoogleLocationServiceEnabled() async { try { - final bool result = await platform.invokeMethod('isLocationServiceEnabled') as bool; + final bool result = await platform.invokeMethod("isLocationServiceEnabled") as bool; return result; } on PlatformException catch (e) { throw "Failed to get state of location services: '${e.message}'."; diff --git a/frontend/lib/map/location_button.dart b/frontend/lib/map/location_button.dart index 31efec19b..782482d7b 100644 --- a/frontend/lib/map/location_button.dart +++ b/frontend/lib/map/location_button.dart @@ -2,8 +2,11 @@ import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/location/determine_position.dart'; import 'package:ehrenamtskarte/widgets/small_button_spinner.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:provider/provider.dart'; +import '../util/i18n.dart'; + class LocationButton extends StatefulWidget { final Future Function(RequestedPosition) bringCameraToUser; @@ -52,9 +55,9 @@ class _LocationButtonState extends State { messengerState.showSnackBar( SnackBar( behavior: SnackBarBehavior.floating, - content: const Text('Die Standortfreigabe ist deaktiviert.'), + content: I18nText('locationAccessDeactivated'), action: SnackBarAction( - label: 'Einstellungen', + label: t(context, 'settings'), onPressed: () async { await openSettingsToGrantPermissions(context); }, diff --git a/frontend/lib/map/map/attribution_dialog.dart b/frontend/lib/map/map/attribution_dialog.dart index ca26e1921..749746249 100644 --- a/frontend/lib/map/map/attribution_dialog.dart +++ b/frontend/lib/map/map/attribution_dialog.dart @@ -9,12 +9,12 @@ class AttributionDialog extends StatelessWidget { Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.primary; return SimpleDialog( - title: const Text('Kartendaten'), + title: const Text("Kartendaten"), children: [ AttributionDialogItem( icon: Icons.copyright, color: color, - text: 'OpenStreetMap Mitwirkende', + text: "OpenStreetMap Mitwirkende", onPressed: () { launchUrlString('https://www.openstreetmap.org/copyright', mode: LaunchMode.externalApplication); }, @@ -22,7 +22,7 @@ class AttributionDialog extends StatelessWidget { AttributionDialogItem( icon: Icons.copyright, color: color, - text: 'OpenMapTiles', + text: "OpenMapTiles", onPressed: () { launchUrlString('https://openmaptiles.org/', mode: LaunchMode.externalApplication); }, @@ -30,7 +30,7 @@ class AttributionDialog extends StatelessWidget { AttributionDialogItem( icon: Icons.copyright, color: color, - text: 'Natural Earth', + text: "Natural Earth", onPressed: () { launchUrlString('https://naturalearthdata.com/', mode: LaunchMode.externalApplication); }, @@ -38,7 +38,7 @@ class AttributionDialog extends StatelessWidget { AttributionDialogItem( icon: Icons.copyright, color: color, - text: 'LBE Bayern', + text: "LBE Bayern", onPressed: () { launchUrlString('https://www.lbe.bayern.de/', mode: LaunchMode.externalApplication); }, diff --git a/frontend/lib/map/map/map.dart b/frontend/lib/map/map/map.dart index fdf9e6c61..2653a9bf5 100644 --- a/frontend/lib/map/map/map.dart +++ b/frontend/lib/map/map/map.dart @@ -96,7 +96,7 @@ class _MapContainerState extends State implements MapController { color: mapboxColor, iconSize: 20, icon: const Icon(Icons.info_outline), - tooltip: 'Zeige Infos über das Urheberrecht der Kartendaten', + tooltip: "Zeige Infos über das Urheberrecht der Kartendaten", onPressed: () { showDialog( context: context, diff --git a/frontend/lib/search/results_loader.dart b/frontend/lib/search/results_loader.dart index b3f0b22d5..ec8962a89 100644 --- a/frontend/lib/search/results_loader.dart +++ b/frontend/lib/search/results_loader.dart @@ -74,7 +74,7 @@ class ResultsLoaderState extends State { if (widget != oldWidget) { // Params are outdated. - // If we're still at the first key, we must manually retrigger fetching. + // If we"re still at the first key, we must manually retrigger fetching. if (pageKey == _pagingController.firstPageKey) { return await _fetchPage(pageKey); } @@ -96,7 +96,7 @@ class ResultsLoaderState extends State { } on Exception catch (error) { if (widget != oldWidget) { // Params are outdated. - // If we're still at the first key, we must manually retrigger fetching. + // If we"re still at the first key, we must manually retrigger fetching. if (pageKey == _pagingController.firstPageKey) { return _fetchPage(pageKey); } diff --git a/frontend/lib/store_widgets/detail/detail_app_bar.dart b/frontend/lib/store_widgets/detail/detail_app_bar.dart index c97af0c40..58a32ab01 100644 --- a/frontend/lib/store_widgets/detail/detail_app_bar.dart +++ b/frontend/lib/store_widgets/detail/detail_app_bar.dart @@ -23,7 +23,7 @@ class DetailAppBarHeaderImage extends StatelessWidget { return SvgPicture.asset( currentDetailIcon, width: double.infinity, - semanticsLabel: 'Header', + semanticsLabel: "Header", alignment: Alignment.bottomRight, ); } diff --git a/frontend/lib/store_widgets/detail/detail_content.dart b/frontend/lib/store_widgets/detail/detail_content.dart index 872d810a6..4694bb30d 100644 --- a/frontend/lib/store_widgets/detail/detail_content.dart +++ b/frontend/lib/store_widgets/detail/detail_content.dart @@ -119,11 +119,11 @@ class DetailContent extends StatelessWidget { Future _launchMap(String query) async { if (Platform.isAndroid) { - await launchUrl(Uri(scheme: 'geo', host: '0,0', queryParameters: {'q': query})); + await launchUrl(Uri(scheme: "geo", host: "0,0", queryParameters: {"q": query})); } else if (Platform.isIOS) { - await launchUrl(Uri.https('maps.apple.com', '/', {'q': query})); + await launchUrl(Uri.https("maps.apple.com", "/", {"q": query})); } else { - await launchUrl(Uri.https('www.google.com', '/maps/search/', {'api': '1', 'query': query})); + await launchUrl(Uri.https("www.google.com", "/maps/search/", {"api": "1", "query": query})); } } diff --git a/frontend/lib/util/i18n.dart b/frontend/lib/util/i18n.dart new file mode 100644 index 000000000..15833853b --- /dev/null +++ b/frontend/lib/util/i18n.dart @@ -0,0 +1,4 @@ +import 'package:flutter_i18n/flutter_i18n.dart'; + +final t = FlutterI18n.translate; +final plural = FlutterI18n.plural; diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 2510f9b47..d4ce06606 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -308,6 +308,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.6" + flutter_i18n: + dependency: "direct main" + description: + name: flutter_i18n + sha256: b71fe887697686368c93e4d2257ecdb3c35fe38686a6fbf3902d6355c3f20363 + url: "https://pub.dev" + source: hosted + version: "0.33.0" flutter_localizations: dependency: "direct main" description: flutter @@ -1260,6 +1268,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + toml: + dependency: transitive + description: + name: toml + sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" + url: "https://pub.dev" + source: hosted + version: "0.14.0" tuple: dependency: "direct main" description: @@ -1436,6 +1452,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.3.0" + xml2json: + dependency: transitive + description: + name: xml2json + sha256: c8cb35b83cce879c2ea86951fd257f4e765b0030a0298b35cf94f2b3d0f32095 + url: "https://pub.dev" + source: hosted + version: "5.3.6" yaml: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 1a9930b11..066d65f8a 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: provider: ^6.0.3 graphql_flutter: ^5.1.2 url_launcher: ^6.1.6 + flutter_i18n: ^0.33.0 flutter_svg: ^2.0.2 permission_handler: ^10.2.0 package_info_plus: ^4.0.1 # for about dialog @@ -101,10 +102,12 @@ flutter: - assets/bayern/header-logo.png - assets/bayern/body-logo.png - assets/bayern/icon.png + - assets/bayern/translations/ - assets/nuernberg/header-logo.png - assets/nuernberg/body-logo.png - assets/nuernberg/background.png - assets/nuernberg/intro_slides/ + - assets/nuernberg/translations/ # An image asset can refer to one or more resolution-specific 'variants', see # https://flutter.dev/assets-and-images/#resolution-aware. From 274aeb6be2bca60ed8a5c749021b44ff597f9e02 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Sat, 26 Aug 2023 18:17:59 +0200 Subject: [PATCH 27/60] 904: More translations --- frontend/assets/bayern/translations/de.json | 51 +++++++++++++++++++ .../activation_workflow/activate_code.dart | 2 +- .../activation_code_scanner_page.dart | 38 +++++++------- .../activation_exception.dart | 4 +- .../activation_overwrite_existing_dialog.dart | 4 +- .../card_detail_view/card_detail_view.dart | 28 +++++----- .../card_detail_view/more_actions_dialog.dart | 5 +- .../card_detail_view/self_verify_card.dart | 2 +- .../identification/id_card/card_content.dart | 18 ++++--- .../qr_code_camera_permission_dialog.dart | 15 +++--- .../qr_code_scanner/qr_code_scanner.dart | 5 +- .../qr_code_scanner_controls.dart | 6 ++- .../qr_code_scanner/qr_overlay_shape.dart | 4 +- .../qr_parsing_error_dialog.dart | 5 +- .../lib/identification/qr_content_parser.dart | 3 +- .../negative_verification_result_dialog.dart | 2 +- .../positive_verification_result_dialog.dart | 2 +- .../dialogs/verification_info_dialog.dart | 12 ++--- .../query_server_verification.dart | 2 +- .../verification_qr_code_processor.dart | 14 ++--- .../verification_qr_scanner_page.dart | 30 +++++------ .../intro_slides/location_request_button.dart | 15 +++--- frontend/lib/location/determine_position.dart | 10 ++-- frontend/lib/location/dialogs.dart | 17 ++++--- frontend/lib/location/location_ffi.dart | 6 +-- frontend/lib/map/map/attribution_dialog.dart | 15 +++--- frontend/lib/map/map/map.dart | 4 +- .../preview/accepting_store_preview_card.dart | 4 +- frontend/lib/search/filter_bar.dart | 4 +- frontend/lib/search/location_button.dart | 10 ++-- frontend/lib/search/results_loader.dart | 11 ++-- frontend/lib/search/search_page.dart | 4 +- .../accepting_store_summary.dart | 12 +++-- .../store_widgets/detail/detail_app_bar.dart | 6 ++- .../store_widgets/detail/detail_content.dart | 23 +++++---- .../lib/store_widgets/detail/detail_page.dart | 8 +-- frontend/lib/util/json_canonicalizer.dart | 2 +- frontend/lib/widgets/app_bars.dart | 4 +- 38 files changed, 246 insertions(+), 161 deletions(-) diff --git a/frontend/assets/bayern/translations/de.json b/frontend/assets/bayern/translations/de.json index f7b1790e8..ad79a190c 100644 --- a/frontend/assets/bayern/translations/de.json +++ b/frontend/assets/bayern/translations/de.json @@ -14,15 +14,66 @@ "locationAccessDeactivated": "Die Standortfreigabe ist deaktiviert.", "checkSettings": "Prüfe Einstellungen...", "connectionFailed": "Keine Verbindung möglich", + "checkConnection": "Bitte Internetverbindung prüfen.", "ok": "OK", "done": "Fertig", "next": "Weiter", "previous": "Zurück", + "cancel": "Abbrechen", + "tryAgain": "Erneut versuchen", "settings": "Einstellungen", + "openSettings": "Einstellungen öffnen", + "moreActions": "Weitere Aktionen", "card": "Karte", "search": "Suche", "identification": "Ausweisen", "about": "Über", + "filterByCategories": "Nach Kategorien filtern", + "findCloseBy": "In meiner Nähe suchen", + "activateLocationAccess": "Standortermittlung aktivieren", + "activateLocationAccessSettings": "Aktivieren Sie die Standortermittlung in den Einstellungen.", + "grantLocation": "Ich möchte meinen Standort freigeben.", + "locationGranted": "Standort ist freigegeben.", + "locationDeactivated": "Standortfreigabe ist deaktiviert.", + "locationPermission": "Standortberechtigung", + "grantPermission": "Berechtigung erteilen", + "askPermissionsAgain": "Soll nocheinmal nach der Berechtigung gefragt werden?", + "cameraAccessRequired": "Zugriff auf Kamera erforderlich", + "cameraAccessRequiredSettings": "Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera.\nIn den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.", + "selfieCamera": "Frontkamera", + "standardCamera": "Standard-Kamera", + "flashOn": "Blitz an", + "flashOff": "Blitz aus", + "noAcceptingStoresFound": "Auf diese Suche trifft keine Akzeptanzstelle zu.", + "acceptingStoreNotFound": "Akzeptanzstelle nicht gefunden.", + "searchResults": "Suchresultate", + "searchHint": "Tippen, um zu suchen …", + "unlimited": "unbegrenzt", + "validFromUntil": "Gültig: {startDate} bis {expirationDate}", + "validUntil": "Gültig bis: {expirationDate}", + "cardExpired": "Ihre Karte ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", + "cardInvalid": "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", + "cardNotYetValid": "Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen.", + "checkFailed": "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Verbindung mit dem Internet besteht und prüfen Sie erneut.", + "checkAgain": "Erneut prüfen", + "timeIncorrect": "Die Uhrzeit Ihres Geräts scheint nicht zu stimmen. Bitte synchronisieren Sie die Uhrzeit in den Systemeinstellungen.", + "authenticationPossible": "Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:", + "scanQRCode": "Halten Sie die Kamera auf den QR Code.", + "scanningFailed": "Fehler beim Lesen des Codes", + "generatingTotpSecretFailed": "", + "loadingDataFailed": "Fehler beim Laden der Daten.", + "loadingInformationFailed": "Fehler beim Laden der Infos.", + "showOnMap": "Auf Karte zeigen", + "email": "E-Mail", + "phone": "Telefon", + "website": "Website", + "address": "Adresse", + "acceptingStore": "Akzeptanzstelle", + "noDescriptionAvailable": "Keine Beschreibung verfügbar", + "unknownCategory": "Unbekannte Kategorie", + "showMapCopyright": "Zeige Infos über das Urheberrecht der Kartendaten", + "mapData": "Kartendaten", + "osmContributors": "OpenStreetMap Mitwirkende", "categories": { "mobilityLong": "Auto/Zweirad", "mobility": "Mobilität", diff --git a/frontend/lib/identification/activation_workflow/activate_code.dart b/frontend/lib/identification/activation_workflow/activate_code.dart index 88db4aaff..1b791b5a2 100644 --- a/frontend/lib/identification/activation_workflow/activate_code.dart +++ b/frontend/lib/identification/activation_workflow/activate_code.dart @@ -49,6 +49,6 @@ class ServerCardActivationException implements Exception { @override String toString() { - return "ServerCardActivationException{cause: $cause}"; + return 'ServerCardActivationException{cause: $cause}'; } } diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 2232874c9..f410c4439 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -54,28 +54,28 @@ class ActivationCodeScannerPage extends StatelessWidget { await showError(e.toString(), null); } on QrCodeFieldMissingException catch (e) { await showError( - 'Der Inhalt des eingescannten Codes ist unvollständig. ' - '(Fehlercode: ${e.missingFieldName}Missing)', + "Der Inhalt des eingescannten Codes ist unvollständig. " + "(Fehlercode: ${e.missingFieldName}Missing)", null); } on QrCodeWrongTypeException catch (_) { - await showError('Der eingescannte Code kann nicht in der App gespeichert werden.', null); + await showError("Der eingescannte Code kann nicht in der App gespeichert werden."); } on CardExpiredException catch (e) { - final dateFormat = DateFormat('dd.MM.yyyy'); - await showError( - 'Der eingescannte Code ist bereits am ' - '${dateFormat.format(e.expiry)} abgelaufen.', - null); + final dateFormat = DateFormat("dd.MM.yyyy"); + await showError("Der eingescannte Code ist bereits am " + "${dateFormat.format(e.expiry)} abgelaufen."); } on ServerCardActivationException catch (e, stackTrace) { String errorMessage = 'Der eingescannte Code konnte nicht aktiviert ' 'werden, da die Kommunikation mit dem Server fehlschlug. ' 'Bitte prüfen Sie Ihre Internetverbindung.'; await ConnectionFailedDialog.show( context, - errorMessage, + "Der eingescannte Code konnte nicht aktiviert " + "werden, da die Kommunikation mit dem Server fehlschlug. " + "Bitte prüfen Sie Ihre Internetverbindung.", ); - await reportError(errorMessage + e.toString(), stackTrace); - } on Exception catch (e, stackTrace) { - await showError('Ein unerwarteter Fehler ist aufgetreten.', stackTrace); + } on Exception catch (e, stacktrace) { + debugPrintStack(stackTrace: stacktrace, label: e.toString()); + await showError("Ein unerwarteter Fehler ist aufgetreten."); } } @@ -90,7 +90,7 @@ class ActivationCodeScannerPage extends StatelessWidget { final activationSecretBase64 = const Base64Encoder().convert(activationCode.activationSecret); final cardInfoBase64 = activationCode.info.hash(activationCode.pepper); - debugPrint('Card Activation: Sending request with overwriteExisting=$overwriteExisting.'); + debugPrint("Card Activation: Sending request with overwriteExisting=$overwriteExisting."); final activationResult = await activateCode( client: client, @@ -107,7 +107,7 @@ class ActivationCodeScannerPage extends StatelessWidget { throw const ActivationInvalidTotpSecretException(); } final totpSecret = const Base64Decoder().convert(activationResult.totpSecret!); - debugPrint('Card Activation: Successfully activated.'); + debugPrint("Card Activation: Successfully activated."); provider.setCode(DynamicUserCode() ..info = activationCode.info @@ -120,7 +120,7 @@ class ActivationCodeScannerPage extends StatelessWidget { case ActivationState.failed: await QrParsingErrorDialog.showErrorDialog( context, - 'Der eingescannte Code ist ungültig.', + "Der eingescannte Code ist ungültig.", ); break; case ActivationState.didNotOverwriteExisting: @@ -128,15 +128,15 @@ class ActivationCodeScannerPage extends StatelessWidget { throw const ActivationDidNotOverwriteExisting(); } debugPrint( - 'Card Activation: Card had been activated already and was not overwritten. Waiting for user feedback.'); + "Card Activation: Card had been activated already and was not overwritten. Waiting for user feedback."); if (await ActivationOverwriteExistingDialog.showActivationOverwriteExistingDialog(context)) { await _activateCode(context, activationCode, overwriteExisting = true); } break; default: - String errorMessage = 'Die Aktivierung befindet sich in einem ungültigen Zustand.'; - reportError(errorMessage, null); - throw ServerCardActivationException(errorMessage); + throw const ServerCardActivationException( + "Die Aktivierung befindet sich in einem ungültigen Zustand.", + ); } } } diff --git a/frontend/lib/identification/activation_workflow/activation_exception.dart b/frontend/lib/identification/activation_workflow/activation_exception.dart index fb9233da5..7565da1fb 100644 --- a/frontend/lib/identification/activation_workflow/activation_exception.dart +++ b/frontend/lib/identification/activation_workflow/activation_exception.dart @@ -2,7 +2,7 @@ import 'package:ehrenamtskarte/identification/activation_workflow/activate_code. class ActivationInvalidTotpSecretException extends ServerCardActivationException { const ActivationInvalidTotpSecretException() - : super('Der Server konnte kein TotpSecret für den eingescannten QRCode generieren.'); + : super("Der Server konnte kein TotpSecret für den eingescannten QRCode generieren."); } class ActivationDidNotOverwriteExisting implements Exception { @@ -10,6 +10,6 @@ class ActivationDidNotOverwriteExisting implements Exception { @override String toString() { - return 'Der eingescannte QRCode wurde bereits aktiviert.'; + return "Der eingescannte QRCode wurde bereits aktiviert."; } } diff --git a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart index 86256b457..9c9747bd2 100644 --- a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart +++ b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart @@ -6,12 +6,12 @@ class ActivationOverwriteExistingDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text('Karte auf diesem Gerät aktivieren?', style: TextStyle(fontSize: 18)), + title: const Text("Karte auf diesem Gerät aktivieren?", style: TextStyle(fontSize: 18)), content: SingleChildScrollView( child: ListBody( children: const [ Text( - 'Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.', + "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", ), ], ), diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index 23bee3409..63a596646 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -6,9 +6,11 @@ import 'package:ehrenamtskarte/identification/id_card/id_card.dart'; import 'package:ehrenamtskarte/identification/util/card_info_utils.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; +import '../../util/i18n.dart'; import '../user_code_model.dart'; import 'verification_code_view.dart'; @@ -42,7 +44,7 @@ class _CardDetailViewState extends State { // in order to detect if one of the following events happened: // - the card was activated on another device // - the card was revoked - // - the card expired (on backend"s system time) + // - the card expired (on backend's system time) _selfVerifyCard(); initiatedSelfVerification = true; } @@ -144,7 +146,7 @@ enum CardStatus { notVerifiedLately, // The time of the device was out of sync with the server. timeOutOfSync, - // The validity period didn"t start yet according to the clock of the local device + // The validity period didn't start yet according to the clock of the local device notYetValid, // The card was verified lately by the server and it responded that the card is invalid. invalid, @@ -187,40 +189,36 @@ class QrCodeAndStatus extends StatelessWidget { children: [ ...switch (status) { CardStatus.expired => [ - _PaddedText( - 'Ihre Karte ist abgelaufen.\nUnter "Weitere Aktionen" können Sie einen Antrag auf Verlängerung stellen.') + _PaddedText(t(context, 'cardExpired')) ], CardStatus.notVerifiedLately => [ - _PaddedText( - "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Verbindung mit dem Internet besteht und prüfen Sie erneut."), + _PaddedText(t(context, 'checkFailed')), Flexible( child: TextButton.icon( icon: const Icon(Icons.refresh), onPressed: onSelfVerifyPressed, - label: Text('Erneut prüfen'), + label: I18nText('checkAgain'), ), ), ], CardStatus.timeOutOfSync => [ - _PaddedText( - "Die Uhrzeit Ihres Geräts scheint nicht zu stimmen. Bitte synchronisieren Sie die Uhrzeit in den Systemeinstellungen."), + _PaddedText(t(context, 'timeIncorrect')), Flexible( child: TextButton.icon( icon: const Icon(Icons.refresh), onPressed: onSelfVerifyPressed, - label: Text('Erneut prüfen'), + label: I18nText('checkAgain'), )) ], CardStatus.invalid => [ - _PaddedText( - "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.") + _PaddedText(t(context, 'cardInvalid')) ], CardStatus.valid => [ - _PaddedText("Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:"), + _PaddedText(t(context, 'authenticationPossible')), Flexible(child: VerificationCodeView(userCode: userCode)) ], CardStatus.notYetValid => [ - _PaddedText("Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen."), + _PaddedText(t(context, 'cardNotYetValid')), ] }, Container( @@ -228,7 +226,7 @@ class QrCodeAndStatus extends StatelessWidget { child: TextButton( onPressed: onMoreActionsPressed, child: Text( - 'Weitere Aktionen', + t(context, 'moreActions'), style: TextStyle(color: Theme.of(context).colorScheme.secondary), ), ), diff --git a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart index 241671b2a..24b2c31c2 100644 --- a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart +++ b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart @@ -1,5 +1,6 @@ import 'package:ehrenamtskarte/build_config/build_config.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; class MoreActionsDialog extends StatelessWidget { final VoidCallback startActivation; @@ -18,7 +19,7 @@ class MoreActionsDialog extends StatelessWidget { final localization = buildConfig.localization.identification.moreActions; return AlertDialog( contentPadding: const EdgeInsets.only(top: 12), - title: const Text('Weitere Aktionen'), + title: I18nText('moreActions'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -53,7 +54,7 @@ class MoreActionsDialog extends StatelessWidget { ], ), ), - actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('Abbrechen'))], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: I18nText('cancel'))], ); } } diff --git a/frontend/lib/identification/card_detail_view/self_verify_card.dart b/frontend/lib/identification/card_detail_view/self_verify_card.dart index 1aee9b3eb..5402f7f8a 100644 --- a/frontend/lib/identification/card_detail_view/self_verify_card.dart +++ b/frontend/lib/identification/card_detail_view/self_verify_card.dart @@ -30,7 +30,7 @@ Future selfVerifyCard(UserCodeModel userCodeModel, String projectId, Graph return; } - debugPrint("Card Self-Verification: Persisting response. Card is ${cardVerification.valid ? "valid." : "INVALID."}"); + debugPrint('Card Self-Verification: Persisting response. Card is ${cardVerification.valid ? 'valid.' : 'INVALID.'}'); userCodeModel.setCode(DynamicUserCode() ..info = userCode.info diff --git a/frontend/lib/identification/id_card/card_content.dart b/frontend/lib/identification/id_card/card_content.dart index 42d7f9f93..5e8b85ff9 100644 --- a/frontend/lib/identification/id_card/card_content.dart +++ b/frontend/lib/identification/id_card/card_content.dart @@ -6,6 +6,8 @@ import 'package:ehrenamtskarte/util/color_utils.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import '../../util/i18n.dart'; + Color standardCardColor = getColorFromHex(buildConfig.cardBranding.colorStandard); Color premiumCardColor = getColorFromHex(buildConfig.cardBranding.colorPremium); Color textColor = getColorFromHex(buildConfig.cardBranding.bodyTextColor); @@ -49,14 +51,14 @@ class CardContent extends StatelessWidget { String get _formattedExpirationDate { final expirationDay = cardInfo.hasExpirationDay() ? cardInfo.expirationDay : null; return expirationDay != null - ? DateFormat("dd.MM.yyyy").format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: expirationDay))) - : "unbegrenzt"; + ? DateFormat('dd.MM.yyyy').format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: expirationDay))) + : t(context, 'unlimited'); } String? get _formattedBirthday { final birthday = cardInfo.extensions.hasExtensionBirthday() ? cardInfo.extensions.extensionBirthday.birthday : null; return birthday != null - ? DateFormat("dd.MM.yyyy").format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: birthday))) + ? DateFormat('dd.MM.yyyy').format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: birthday))) : null; } @@ -69,12 +71,14 @@ class CardContent extends StatelessWidget { String? get _formattedStartDate { final startDay = cardInfo.extensions.hasExtensionStartDay() ? cardInfo.extensions.extensionStartDay.startDay : null; return startDay != null - ? DateFormat("dd.MM.yyyy").format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: startDay))) + ? DateFormat('dd.MM.yyyy').format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: startDay))) : null; } String _getCardValidityDate(String? startDate, String expirationDate) { - return startDate != null ? "Gültig: $startDate bis $expirationDate" : "Gültig bis: $expirationDate"; + return startDate != null + ? t(context, 'validFromUntil', translationParams: {'startDate': startDate, 'expirationDate': expirationDate}) + : t(context, 'validUntil', translationParams: { 'expirationDate': expirationDate }); } @override @@ -90,7 +94,7 @@ class CardContent extends StatelessWidget { final scaleFactor = constraints.maxWidth / 300; final currentRegion = region; final headerLeftTitle = buildConfig.cardBranding.headerTitleLeft.isEmpty && currentRegion != null - ? "${currentRegion.prefix} ${currentRegion.name}" + ? '${currentRegion.prefix} ${currentRegion.name}' : buildConfig.cardBranding.headerTitleLeft; return Column( mainAxisAlignment: MainAxisAlignment.start, @@ -163,7 +167,7 @@ class CardContent extends StatelessWidget { AspectRatio( aspectRatio: 6 / 1.5, child: Align( - alignment: buildConfig.cardBranding.bodyLogoPosition == "center" + alignment: buildConfig.cardBranding.bodyLogoPosition == 'center' ? Alignment.center : Alignment.centerRight, child: Image( diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart index f1177a981..d6f6c2b21 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart @@ -1,31 +1,32 @@ import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:permission_handler/permission_handler.dart'; +import '../../util/i18n.dart'; + class QrCodeCameraPermissionDialog extends StatelessWidget { const QrCodeCameraPermissionDialog(); @override Widget build(BuildContext context) { return AlertDialog( - title: const Text('Zugriff auf Kamera erforderlich', style: TextStyle(fontSize: 18)), + title: Text(t(context, 'cameraAccessRequired'), style: TextStyle(fontSize: 18)), content: SingleChildScrollView( child: ListBody( - children: const [ - Text( - 'Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera.\nIn den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.', - ), + children: [ + I18nText('cameraAccessRequiredSettings'), ], ), ), actions: [ TextButton( - child: const Text("Abbrechen"), + child: I18nText('cancel'), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( - child: const Text("Einstellungen öffnen"), + child: I18nText('openSettings'), onPressed: () { openAppSettings(); }, diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart index d07dcb2d1..e310e2599 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_scanner_controls.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_overlay_shape.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; typedef OnCodeScannedCallback = Future Function(Uint8List code); @@ -24,7 +25,7 @@ class _QRViewState extends State { formats: [BarcodeFormat.qrCode], returnImage: false, ); - final GlobalKey qrKey = GlobalKey(debugLabel: "QR"); + final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); // Determines whether a code is currently processed by the onCodeScanned callback // During this time, we do not re-trigger the callback. @@ -75,7 +76,7 @@ class _QRViewState extends State { children: [ Container( margin: const EdgeInsets.all(8), - child: const Text("Halten Sie die Kamera auf den QR Code."), + child: I18nText('scanQRCode'), ), QrCodeScannerControls(controller: controller) ], diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart index ade18c3f1..018349787 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import '../../util/i18n.dart'; + class QrCodeScannerControls extends StatelessWidget { final MobileScannerController controller; @@ -19,7 +21,7 @@ class QrCodeScannerControls extends StatelessWidget { child: ValueListenableBuilder( valueListenable: controller.torchState, builder: (ctx, state, child) => Text( - state == TorchState.on ? 'Blitz aus' : 'Blitz an', + t(context, state == TorchState.on ? 'flashOff' : 'flashOn'), style: const TextStyle(fontSize: 16), ), ), @@ -32,7 +34,7 @@ class QrCodeScannerControls extends StatelessWidget { child: ValueListenableBuilder( valueListenable: controller.cameraFacingState, builder: (ctx, state, child) => Text( - state == CameraFacing.back ? 'Frontkamera' : 'Standard-Kamera', + t(context, state == CameraFacing.back ? 'selfieCamera' : 'standardCamera'), style: const TextStyle(fontSize: 16), ), ), diff --git a/frontend/lib/identification/qr_code_scanner/qr_overlay_shape.dart b/frontend/lib/identification/qr_code_scanner/qr_overlay_shape.dart index ceefa2c13..e0a85596a 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_overlay_shape.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_overlay_shape.dart @@ -19,13 +19,13 @@ class QrScannerOverlayShape extends ShapeBorder { cutOutHeight = cutOutHeight ?? cutOutSize ?? 250 { assert( borderLength <= min(this.cutOutWidth, this.cutOutHeight) / 2 + borderWidth * 2, - "Border can't be larger than ${min(this.cutOutWidth, this.cutOutHeight) / 2 + borderWidth * 2}", + 'Border can\'t be larger than ${min(this.cutOutWidth, this.cutOutHeight) / 2 + borderWidth * 2}', ); // ignore: prefer_asserts_in_initializer_lists assert( (cutOutWidth == null && cutOutHeight == null) || (cutOutSize == null && cutOutWidth != null && cutOutHeight != null), - "Use only cutOutWidth and cutOutHeight or only cutOutSize", + 'Use only cutOutWidth and cutOutHeight or only cutOutSize', ); } diff --git a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart index 2118b3672..4d6028205 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_i18n/widgets/I18nText.dart'; class QrParsingErrorDialog extends StatelessWidget { final String message; @@ -8,7 +9,7 @@ class QrParsingErrorDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text("Fehler beim Lesen des Codes"), + title: I18nText('scanningFailed'), content: SingleChildScrollView( child: ListBody( children: [ @@ -18,7 +19,7 @@ class QrParsingErrorDialog extends StatelessWidget { ), actions: [ TextButton( - child: const Text("Ok"), + child: I18nText('ok'), onPressed: () { Navigator.of(context).pop(); }, diff --git a/frontend/lib/identification/qr_content_parser.dart b/frontend/lib/identification/qr_content_parser.dart index 49c48b9bb..720804dfe 100644 --- a/frontend/lib/identification/qr_content_parser.dart +++ b/frontend/lib/identification/qr_content_parser.dart @@ -22,8 +22,7 @@ extension QRParsing on Uint8List { qrcode = QrCode.fromBuffer(this); } on Exception catch (e, stackTrace) { throw VerificationParseException( - internalMessage: 'Failed to parse QrCode from encoded data. ' - 'Message: ${e.toString()}', + internalMessage: 'Failed to parse QrCode from encoded data. Message: ${e.toString()}', cause: e, stackTrace: stackTrace, ); diff --git a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart index 72e234bfd..ab080dcd0 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart @@ -9,7 +9,7 @@ class NegativeVerificationResultDialog extends StatelessWidget { @override Widget build(BuildContext context) { return InfoDialog( - title: 'Nicht verifiziert', + title: "Nicht verifiziert", icon: Icons.error, iconColor: Colors.red, child: Text(reason), diff --git a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart index 3cd79fe39..15bd6196e 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart @@ -55,7 +55,7 @@ class PositiveVerificationResultDialogState extends State _onDone(context), ) ], @@ -78,7 +78,7 @@ class _EnumeratedListItem extends StatelessWidget { CircleAvatar( backgroundColor: Theme.of(context).colorScheme.primary, child: Text( - '${index + 1}', + "${index + 1}", style: TextStyle( color: Theme.of(context).colorScheme.background, fontWeight: FontWeight.bold, diff --git a/frontend/lib/identification/verification_workflow/query_server_verification.dart b/frontend/lib/identification/verification_workflow/query_server_verification.dart index 976cc9070..e000cfc06 100644 --- a/frontend/lib/identification/verification_workflow/query_server_verification.dart +++ b/frontend/lib/identification/verification_workflow/query_server_verification.dart @@ -79,7 +79,7 @@ Future _queryServerVer final data = queryResult.data; if (data == null) { - throw ServerVerificationException('Returned data is null.'); + throw ServerVerificationException("Returned data is null."); } final parsedResult = byCardDetailsHash.parse(data); diff --git a/frontend/lib/identification/verification_workflow/verification_qr_code_processor.dart b/frontend/lib/identification/verification_workflow/verification_qr_code_processor.dart index 7c7dd91e1..6613c6918 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_code_processor.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_code_processor.dart @@ -33,8 +33,8 @@ Future verifyDynamicVerificationCode( _assertConsistentDynamicVerificationCode(code); final (outOfSync: outOfSync, result: result) = await queryDynamicServerVerification(client, projectId, code); if (outOfSync) { - debugPrint("Verification: This device's time is out of sync with the server." - 'Ignoring, as only the time of the device that generates the QR code is relevant for the verification process.'); + debugPrint("Verification: This device"s time is out of sync with the server." + "Ignoring, as only the time of the device that generates the QR code is relevant for the verification process."); } if (!result.valid) { @@ -58,7 +58,7 @@ Future verifyStaticVerificationCode( void assertConsistentCardInfo(CardInfo cardInfo) { if (!cardInfo.hasFullName()) { - throw QrCodeFieldMissingException('fullName'); + throw QrCodeFieldMissingException("fullName"); } if (!cardInfo.hasExpirationDay() && cardInfo.extensions.extensionBavariaCardType.cardType != BavariaCardType.GOLD) { throw QRCodeMissingExpiryException(); @@ -75,16 +75,16 @@ void assertConsistentCardInfo(CardInfo cardInfo) { void _assertConsistentDynamicVerificationCode(DynamicVerificationCode verificationCode) { if (!verificationCode.hasPepper()) { - throw QrCodeFieldMissingException('pepper'); + throw QrCodeFieldMissingException("pepper"); } if (verificationCode.otp <= 0) { - throw QrCodeFieldMissingException('otp'); + throw QrCodeFieldMissingException("otp"); } } void _assertConsistentStaticVerificationCode(StaticVerificationCode verificationCode) { if (!verificationCode.hasPepper()) { - throw QrCodeFieldMissingException('pepper'); + throw QrCodeFieldMissingException("pepper"); } } @@ -98,5 +98,5 @@ extension DateComparison on DateTime { class CardExpiredException extends QrCodeParseException { final DateTime expiry; - CardExpiredException(this.expiry) : super('card already expired'); + CardExpiredException(this.expiry) : super("card already expired"); } diff --git a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart index cb5884a43..e37a85b28 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart @@ -61,7 +61,7 @@ class VerificationQrScannerPage extends StatelessWidget { final verificationCode = verificationQrCode.writeToBuffer(); _handleQrCode(context, verificationCode); }, - child: const Text('Verify activated Card'), + child: const Text("Verify activated Card"), ) ], ); @@ -75,7 +75,7 @@ class VerificationQrScannerPage extends StatelessWidget { if (cardInfo == null) { await _onError( context, - 'Der eingescannte Code konnte vom Server nicht verifiziert werden!', + "Der eingescannte Code konnte vom Server nicht verifiziert werden!", ); } else { await _onSuccess(context, cardInfo, qrcode.hasStaticVerificationCode()); @@ -83,37 +83,37 @@ class VerificationQrScannerPage extends StatelessWidget { } on ServerVerificationException catch (e) { await _onConnectionError( context, - 'Der eingescannte Code konnte nicht verifiziert ' - 'werden, da die Kommunikation mit dem Server fehlschlug. ' - 'Bitte prüfen Sie Ihre Internetverbindung.', + "Der eingescannte Code konnte nicht verifiziert " + "werden, da die Kommunikation mit dem Server fehlschlug. " + "Bitte prüfen Sie Ihre Internetverbindung.", e, ); } on QrCodeFieldMissingException catch (e) { await _onError( context, - 'Der eingescannte Code ist nicht gültig, ' - 'da erforderliche Daten fehlen.', + "Der eingescannte Code ist nicht gültig, " + "da erforderliche Daten fehlen.", e, ); } on CardExpiredException catch (e) { - final dateFormat = DateFormat('dd.MM.yyyy'); + final dateFormat = DateFormat("dd.MM.yyyy"); await _onError( context, - 'Der eingescannte Code ist bereits am ${dateFormat.format(e.expiry)} abgelaufen.', + "Der eingescannte Code ist bereits am ${dateFormat.format(e.expiry)} abgelaufen.", e, ); } on QrCodeParseException catch (e) { await _onError( context, - 'Der Inhalt des eingescannten Codes kann nicht verstanden ' - 'werden. Vermutlich handelt es sich um einen QR-Code, der nicht für ' - 'diese App generiert wurde.', + "Der Inhalt des eingescannten Codes kann nicht verstanden " + "werden. Vermutlich handelt es sich um einen QR-Code, der nicht für " + "diese App generiert wurde.", e, ); } on Exception catch (e) { await _onError( context, - 'Beim Einlesen des QR-Codes ist ein unbekannter Fehler aufgetreten.', + "Beim Einlesen des QR-Codes ist ein unbekannter Fehler aufgetreten.", e, ); } finally { @@ -124,14 +124,14 @@ class VerificationQrScannerPage extends StatelessWidget { Future _onError(BuildContext context, String message, [Exception? exception]) async { if (exception != null) { - debugPrint('Verification failed: $exception'); + debugPrint("Verification failed: $exception"); } await NegativeVerificationResultDialog.show(context, message); } Future _onConnectionError(BuildContext context, String message, [Exception? exception]) async { if (exception != null) { - debugPrint('Connection failed: $exception'); + debugPrint("Connection failed: $exception"); } await ConnectionFailedDialog.show(context, message); } diff --git a/frontend/lib/intro_slides/location_request_button.dart b/frontend/lib/intro_slides/location_request_button.dart index d6758765a..77a34911f 100644 --- a/frontend/lib/intro_slides/location_request_button.dart +++ b/frontend/lib/intro_slides/location_request_button.dart @@ -1,6 +1,7 @@ import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/location/determine_position.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:provider/provider.dart'; class LocationRequestButton extends StatefulWidget { @@ -49,9 +50,9 @@ class _LocationRequestButtonState extends State { final settings = Provider.of(context); final status = _locationPermissionStatus; if (status == null) { - return const ElevatedButton( + return ElevatedButton( onPressed: null, - child: Text('Prüfe Einstellungen...'), + child: I18nText('checkSettings'), ); } switch (status) { @@ -59,18 +60,18 @@ class _LocationRequestButtonState extends State { case LocationStatus.notSupported: return ElevatedButton( onPressed: () => _onLocationButtonClicked(settings), - child: const Text('Ich möchte meinen Standort freigeben.'), + child: I18nText('grantLocation'), ); case LocationStatus.whileInUse: case LocationStatus.always: - return const ElevatedButton( + return ElevatedButton( onPressed: null, - child: Text('Standort ist freigegeben.'), + child: I18nText('locationGranted'), ); case LocationStatus.deniedForever: - return const ElevatedButton( + return ElevatedButton( onPressed: null, - child: Text('Standortfreigabe ist deaktiviert.'), + child: I18nText('locationDeactivated'), ); } } diff --git a/frontend/lib/location/determine_position.dart b/frontend/lib/location/determine_position.dart index 2255216f1..72e4c555c 100644 --- a/frontend/lib/location/determine_position.dart +++ b/frontend/lib/location/determine_position.dart @@ -11,16 +11,16 @@ enum LocationStatus { /// request for permission another time. denied, - /// Permission to access the device"s location is permenantly denied. When + /// Permission to access the device's location is permenantly denied. When /// requestiong permissions the permission dialog will not been shown until /// the user updates the permission in the App settings. deniedForever, - /// Permission to access the device"s location is allowed only while + /// Permission to access the device's location is allowed only while /// the App is in use. whileInUse, - /// Permission to access the device"s location is allowed even when the + /// Permission to access the device's location is allowed even when the /// App is running in the background. always, @@ -102,7 +102,7 @@ Future checkAndRequestLocationPermission( BuildContext context, { bool requestIfNotGranted = true, String rationale = - 'Erlauben Sie der App Ihren Standort zu benutzen, um Akzeptanzstellen in Ihrer Umgebung anzuzeigen.', + "Erlauben Sie der App Ihren Standort zu benutzen, um Akzeptanzstellen in Ihrer Umgebung anzuzeigen.", Future Function()? onDisableFeature, Future Function()? onEnableFeature, }) async { @@ -140,7 +140,7 @@ Future checkAndRequestLocationPermission( if (requestResult == LocationPermission.denied) { // Permissions are denied, next time you could try // requesting permissions again (this is also where - // Android"s shouldShowRequestPermissionRationale + // Android's shouldShowRequestPermissionRationale // returned true. According to Android guidelines // your App should show an explanatory UI now. diff --git a/frontend/lib/location/dialogs.dart b/frontend/lib/location/dialogs.dart index 3d4ac88a7..2d576f7ce 100644 --- a/frontend/lib/location/dialogs.dart +++ b/frontend/lib/location/dialogs.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_i18n/widgets/I18nText.dart'; class LocationServiceDialog extends StatelessWidget { const LocationServiceDialog({super.key}); @@ -6,11 +7,11 @@ class LocationServiceDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text('Standortermittlung aktivieren'), - content: const Text('Aktivieren Sie die Standortermittlung in den Einstellungen.'), + title: I18nText('activateLocationAccess'), + content: I18nText('activateLocationAccessSettings'), actions: [ - TextButton(child: const Text('Abbrechen'), onPressed: () => Navigator.of(context).pop(false)), - TextButton(child: const Text('Einstellungen öffnen'), onPressed: () => Navigator.of(context).pop(true)) + TextButton(child: I18nText('cancel'), onPressed: () => Navigator.of(context).pop(false)), + TextButton(child: I18nText('openSettings'), onPressed: () => Navigator.of(context).pop(true)) ], ); } @@ -24,15 +25,15 @@ class RationaleDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text('Standortberechtigung'), + title: I18nText('locationPermission'), content: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: [Text(_rationale), const Text("Soll nocheinmal nach der Berechtigung gefragt werden?")], + children: [Text(_rationale), I18nText('askPermissionsAgain')], ), actions: [ - TextButton(child: const Text('Berechtigung erteilen'), onPressed: () => Navigator.of(context).pop(true)), - TextButton(child: const Text('Abbrechen'), onPressed: () => Navigator.of(context).pop(false)) + TextButton(child: I18nText('grantPermission'), onPressed: () => Navigator.of(context).pop(true)), + TextButton(child: I18nText('cancel'), onPressed: () => Navigator.of(context).pop(false)) ], ); } diff --git a/frontend/lib/location/location_ffi.dart b/frontend/lib/location/location_ffi.dart index 7ef4c0afe..c2bff91c0 100644 --- a/frontend/lib/location/location_ffi.dart +++ b/frontend/lib/location/location_ffi.dart @@ -1,14 +1,14 @@ import 'package:flutter/services.dart'; -const platform = MethodChannel("app.ehrenamtskarte/location"); +const platform = MethodChannel('app.ehrenamtskarte/location'); /// Checks whether location services are enabled /// without using Google Play Services Future isNonGoogleLocationServiceEnabled() async { try { - final bool result = await platform.invokeMethod("isLocationServiceEnabled") as bool; + final bool result = await platform.invokeMethod('isLocationServiceEnabled') as bool; return result; } on PlatformException catch (e) { - throw "Failed to get state of location services: '${e.message}'."; + throw 'Failed to get state of location services: "${e.message}".'; } } diff --git a/frontend/lib/map/map/attribution_dialog.dart b/frontend/lib/map/map/attribution_dialog.dart index 749746249..6442f488c 100644 --- a/frontend/lib/map/map/attribution_dialog.dart +++ b/frontend/lib/map/map/attribution_dialog.dart @@ -1,7 +1,10 @@ import 'package:ehrenamtskarte/map/map/attribution_dialog_item.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import '../../util/i18n.dart'; + class AttributionDialog extends StatelessWidget { const AttributionDialog({super.key}); @@ -9,14 +12,14 @@ class AttributionDialog extends StatelessWidget { Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.primary; return SimpleDialog( - title: const Text("Kartendaten"), + title: I18nText('mapData'), children: [ AttributionDialogItem( icon: Icons.copyright, color: color, - text: "OpenStreetMap Mitwirkende", + text: t(context, 'osmContributors'), onPressed: () { - launchUrlString('https://www.openstreetmap.org/copyright', mode: LaunchMode.externalApplication); + launchUrlString("https://www.openstreetmap.org/copyright", mode: LaunchMode.externalApplication); }, ), AttributionDialogItem( @@ -24,7 +27,7 @@ class AttributionDialog extends StatelessWidget { color: color, text: "OpenMapTiles", onPressed: () { - launchUrlString('https://openmaptiles.org/', mode: LaunchMode.externalApplication); + launchUrlString("https://openmaptiles.org/", mode: LaunchMode.externalApplication); }, ), AttributionDialogItem( @@ -32,7 +35,7 @@ class AttributionDialog extends StatelessWidget { color: color, text: "Natural Earth", onPressed: () { - launchUrlString('https://naturalearthdata.com/', mode: LaunchMode.externalApplication); + launchUrlString("https://naturalearthdata.com/", mode: LaunchMode.externalApplication); }, ), AttributionDialogItem( @@ -40,7 +43,7 @@ class AttributionDialog extends StatelessWidget { color: color, text: "LBE Bayern", onPressed: () { - launchUrlString('https://www.lbe.bayern.de/', mode: LaunchMode.externalApplication); + launchUrlString("https://www.lbe.bayern.de/", mode: LaunchMode.externalApplication); }, ) ], diff --git a/frontend/lib/map/map/map.dart b/frontend/lib/map/map/map.dart index 2653a9bf5..004a7b4a2 100644 --- a/frontend/lib/map/map/map.dart +++ b/frontend/lib/map/map/map.dart @@ -12,6 +12,8 @@ import 'package:geolocator/geolocator.dart'; import 'package:maplibre_gl/mapbox_gl.dart'; import 'package:tuple/tuple.dart'; +import '../../util/i18n.dart'; + typedef OnFeatureClickCallback = void Function(dynamic feature); typedef OnNoFeatureClickCallback = void Function(); typedef OnMapCreatedCallback = void Function(MapController controller); @@ -96,7 +98,7 @@ class _MapContainerState extends State implements MapController { color: mapboxColor, iconSize: 20, icon: const Icon(Icons.info_outline), - tooltip: "Zeige Infos über das Urheberrecht der Kartendaten", + tooltip: t(context, 'showMapCopyright'), onPressed: () { showDialog( context: context, diff --git a/frontend/lib/map/preview/accepting_store_preview_card.dart b/frontend/lib/map/preview/accepting_store_preview_card.dart index 7c0f8f646..ad704d509 100644 --- a/frontend/lib/map/preview/accepting_store_preview_card.dart +++ b/frontend/lib/map/preview/accepting_store_preview_card.dart @@ -4,6 +4,8 @@ import 'package:ehrenamtskarte/store_widgets/accepting_store_summary.dart'; import 'package:ehrenamtskarte/widgets/error_message.dart'; import 'package:flutter/material.dart'; +import '../../util/i18n.dart'; + class AcceptingStorePreviewError extends StatelessWidget { final void Function()? refetch; @@ -16,7 +18,7 @@ class AcceptingStorePreviewError extends StatelessWidget { child: Container( height: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16), - child: const ErrorMessage('Fehler beim Laden der Infos.'), + child: const ErrorMessage(t(context, 'loadingInformationFailed')), ), ); } diff --git a/frontend/lib/search/filter_bar.dart b/frontend/lib/search/filter_bar.dart index faf93b41c..a95916189 100644 --- a/frontend/lib/search/filter_bar.dart +++ b/frontend/lib/search/filter_bar.dart @@ -4,6 +4,8 @@ import 'package:ehrenamtskarte/category_assets.dart'; import 'package:ehrenamtskarte/search/filter_bar_button.dart'; import 'package:flutter/material.dart'; +import '../util/i18n.dart'; + class FilterBar extends StatelessWidget { final Function(CategoryAsset, bool) onCategoryPress; @@ -23,7 +25,7 @@ class FilterBar extends StatelessWidget { padding: const EdgeInsets.all(8), child: Row( children: [ - Text('Nach Kategorien filtern'.toUpperCase(), maxLines: 1, style: const TextStyle(color: Colors.grey)), + Text(t(context, 'filterByCategories').toUpperCase(), maxLines: 1, style: const TextStyle(color: Colors.grey)), const Expanded(child: Padding(padding: EdgeInsets.only(left: 8), child: Divider(thickness: 0.7))) ], ), diff --git a/frontend/lib/search/location_button.dart b/frontend/lib/search/location_button.dart index 2ce536e7d..e032ee499 100644 --- a/frontend/lib/search/location_button.dart +++ b/frontend/lib/search/location_button.dart @@ -2,6 +2,7 @@ import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/location/determine_position.dart'; import 'package:ehrenamtskarte/widgets/small_button_spinner.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:geolocator/geolocator.dart'; import 'package:provider/provider.dart'; @@ -54,10 +55,11 @@ class _LocationButtonState extends State { color: Theme.of(context).colorScheme.secondary, ), ), - label: Text( - 'In meiner Nähe suchen', - style: TextStyle(color: Theme.of(context).hintColor), - ), + label: I18nText('findCloseBy', + child: Text( + '', + style: TextStyle(color: Theme.of(context).hintColor), + )), ), ); } else { diff --git a/frontend/lib/search/results_loader.dart b/frontend/lib/search/results_loader.dart index ec8962a89..2e72d3850 100644 --- a/frontend/lib/search/results_loader.dart +++ b/frontend/lib/search/results_loader.dart @@ -3,6 +3,7 @@ import 'package:ehrenamtskarte/graphql/graphql_api.dart'; import 'package:ehrenamtskarte/map/preview/models.dart'; import 'package:ehrenamtskarte/store_widgets/accepting_store_summary.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; @@ -74,7 +75,7 @@ class ResultsLoaderState extends State { if (widget != oldWidget) { // Params are outdated. - // If we"re still at the first key, we must manually retrigger fetching. + // If we're still at the first key, we must manually retrigger fetching. if (pageKey == _pagingController.firstPageKey) { return await _fetchPage(pageKey); } @@ -96,7 +97,7 @@ class ResultsLoaderState extends State { } on Exception catch (error) { if (widget != oldWidget) { // Params are outdated. - // If we"re still at the first key, we must manually retrigger fetching. + // If we're still at the first key, we must manually retrigger fetching. if (pageKey == _pagingController.firstPageKey) { return _fetchPage(pageKey); } @@ -146,10 +147,10 @@ class ResultsLoaderState extends State { mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.warning, size: 60, color: Colors.orange), - const Text('Bitte Internetverbindung prüfen.'), + I18nText('checkConnection'), OutlinedButton( onPressed: _pagingController.retryLastFailedRequest, - child: const Text('Erneut versuchen'), + child: I18nText('tryAgain'), ) ], ), @@ -160,7 +161,7 @@ class ResultsLoaderState extends State { mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.search_off, size: 60, color: Theme.of(context).disabledColor), - const Text('Auf diese Suche trifft keine Akzeptanzstelle zu.'), + I18nText('noAcceptingStoresFound'), ], ), ); diff --git a/frontend/lib/search/search_page.dart b/frontend/lib/search/search_page.dart index f436cc593..d8ca4eb10 100644 --- a/frontend/lib/search/search_page.dart +++ b/frontend/lib/search/search_page.dart @@ -6,6 +6,8 @@ import 'package:ehrenamtskarte/search/results_loader.dart'; import 'package:ehrenamtskarte/widgets/app_bars.dart'; import 'package:flutter/material.dart'; +import '../util/i18n.dart'; + class SearchPage extends StatefulWidget { const SearchPage({super.key}); @@ -37,7 +39,7 @@ class _SearchPageState extends State { child: Row( children: [ Text( - 'Suchresultate'.toUpperCase(), + t(context, 'searchResults').toUpperCase(), style: const TextStyle(color: Colors.grey), ), const Expanded(child: Padding(padding: EdgeInsets.only(left: 8), child: Divider())) diff --git a/frontend/lib/store_widgets/accepting_store_summary.dart b/frontend/lib/store_widgets/accepting_store_summary.dart index c1a68da94..a3a019984 100644 --- a/frontend/lib/store_widgets/accepting_store_summary.dart +++ b/frontend/lib/store_widgets/accepting_store_summary.dart @@ -9,6 +9,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; +import '../util/i18n.dart'; + class AcceptingStoreSummary extends StatelessWidget { final AcceptingStoreSummaryModel store; final CoordinatesInput? coordinates; @@ -37,7 +39,7 @@ class AcceptingStoreSummary extends StatelessWidget { @override Widget build(BuildContext context) { final itemCategoryAsset = store.categoryId < categoryAssets.length ? categoryAssets[store.categoryId] : null; - final categoryName = itemCategoryAsset?.name ?? 'Unbekannte Kategorie'; + final categoryName = itemCategoryAsset?.name ?? t(context, 'unknownCategory'); final categoryColor = itemCategoryAsset?.color; final useWideDepiction = MediaQuery.of(context).size.width > 400; @@ -152,14 +154,14 @@ class StoreTextOverview extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - store.name ?? 'Akzeptanzstelle', + store.name ?? t(context, 'acceptingStore'), maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 4), Text( - store.description ?? 'Keine Beschreibung verfügbar', + store.description ?? t(context, 'noDescriptionAvailable'), maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium, @@ -180,9 +182,9 @@ class DistanceText extends StatelessWidget { if (d < 1) { return '${(d * 100).round() * 10} m'; } else if (d < 10) { - return "${NumberFormat("##.0", "de").format(d)} km"; + return '${NumberFormat('##.0', 'de').format(d)} km'; } else { - return "${NumberFormat("###,###", "de").format(d)} km"; + return '${NumberFormat('###,###', 'de').format(d)} km'; } } diff --git a/frontend/lib/store_widgets/detail/detail_app_bar.dart b/frontend/lib/store_widgets/detail/detail_app_bar.dart index 58a32ab01..990003ed4 100644 --- a/frontend/lib/store_widgets/detail/detail_app_bar.dart +++ b/frontend/lib/store_widgets/detail/detail_app_bar.dart @@ -6,6 +6,8 @@ import 'package:ehrenamtskarte/widgets/app_bars.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import '../../util/i18n.dart'; + const double bottomSize = 100; class DetailAppBarHeaderImage extends StatelessWidget { @@ -23,7 +25,7 @@ class DetailAppBarHeaderImage extends StatelessWidget { return SvgPicture.asset( currentDetailIcon, width: double.infinity, - semanticsLabel: "Header", + semanticsLabel: 'Header', alignment: Alignment.bottomRight, ); } @@ -87,7 +89,7 @@ class DetailAppBar extends StatelessWidget { final accentColor = getDarkenedColorForCategory(categoryId); final categoryName = matchingStore.store.category.name; - final title = matchingStore.store.name ?? 'Akzeptanzstelle'; + final title = matchingStore.store.name ?? t(context, 'acceptingStore'); final backgroundColor = accentColor ?? Theme.of(context).colorScheme.primary; final textColor = getReadableOnColor(backgroundColor); diff --git a/frontend/lib/store_widgets/detail/detail_content.dart b/frontend/lib/store_widgets/detail/detail_content.dart index 4694bb30d..59e864f57 100644 --- a/frontend/lib/store_widgets/detail/detail_content.dart +++ b/frontend/lib/store_widgets/detail/detail_content.dart @@ -7,10 +7,13 @@ import 'package:ehrenamtskarte/store_widgets/detail/contact_info_row.dart'; import 'package:ehrenamtskarte/util/color_utils.dart'; import 'package:ehrenamtskarte/util/sanitize_contact_details.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:maplibre_gl/mapbox_gl.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import '../../util/i18n.dart'; + class DetailContent extends StatelessWidget { final AcceptingStoreById$Query$PhysicalStore acceptingStore; final bool hideShowOnMapButton; @@ -30,8 +33,8 @@ class DetailContent extends StatelessWidget { final address = acceptingStore.address; final street = address.street; final location = '${address.postalCode} ${address.location}'; - final addressString = "${street != null ? "$street\n" : ""}$location"; - final mapQueryString = "${street != null ? "$street, " : ""}$location"; + final addressString = '${street != null ? '$street\n' : ''}$location'; + final mapQueryString = '${street != null ? '$street, ' : ''}$location'; final contact = acceptingStore.store.contact; final currentAccentColor = accentColor; @@ -59,7 +62,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.location_on, addressString, - 'Adresse', + t(context, 'address'), onTap: () => _launchMap(mapQueryString), iconColor: readableOnAccentColor, iconFillColor: accentColor, @@ -68,7 +71,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.language, prepareWebsiteUrlForDisplay(website), - 'Website', + t(context, 'website'), onTap: () => launchUrlString(prepareWebsiteUrlForLaunch(website), mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, @@ -78,7 +81,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.phone, telephone, - 'Telefon', + t(context, 'phone'), onTap: () => launchUrlString('tel:${sanitizePhoneNumber(telephone)}', mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, @@ -88,7 +91,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.alternate_email, email, - 'E-Mail', + t(context, 'email'), onTap: () => launchUrlString('mailto:${email.trim()}', mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, iconFillColor: accentColor, @@ -105,7 +108,7 @@ class DetailContent extends StatelessWidget { alignment: MainAxisAlignment.center, children: [ OutlinedButton( - child: const Text('Auf Karte zeigen'), + child: I18nText('showOnMap'), onPressed: () => _showOnMap(context), ), ], @@ -119,11 +122,11 @@ class DetailContent extends StatelessWidget { Future _launchMap(String query) async { if (Platform.isAndroid) { - await launchUrl(Uri(scheme: "geo", host: "0,0", queryParameters: {"q": query})); + await launchUrl(Uri(scheme: 'geo', host: '0,0', queryParameters: {'q': query})); } else if (Platform.isIOS) { - await launchUrl(Uri.https("maps.apple.com", "/", {"q": query})); + await launchUrl(Uri.https('maps.apple.com', '/', {'q': query})); } else { - await launchUrl(Uri.https("www.google.com", "/maps/search/", {"api": "1", "query": query})); + await launchUrl(Uri.https('www.google.com', '/maps/search/', {'api': '1', 'query': query})); } } diff --git a/frontend/lib/store_widgets/detail/detail_page.dart b/frontend/lib/store_widgets/detail/detail_page.dart index f54825cec..f11199080 100644 --- a/frontend/lib/store_widgets/detail/detail_page.dart +++ b/frontend/lib/store_widgets/detail/detail_page.dart @@ -9,6 +9,8 @@ import 'package:ehrenamtskarte/widgets/top_loading_spinner.dart'; import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; +import '../../util/i18n.dart'; + class DetailPage extends StatelessWidget { final int _acceptingStoreId; final bool hideShowOnMapButton; @@ -27,15 +29,15 @@ class DetailPage extends StatelessWidget { final data = result.data; if (result.hasException && exception != null) { - return DetailErrorMessage(message: 'Fehler beim Laden der Daten', refetch: refetch); + return DetailErrorMessage(message: t(context, 'loadingDataFailed'), refetch: refetch); } else if (result.isNotLoading && data != null) { final matchingStores = byIdQuery.parse(data).physicalStoresByIdInProject; if (matchingStores.length != 1) { - return DetailErrorMessage(message: 'Fehler beim Laden der Daten.', refetch: refetch); + return DetailErrorMessage(message: t(context, 'loadingDataFailed'), refetch: refetch); } final matchingStore = matchingStores.first; if (matchingStore == null) { - return const DetailErrorMessage(message: 'Akzeptanzstelle nicht gefunden.'); + return const DetailErrorMessage(message: t(context, 'acceptingStoreNotFound')); } final categoryId = matchingStore.store.category.id; final accentColor = getDarkenedColorForCategory(categoryId); diff --git a/frontend/lib/util/json_canonicalizer.dart b/frontend/lib/util/json_canonicalizer.dart index 2bbe24117..353e9bf4c 100644 --- a/frontend/lib/util/json_canonicalizer.dart +++ b/frontend/lib/util/json_canonicalizer.dart @@ -49,7 +49,7 @@ class JsonCanonicalizer { } stringBuffer.write('}'); } else { - throw ArgumentError("Could not serialize value '$jsonObject'!"); + throw ArgumentError('Could not serialize value "$jsonObject"!'); } } } diff --git a/frontend/lib/widgets/app_bars.dart b/frontend/lib/widgets/app_bars.dart index 90bb09ac3..36d3fbd04 100644 --- a/frontend/lib/widgets/app_bars.dart +++ b/frontend/lib/widgets/app_bars.dart @@ -5,6 +5,8 @@ library navigation_bars; import 'package:ehrenamtskarte/debouncer.dart'; import 'package:flutter/material.dart'; +import '../util/i18n.dart'; + class CustomAppBar extends StatelessWidget { final String title; final List? actions; @@ -91,7 +93,7 @@ class SearchSliverAppBarState extends State { controller: textEditingController, focusNode: focusNode, decoration: InputDecoration.collapsed( - hintText: 'Tippen, um zu suchen …', + hintText: t(context, 'searchHint'), hintStyle: TextStyle(color: foregroundColor?.withOpacity(0.8)), ), cursorColor: foregroundColor, From 06324d4a4ba2c36e70d61ec87694c8f5e35d1468 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Sat, 26 Aug 2023 19:07:55 +0200 Subject: [PATCH 28/60] 904: Add more translations --- frontend/assets/bayern/translations/de.json | 23 +++++++++++- frontend/lib/home/home_page.dart | 2 +- .../activation_code_scanner_page.dart | 36 ++++++++----------- .../activation_exception.dart | 8 +---- .../activation_overwrite_existing_dialog.dart | 15 ++++---- .../identification/id_card/card_content.dart | 6 ++-- .../negative_verification_result_dialog.dart | 4 ++- .../positive_verification_result_dialog.dart | 7 ++-- .../dialogs/verification_info_dialog.dart | 21 +++++------ .../query_server_verification.dart | 4 +-- .../verification_qr_code_processor.dart | 14 ++++---- .../verification_qr_scanner_page.dart | 29 +++++++-------- frontend/lib/location/determine_position.dart | 7 ++-- frontend/lib/map/map/attribution_dialog.dart | 14 ++++---- .../preview/accepting_store_preview_card.dart | 2 +- .../lib/store_widgets/detail/detail_page.dart | 2 +- 16 files changed, 104 insertions(+), 90 deletions(-) diff --git a/frontend/assets/bayern/translations/de.json b/frontend/assets/bayern/translations/de.json index ad79a190c..57a684d6e 100644 --- a/frontend/assets/bayern/translations/de.json +++ b/frontend/assets/bayern/translations/de.json @@ -24,14 +24,17 @@ "settings": "Einstellungen", "openSettings": "Einstellungen öffnen", "moreActions": "Weitere Aktionen", + "activate": "Aktivieren", "card": "Karte", "search": "Suche", - "identification": "Ausweisen", + "authentication": "Ausweisen", "about": "Über", + "stopShowing": "Nicht mehr anzeigen", "filterByCategories": "Nach Kategorien filtern", "findCloseBy": "In meiner Nähe suchen", "activateLocationAccess": "Standortermittlung aktivieren", "activateLocationAccessSettings": "Aktivieren Sie die Standortermittlung in den Einstellungen.", + "activateLocationAccessRationale": "Erlauben Sie der App Ihren Standort zu benutzen, um Akzeptanzstellen in Ihrer Umgebung anzuzeigen.", "grantLocation": "Ich möchte meinen Standort freigeben.", "locationGranted": "Standort ist freigegeben.", "locationDeactivated": "Standortfreigabe ist deaktiviert.", @@ -54,7 +57,15 @@ "cardExpired": "Ihre Karte ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", "cardInvalid": "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", "cardNotYetValid": "Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen.", + "cardAlreadyActivated": "Der eingescannte QRCode wurde bereits aktiviert.", "checkFailed": "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Verbindung mit dem Internet besteht und prüfen Sie erneut.", + "codeVerificationFailed": "Der eingescannte Code konnte vom Server nicht verifiziert werden!", + "codeVerificationFailedConnection": "Der eingescannte Code konnte nicht verifiziert werden, da die Kommunikation mit dem Server fehlschlug. Bitte prüfen Sie Ihre Internetverbindung.", + "codeInvalidMissing": "Der Inhalt des eingescannten Codes ist unvollständig. (Fehlercode: {missing}Missing)", + "codeExpired": "Der eingescannte Code ist bereits am {expirationDate} abgelaufen.", + "codeInvalid": "Der Inhalt des eingescannten Codes kann nicht verstanden werden. Vermutlich handelt es sich um einen QR-Code, der nicht für diese App generiert wurde.", + "codeSavingFailed": "Der eingescannte Code kann nicht in der App gespeichert werden.", + "codeUnknownError": "Beim Einlesen des QR-Codes ist ein unbekannter Fehler aufgetreten.", "checkAgain": "Erneut prüfen", "timeIncorrect": "Die Uhrzeit Ihres Geräts scheint nicht zu stimmen. Bitte synchronisieren Sie die Uhrzeit in den Systemeinstellungen.", "authenticationPossible": "Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:", @@ -74,6 +85,16 @@ "showMapCopyright": "Zeige Infos über das Urheberrecht der Kartendaten", "mapData": "Kartendaten", "osmContributors": "OpenStreetMap Mitwirkende", + "internetRequired": "Eine Internetverbindung wird benötigt.", + "compareWithID": "Gleichen Sie die angezeigten Daten mit einem amtlichen Lichtbildausweis ab.", + "comparedWithID": "Ich habe die Daten mit einem amtlichen Lichtbildausweis abgeglichen.", + "checkingCode": "Der QR-Code wird durch eine Server-Anfrage geprüft.", + "scanCode": "Scannen Sie den QR-Code, der auf dem \"Ausweisen\"-Tab Ihres Gegenübers angezeigt wird.", + "checkRequired": "Prüfung nötig", + "notVerified": "Nicht verifiziert", + "activateCardCurrentDevice": "Karte auf diesem Gerät aktivieren?", + "activateCardCurrentDeviceRationale": "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", + "activationInvalidState": "Die Aktivierung befindet sich in einem ungültigen Zustand.", "categories": { "mobilityLong": "Auto/Zweirad", "mobility": "Mobilität", diff --git a/frontend/lib/home/home_page.dart b/frontend/lib/home/home_page.dart index a17ae1576..281ae01b1 100644 --- a/frontend/lib/home/home_page.dart +++ b/frontend/lib/home/home_page.dart @@ -52,7 +52,7 @@ class HomePageState extends State { AppFlow( IdentificationPage(), Icons.remove_red_eye_outlined, - t(context, 'identification'), + t(context, 'authentication'), GlobalKey(debugLabel: 'Auth tab key'), ), AppFlow(const AboutPage(), Icons.info_outline, t(context, 'about'), GlobalKey(debugLabel: 'About tab key')), diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index f410c4439..7270abf57 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -24,6 +24,8 @@ import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import '../../util/i18n.dart'; + class ActivationCodeScannerPage extends StatelessWidget { const ActivationCodeScannerPage({super.key}); @@ -50,32 +52,26 @@ class ActivationCodeScannerPage extends StatelessWidget { final activationCode = const ActivationCodeParser().parseQrCodeContent(code); await _activateCode(context, activationCode); - } on ActivationDidNotOverwriteExisting catch (e) { - await showError(e.toString(), null); + } on ActivationDidNotOverwriteExisting catch (_) { + await showError(t(context, 'cardAlreadyActivated'), null); } on QrCodeFieldMissingException catch (e) { - await showError( - "Der Inhalt des eingescannten Codes ist unvollständig. " - "(Fehlercode: ${e.missingFieldName}Missing)", - null); + await showError(t(context, 'codeInvalidMissing', translationParams: { 'missing': e.missingFieldName })); } on QrCodeWrongTypeException catch (_) { - await showError("Der eingescannte Code kann nicht in der App gespeichert werden."); + await showError(t(context, 'codeSavingFailed')); } on CardExpiredException catch (e) { - final dateFormat = DateFormat("dd.MM.yyyy"); - await showError("Der eingescannte Code ist bereits am " - "${dateFormat.format(e.expiry)} abgelaufen."); + final expirationDate = DateFormat('dd.MM.yyyy').format(e.expiry); + await showError(t(context, 'codeExpired', translationParams: { 'expirationDate': expirationDate })); } on ServerCardActivationException catch (e, stackTrace) { String errorMessage = 'Der eingescannte Code konnte nicht aktiviert ' 'werden, da die Kommunikation mit dem Server fehlschlug. ' 'Bitte prüfen Sie Ihre Internetverbindung.'; await ConnectionFailedDialog.show( context, - "Der eingescannte Code konnte nicht aktiviert " - "werden, da die Kommunikation mit dem Server fehlschlug. " - "Bitte prüfen Sie Ihre Internetverbindung.", + t(context, 'codeVerificationFailedConnection') ); } on Exception catch (e, stacktrace) { debugPrintStack(stackTrace: stacktrace, label: e.toString()); - await showError("Ein unerwarteter Fehler ist aufgetreten."); + await showError(t(context, 'codeUnknownError')); } } @@ -90,7 +86,7 @@ class ActivationCodeScannerPage extends StatelessWidget { final activationSecretBase64 = const Base64Encoder().convert(activationCode.activationSecret); final cardInfoBase64 = activationCode.info.hash(activationCode.pepper); - debugPrint("Card Activation: Sending request with overwriteExisting=$overwriteExisting."); + debugPrint('Card Activation: Sending request with overwriteExisting=$overwriteExisting.'); final activationResult = await activateCode( client: client, @@ -107,7 +103,7 @@ class ActivationCodeScannerPage extends StatelessWidget { throw const ActivationInvalidTotpSecretException(); } final totpSecret = const Base64Decoder().convert(activationResult.totpSecret!); - debugPrint("Card Activation: Successfully activated."); + debugPrint('Card Activation: Successfully activated.'); provider.setCode(DynamicUserCode() ..info = activationCode.info @@ -120,7 +116,7 @@ class ActivationCodeScannerPage extends StatelessWidget { case ActivationState.failed: await QrParsingErrorDialog.showErrorDialog( context, - "Der eingescannte Code ist ungültig.", + t(context, 'codeInvalid'), ); break; case ActivationState.didNotOverwriteExisting: @@ -128,15 +124,13 @@ class ActivationCodeScannerPage extends StatelessWidget { throw const ActivationDidNotOverwriteExisting(); } debugPrint( - "Card Activation: Card had been activated already and was not overwritten. Waiting for user feedback."); + 'Card Activation: Card had been activated already and was not overwritten. Waiting for user feedback.'); if (await ActivationOverwriteExistingDialog.showActivationOverwriteExistingDialog(context)) { await _activateCode(context, activationCode, overwriteExisting = true); } break; default: - throw const ServerCardActivationException( - "Die Aktivierung befindet sich in einem ungültigen Zustand.", - ); + throw ServerCardActivationException(t(context, 'activationInvalidState')); } } } diff --git a/frontend/lib/identification/activation_workflow/activation_exception.dart b/frontend/lib/identification/activation_workflow/activation_exception.dart index 7565da1fb..bd28d96b7 100644 --- a/frontend/lib/identification/activation_workflow/activation_exception.dart +++ b/frontend/lib/identification/activation_workflow/activation_exception.dart @@ -1,15 +1,9 @@ import 'package:ehrenamtskarte/identification/activation_workflow/activate_code.dart'; class ActivationInvalidTotpSecretException extends ServerCardActivationException { - const ActivationInvalidTotpSecretException() - : super("Der Server konnte kein TotpSecret für den eingescannten QRCode generieren."); + const ActivationInvalidTotpSecretException() : super('Server failed to create totp secret for the scanned code.'); } class ActivationDidNotOverwriteExisting implements Exception { const ActivationDidNotOverwriteExisting() : super(); - - @override - String toString() { - return "Der eingescannte QRCode wurde bereits aktiviert."; - } } diff --git a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart index 9c9747bd2..f32326e2f 100644 --- a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart +++ b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; + +import '../../util/i18n.dart'; class ActivationOverwriteExistingDialog extends StatelessWidget { const ActivationOverwriteExistingDialog({super.key}); @@ -6,25 +9,23 @@ class ActivationOverwriteExistingDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text("Karte auf diesem Gerät aktivieren?", style: TextStyle(fontSize: 18)), + title: Text(t(context, 'activateCardCurrentDevice'), style: TextStyle(fontSize: 18)), content: SingleChildScrollView( child: ListBody( - children: const [ - Text( - "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", - ), + children: [ + I18nText('activateCardCurrentDeviceRationale'), ], ), ), actions: [ TextButton( - child: const Text("Abbrechen"), + child: I18nText('cancel'), onPressed: () { Navigator.of(context).pop(false); }, ), TextButton( - child: const Text("Aktivieren"), + child: I18nText('activate'), onPressed: () { Navigator.of(context).pop(true); }, diff --git a/frontend/lib/identification/id_card/card_content.dart b/frontend/lib/identification/id_card/card_content.dart index 5e8b85ff9..ee90632d1 100644 --- a/frontend/lib/identification/id_card/card_content.dart +++ b/frontend/lib/identification/id_card/card_content.dart @@ -48,7 +48,7 @@ class CardContent extends StatelessWidget { const CardContent( {super.key, required this.cardInfo, this.region, required this.isExpired, required this.isNotYetValid}); - String get _formattedExpirationDate { + String _getFormattedExpirationDate(BuildContext context) { final expirationDay = cardInfo.hasExpirationDay() ? cardInfo.expirationDay : null; return expirationDay != null ? DateFormat('dd.MM.yyyy').format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: expirationDay))) @@ -75,7 +75,7 @@ class CardContent extends StatelessWidget { : null; } - String _getCardValidityDate(String? startDate, String expirationDate) { + String _getCardValidityDate(BuildContext context, String? startDate, String expirationDate) { return startDate != null ? t(context, 'validFromUntil', translationParams: {'startDate': startDate, 'expirationDate': expirationDate}) : t(context, 'validUntil', translationParams: { 'expirationDate': expirationDate }); @@ -213,7 +213,7 @@ class CardContent extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 3.0), child: Text( - _getCardValidityDate(startDate, _formattedExpirationDate), + _getCardValidityDate(context, startDate, _getFormattedExpirationDate(context)), style: TextStyle( fontSize: 14 * scaleFactor, color: diff --git a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart index ab080dcd0..c94a062c1 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart @@ -1,6 +1,8 @@ import 'package:ehrenamtskarte/identification/info_dialog.dart'; import 'package:flutter/material.dart'; +import '../../../util/i18n.dart'; + class NegativeVerificationResultDialog extends StatelessWidget { final String reason; @@ -9,7 +11,7 @@ class NegativeVerificationResultDialog extends StatelessWidget { @override Widget build(BuildContext context) { return InfoDialog( - title: "Nicht verifiziert", + title: t(context, 'notVerified'), icon: Icons.error, iconColor: Colors.red, child: Text(reason), diff --git a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart index 15bd6196e..7dc7bcce7 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart @@ -5,8 +5,11 @@ import 'package:ehrenamtskarte/identification/id_card/id_card.dart'; import 'package:ehrenamtskarte/identification/info_dialog.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; +import '../../../util/i18n.dart'; + class PositiveVerificationResultDialog extends StatefulWidget { final CardInfo cardInfo; final bool isStaticVerificationCode; @@ -55,7 +58,7 @@ class PositiveVerificationResultDialogState extends State _onDone(context), ) ], @@ -78,7 +79,7 @@ class _EnumeratedListItem extends StatelessWidget { CircleAvatar( backgroundColor: Theme.of(context).colorScheme.primary, child: Text( - "${index + 1}", + '${index + 1}', style: TextStyle( color: Theme.of(context).colorScheme.background, fontWeight: FontWeight.bold, diff --git a/frontend/lib/identification/verification_workflow/query_server_verification.dart b/frontend/lib/identification/verification_workflow/query_server_verification.dart index e000cfc06..5b17d7222 100644 --- a/frontend/lib/identification/verification_workflow/query_server_verification.dart +++ b/frontend/lib/identification/verification_workflow/query_server_verification.dart @@ -79,7 +79,7 @@ Future _queryServerVer final data = queryResult.data; if (data == null) { - throw ServerVerificationException("Returned data is null."); + throw ServerVerificationException('Returned data is null.'); } final parsedResult = byCardDetailsHash.parse(data); @@ -96,6 +96,6 @@ class ServerVerificationException implements Exception { @override String toString() { - return "ServerVerificationException{cause: $cause}"; + return 'ServerVerificationException{cause: $cause}'; } } diff --git a/frontend/lib/identification/verification_workflow/verification_qr_code_processor.dart b/frontend/lib/identification/verification_workflow/verification_qr_code_processor.dart index 6613c6918..47c251578 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_code_processor.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_code_processor.dart @@ -33,8 +33,8 @@ Future verifyDynamicVerificationCode( _assertConsistentDynamicVerificationCode(code); final (outOfSync: outOfSync, result: result) = await queryDynamicServerVerification(client, projectId, code); if (outOfSync) { - debugPrint("Verification: This device"s time is out of sync with the server." - "Ignoring, as only the time of the device that generates the QR code is relevant for the verification process."); + debugPrint('Verification: This device\'s time is out of sync with the server.' + 'Ignoring, as only the time of the device that generates the QR code is relevant for the verification process.'); } if (!result.valid) { @@ -58,7 +58,7 @@ Future verifyStaticVerificationCode( void assertConsistentCardInfo(CardInfo cardInfo) { if (!cardInfo.hasFullName()) { - throw QrCodeFieldMissingException("fullName"); + throw QrCodeFieldMissingException('fullName'); } if (!cardInfo.hasExpirationDay() && cardInfo.extensions.extensionBavariaCardType.cardType != BavariaCardType.GOLD) { throw QRCodeMissingExpiryException(); @@ -75,16 +75,16 @@ void assertConsistentCardInfo(CardInfo cardInfo) { void _assertConsistentDynamicVerificationCode(DynamicVerificationCode verificationCode) { if (!verificationCode.hasPepper()) { - throw QrCodeFieldMissingException("pepper"); + throw QrCodeFieldMissingException('pepper'); } if (verificationCode.otp <= 0) { - throw QrCodeFieldMissingException("otp"); + throw QrCodeFieldMissingException('otp'); } } void _assertConsistentStaticVerificationCode(StaticVerificationCode verificationCode) { if (!verificationCode.hasPepper()) { - throw QrCodeFieldMissingException("pepper"); + throw QrCodeFieldMissingException('pepper'); } } @@ -98,5 +98,5 @@ extension DateComparison on DateTime { class CardExpiredException extends QrCodeParseException { final DateTime expiry; - CardExpiredException(this.expiry) : super("card already expired"); + CardExpiredException(this.expiry) : super('card already expired'); } diff --git a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart index e37a85b28..886bc2951 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart @@ -20,6 +20,8 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import '../../util/i18n.dart'; + class VerificationQrScannerPage extends StatelessWidget { const VerificationQrScannerPage({super.key}); @@ -61,7 +63,7 @@ class VerificationQrScannerPage extends StatelessWidget { final verificationCode = verificationQrCode.writeToBuffer(); _handleQrCode(context, verificationCode); }, - child: const Text("Verify activated Card"), + child: const Text('Verify activated Card'), ) ], ); @@ -75,7 +77,7 @@ class VerificationQrScannerPage extends StatelessWidget { if (cardInfo == null) { await _onError( context, - "Der eingescannte Code konnte vom Server nicht verifiziert werden!", + t(context, 'codeVerificationFailed'), ); } else { await _onSuccess(context, cardInfo, qrcode.hasStaticVerificationCode()); @@ -83,55 +85,50 @@ class VerificationQrScannerPage extends StatelessWidget { } on ServerVerificationException catch (e) { await _onConnectionError( context, - "Der eingescannte Code konnte nicht verifiziert " - "werden, da die Kommunikation mit dem Server fehlschlug. " - "Bitte prüfen Sie Ihre Internetverbindung.", + t(context, 'codeVerificationFailedConnection'), e, ); } on QrCodeFieldMissingException catch (e) { await _onError( context, - "Der eingescannte Code ist nicht gültig, " - "da erforderliche Daten fehlen.", + t(context, 'codeInvalidMissing'), e, ); } on CardExpiredException catch (e) { - final dateFormat = DateFormat("dd.MM.yyyy"); + final expirationDate = DateFormat('dd.MM.yyyy').format(e.expiry); await _onError( context, - "Der eingescannte Code ist bereits am ${dateFormat.format(e.expiry)} abgelaufen.", + t(context, 'codeExpired', translationParams: {'expirationDate': expirationDate}), e, ); } on QrCodeParseException catch (e) { await _onError( context, - "Der Inhalt des eingescannten Codes kann nicht verstanden " - "werden. Vermutlich handelt es sich um einen QR-Code, der nicht für " - "diese App generiert wurde.", + t(context, 'codeInvalid'), e, ); } on Exception catch (e) { await _onError( context, - "Beim Einlesen des QR-Codes ist ein unbekannter Fehler aufgetreten.", + t(context, 'codeUnknownError'), e, ); } finally { - // close current "Karte verifizieren" view + // close current 'Karte verifizieren' view await Navigator.of(context).maybePop(); } } Future _onError(BuildContext context, String message, [Exception? exception]) async { if (exception != null) { - debugPrint("Verification failed: $exception"); + debugPrint('Verification failed: $exception'); } await NegativeVerificationResultDialog.show(context, message); } Future _onConnectionError(BuildContext context, String message, [Exception? exception]) async { if (exception != null) { - debugPrint("Connection failed: $exception"); + debugPrint('Connection failed: $exception'); } await ConnectionFailedDialog.show(context, message); } diff --git a/frontend/lib/location/determine_position.dart b/frontend/lib/location/determine_position.dart index 72e4c555c..8c8ba75ed 100644 --- a/frontend/lib/location/determine_position.dart +++ b/frontend/lib/location/determine_position.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:maplibre_gl/mapbox_gl.dart'; +import '../util/i18n.dart'; + enum LocationStatus { /// This is the initial state on both Android and iOS, but on Android the /// user can still choose to deny permissions, meaning the App can still @@ -101,8 +103,7 @@ Future determinePosition( Future checkAndRequestLocationPermission( BuildContext context, { bool requestIfNotGranted = true, - String rationale = - "Erlauben Sie der App Ihren Standort zu benutzen, um Akzeptanzstellen in Ihrer Umgebung anzuzeigen.", + String? rationale, Future Function()? onDisableFeature, Future Function()? onEnableFeature, }) async { @@ -144,7 +145,7 @@ Future checkAndRequestLocationPermission( // returned true. According to Android guidelines // your App should show an explanatory UI now. - final result = await showDialog(context: context, builder: (context) => RationaleDialog(rationale: rationale)); + final result = await showDialog(context: context, builder: (context) => RationaleDialog(rationale: rationale ?? t(context, 'activateLocationAccessRationale'))); if (result == true) { return checkAndRequestLocationPermission( diff --git a/frontend/lib/map/map/attribution_dialog.dart b/frontend/lib/map/map/attribution_dialog.dart index 6442f488c..e6a8af023 100644 --- a/frontend/lib/map/map/attribution_dialog.dart +++ b/frontend/lib/map/map/attribution_dialog.dart @@ -19,31 +19,31 @@ class AttributionDialog extends StatelessWidget { color: color, text: t(context, 'osmContributors'), onPressed: () { - launchUrlString("https://www.openstreetmap.org/copyright", mode: LaunchMode.externalApplication); + launchUrlString('https://www.openstreetmap.org/copyright', mode: LaunchMode.externalApplication); }, ), AttributionDialogItem( icon: Icons.copyright, color: color, - text: "OpenMapTiles", + text: 'OpenMapTiles', onPressed: () { - launchUrlString("https://openmaptiles.org/", mode: LaunchMode.externalApplication); + launchUrlString('https://openmaptiles.org/', mode: LaunchMode.externalApplication); }, ), AttributionDialogItem( icon: Icons.copyright, color: color, - text: "Natural Earth", + text: 'Natural Earth', onPressed: () { - launchUrlString("https://naturalearthdata.com/", mode: LaunchMode.externalApplication); + launchUrlString('https://naturalearthdata.com/', mode: LaunchMode.externalApplication); }, ), AttributionDialogItem( icon: Icons.copyright, color: color, - text: "LBE Bayern", + text: 'LBE Bayern', onPressed: () { - launchUrlString("https://www.lbe.bayern.de/", mode: LaunchMode.externalApplication); + launchUrlString('https://www.lbe.bayern.de/', mode: LaunchMode.externalApplication); }, ) ], diff --git a/frontend/lib/map/preview/accepting_store_preview_card.dart b/frontend/lib/map/preview/accepting_store_preview_card.dart index ad704d509..1b5d42025 100644 --- a/frontend/lib/map/preview/accepting_store_preview_card.dart +++ b/frontend/lib/map/preview/accepting_store_preview_card.dart @@ -18,7 +18,7 @@ class AcceptingStorePreviewError extends StatelessWidget { child: Container( height: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16), - child: const ErrorMessage(t(context, 'loadingInformationFailed')), + child: ErrorMessage(t(context, 'loadingInformationFailed')), ), ); } diff --git a/frontend/lib/store_widgets/detail/detail_page.dart b/frontend/lib/store_widgets/detail/detail_page.dart index f11199080..ea3ba6b29 100644 --- a/frontend/lib/store_widgets/detail/detail_page.dart +++ b/frontend/lib/store_widgets/detail/detail_page.dart @@ -37,7 +37,7 @@ class DetailPage extends StatelessWidget { } final matchingStore = matchingStores.first; if (matchingStore == null) { - return const DetailErrorMessage(message: t(context, 'acceptingStoreNotFound')); + return DetailErrorMessage(message: t(context, 'acceptingStoreNotFound')); } final categoryId = matchingStore.store.category.id; final accentColor = getDarkenedColorForCategory(categoryId); From 1043553953f3aa9ee9905f4790cd7dd6dd1fa649 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 28 Aug 2023 10:38:51 +0200 Subject: [PATCH 29/60] 904: Remove english locale for now --- frontend/assets/bayern/translations/en.json | 10 ---------- frontend/lib/app.dart | 4 ++-- 2 files changed, 2 insertions(+), 12 deletions(-) delete mode 100644 frontend/assets/bayern/translations/en.json diff --git a/frontend/assets/bayern/translations/en.json b/frontend/assets/bayern/translations/en.json deleted file mode 100644 index 504d7da7f..000000000 --- a/frontend/assets/bayern/translations/en.json +++ /dev/null @@ -1,10 +0,0 @@ -{ -"publisher": "Publisher", -"moreInformation": "More Information", -"licenses-0": "Licenses", -"licenses-1": "License", -"licenses-2": "Licenses", -"number-licenses-0": "{number} Licenses", -"number-licenses-1": "{number} License", -"number-licenses-2": "{number} Licenses" -} diff --git a/frontend/lib/app.dart b/frontend/lib/app.dart index 64f324888..2feecef03 100644 --- a/frontend/lib/app.dart +++ b/frontend/lib/app.dart @@ -69,7 +69,7 @@ class App extends StatelessWidget { debugShowCheckedModeBanner: false, localizationsDelegates: [ FlutterI18nDelegate( - translationLoader: FileTranslationLoader(useCountryCode: false, fallbackFile: 'en', basePath: 'assets/bayern/translations'), + translationLoader: FileTranslationLoader(useCountryCode: false, fallbackFile: 'de', basePath: 'assets/bayern/translations'), missingTranslationHandler: (key, locale) => print('--- Missing Key: $key, languageCode: ${locale?.languageCode}'), ), @@ -77,7 +77,7 @@ class App extends StatelessWidget { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: const [Locale('de'), Locale('en')], + supportedLocales: const [Locale('de')], initialRoute: initialRoute, routes: routes, ), From e81c45b1bb0f7649674d7440a3c266d189da12ed Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 16 Oct 2023 16:33:07 +0200 Subject: [PATCH 30/60] 904: Use built in flutter localization --- frontend/assets/bayern/translations/de.json | 130 ----------------- frontend/l10n.yaml | 3 + frontend/lib/about/about_page.dart | 27 ++-- frontend/lib/about/license_page.dart | 5 +- frontend/lib/app.dart | 8 +- frontend/lib/home/home_page.dart | 9 +- .../activation_code_scanner_page.dart | 24 ++-- .../activation_overwrite_existing_dialog.dart | 9 +- .../card_detail_view/card_detail_view.dart | 19 ++- .../card_detail_view/more_actions_dialog.dart | 7 +- .../connection_failed_dialog.dart | 2 +- .../identification/id_card/card_content.dart | 6 +- frontend/lib/identification/info_dialog.dart | 5 +- .../qr_code_camera_permission_dialog.dart | 9 +- .../qr_code_scanner/qr_code_scanner.dart | 5 +- .../qr_code_scanner_controls.dart | 4 +- .../qr_parsing_error_dialog.dart | 7 +- .../negative_verification_result_dialog.dart | 2 +- .../positive_verification_result_dialog.dart | 5 +- .../dialogs/verification_info_dialog.dart | 13 +- .../verification_qr_scanner_page.dart | 12 +- frontend/lib/intro_slides/intro_screen.dart | 9 +- .../intro_slides/location_request_button.dart | 11 +- frontend/lib/l10n/app_de.arb | 133 ++++++++++++++++++ frontend/lib/l10n/app_en.arb | 133 ++++++++++++++++++ frontend/lib/location/determine_position.dart | 2 +- frontend/lib/location/dialogs.dart | 19 +-- frontend/lib/map/location_button.dart | 5 +- frontend/lib/map/map/attribution_dialog.dart | 5 +- frontend/lib/map/map/map.dart | 2 +- .../preview/accepting_store_preview_card.dart | 2 +- frontend/lib/search/filter_bar.dart | 2 +- frontend/lib/search/location_button.dart | 12 +- frontend/lib/search/results_loader.dart | 9 +- frontend/lib/search/search_page.dart | 2 +- .../accepting_store_summary.dart | 6 +- .../store_widgets/detail/detail_app_bar.dart | 2 +- .../store_widgets/detail/detail_content.dart | 11 +- .../lib/store_widgets/detail/detail_page.dart | 6 +- frontend/lib/util/i18n.dart | 8 +- frontend/lib/widgets/app_bars.dart | 2 +- frontend/pubspec.lock | 24 ---- frontend/pubspec.yaml | 6 +- 43 files changed, 414 insertions(+), 308 deletions(-) delete mode 100644 frontend/assets/bayern/translations/de.json create mode 100644 frontend/l10n.yaml create mode 100644 frontend/lib/l10n/app_de.arb create mode 100644 frontend/lib/l10n/app_en.arb diff --git a/frontend/assets/bayern/translations/de.json b/frontend/assets/bayern/translations/de.json deleted file mode 100644 index 57a684d6e..000000000 --- a/frontend/assets/bayern/translations/de.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "publisher": "Herausgeber", - "moreInformation": "Mehr Informationen", - "licenses": "Lizenzen", - "license": "Lizenz", - "numberLicenses0": "{number} Lizenzen", - "numberLicenses1": "{number} Lizenz", - "numberLicenses2": "{number} Lizenzen", - "privacyDeclaration": "Datenschutzerklärung", - "disclaimer": "Haftung, Haftungsausschluss und Impressum", - "dependencies": "Software-Bibliotheken", - "sourceCode": "Quellcode der App", - "developmentOptions": "Entwickleroptionen", - "locationAccessDeactivated": "Die Standortfreigabe ist deaktiviert.", - "checkSettings": "Prüfe Einstellungen...", - "connectionFailed": "Keine Verbindung möglich", - "checkConnection": "Bitte Internetverbindung prüfen.", - "ok": "OK", - "done": "Fertig", - "next": "Weiter", - "previous": "Zurück", - "cancel": "Abbrechen", - "tryAgain": "Erneut versuchen", - "settings": "Einstellungen", - "openSettings": "Einstellungen öffnen", - "moreActions": "Weitere Aktionen", - "activate": "Aktivieren", - "card": "Karte", - "search": "Suche", - "authentication": "Ausweisen", - "about": "Über", - "stopShowing": "Nicht mehr anzeigen", - "filterByCategories": "Nach Kategorien filtern", - "findCloseBy": "In meiner Nähe suchen", - "activateLocationAccess": "Standortermittlung aktivieren", - "activateLocationAccessSettings": "Aktivieren Sie die Standortermittlung in den Einstellungen.", - "activateLocationAccessRationale": "Erlauben Sie der App Ihren Standort zu benutzen, um Akzeptanzstellen in Ihrer Umgebung anzuzeigen.", - "grantLocation": "Ich möchte meinen Standort freigeben.", - "locationGranted": "Standort ist freigegeben.", - "locationDeactivated": "Standortfreigabe ist deaktiviert.", - "locationPermission": "Standortberechtigung", - "grantPermission": "Berechtigung erteilen", - "askPermissionsAgain": "Soll nocheinmal nach der Berechtigung gefragt werden?", - "cameraAccessRequired": "Zugriff auf Kamera erforderlich", - "cameraAccessRequiredSettings": "Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera.\nIn den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.", - "selfieCamera": "Frontkamera", - "standardCamera": "Standard-Kamera", - "flashOn": "Blitz an", - "flashOff": "Blitz aus", - "noAcceptingStoresFound": "Auf diese Suche trifft keine Akzeptanzstelle zu.", - "acceptingStoreNotFound": "Akzeptanzstelle nicht gefunden.", - "searchResults": "Suchresultate", - "searchHint": "Tippen, um zu suchen …", - "unlimited": "unbegrenzt", - "validFromUntil": "Gültig: {startDate} bis {expirationDate}", - "validUntil": "Gültig bis: {expirationDate}", - "cardExpired": "Ihre Karte ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", - "cardInvalid": "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", - "cardNotYetValid": "Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen.", - "cardAlreadyActivated": "Der eingescannte QRCode wurde bereits aktiviert.", - "checkFailed": "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Verbindung mit dem Internet besteht und prüfen Sie erneut.", - "codeVerificationFailed": "Der eingescannte Code konnte vom Server nicht verifiziert werden!", - "codeVerificationFailedConnection": "Der eingescannte Code konnte nicht verifiziert werden, da die Kommunikation mit dem Server fehlschlug. Bitte prüfen Sie Ihre Internetverbindung.", - "codeInvalidMissing": "Der Inhalt des eingescannten Codes ist unvollständig. (Fehlercode: {missing}Missing)", - "codeExpired": "Der eingescannte Code ist bereits am {expirationDate} abgelaufen.", - "codeInvalid": "Der Inhalt des eingescannten Codes kann nicht verstanden werden. Vermutlich handelt es sich um einen QR-Code, der nicht für diese App generiert wurde.", - "codeSavingFailed": "Der eingescannte Code kann nicht in der App gespeichert werden.", - "codeUnknownError": "Beim Einlesen des QR-Codes ist ein unbekannter Fehler aufgetreten.", - "checkAgain": "Erneut prüfen", - "timeIncorrect": "Die Uhrzeit Ihres Geräts scheint nicht zu stimmen. Bitte synchronisieren Sie die Uhrzeit in den Systemeinstellungen.", - "authenticationPossible": "Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:", - "scanQRCode": "Halten Sie die Kamera auf den QR Code.", - "scanningFailed": "Fehler beim Lesen des Codes", - "generatingTotpSecretFailed": "", - "loadingDataFailed": "Fehler beim Laden der Daten.", - "loadingInformationFailed": "Fehler beim Laden der Infos.", - "showOnMap": "Auf Karte zeigen", - "email": "E-Mail", - "phone": "Telefon", - "website": "Website", - "address": "Adresse", - "acceptingStore": "Akzeptanzstelle", - "noDescriptionAvailable": "Keine Beschreibung verfügbar", - "unknownCategory": "Unbekannte Kategorie", - "showMapCopyright": "Zeige Infos über das Urheberrecht der Kartendaten", - "mapData": "Kartendaten", - "osmContributors": "OpenStreetMap Mitwirkende", - "internetRequired": "Eine Internetverbindung wird benötigt.", - "compareWithID": "Gleichen Sie die angezeigten Daten mit einem amtlichen Lichtbildausweis ab.", - "comparedWithID": "Ich habe die Daten mit einem amtlichen Lichtbildausweis abgeglichen.", - "checkingCode": "Der QR-Code wird durch eine Server-Anfrage geprüft.", - "scanCode": "Scannen Sie den QR-Code, der auf dem \"Ausweisen\"-Tab Ihres Gegenübers angezeigt wird.", - "checkRequired": "Prüfung nötig", - "notVerified": "Nicht verifiziert", - "activateCardCurrentDevice": "Karte auf diesem Gerät aktivieren?", - "activateCardCurrentDeviceRationale": "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", - "activationInvalidState": "Die Aktivierung befindet sich in einem ungültigen Zustand.", - "categories": { - "mobilityLong": "Auto/Zweirad", - "mobility": "Mobilität", - "media": "Multimedia", - "healthLong": "Gesundheit/Sport/Wellness", - "health": "Gesundheit", - "cultureLong": "Bildung/Kultur/Unterhaltung", - "culture": "Kultur", - "servicesLong": "Dienstleistungen/Finanzen", - "services": "Dienstleistung", - "fashionLong": "Mode/Beauty", - "fashion": "Mode", - "livingLong": "Wohnen/Haus/Garten", - "living": "Einrichtung", - "leisureLong": "Freizeit/Reise/Unterkünfte", - "leisure": "Freizeit", - "foodLong": "Essen/Trinken/Gastronomie", - "food": "Gastronomie", - "other": "Anderes", - "lunchTables": "Mittagstische", - "clothingLong": "Kleidung/Gebrauchtes", - "clothing": "Kleidung", - "cultureLongAlternative": "Kultur/Museen/Freizeit", - "education": "Bildung", - "moviesLong": "Kinos/Theater/Konzerte", - "movies": "Schauspiel", - "pharmaciesLong": "Apotheken/Gesundheit", - "pharmacies": "Apotheken", - "digitalParticipation": "Digitale Teilhabe", - "sportsLong": "Sport/Bewegung/Tanz", - "sports": "Sport" - } -} diff --git a/frontend/l10n.yaml b/frontend/l10n.yaml new file mode 100644 index 000000000..15338f2dd --- /dev/null +++ b/frontend/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/frontend/lib/about/about_page.dart b/frontend/lib/about/about_page.dart index 52169ad4e..9005c7cf2 100644 --- a/frontend/lib/about/about_page.dart +++ b/frontend/lib/about/about_page.dart @@ -7,7 +7,6 @@ import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/configuration/configuration.dart'; import 'package:ehrenamtskarte/routing.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -15,6 +14,7 @@ import '../util/i18n.dart'; class AboutPage extends StatefulWidget { final countToEnableSwitch = 10; + const AboutPage({super.key}); @override @@ -70,21 +70,19 @@ class AboutPageState extends State { child: Column( children: [ Center( - child: I18nText('publisher', child: Text('', style: Theme.of(context).textTheme.titleSmall)), + child: Text(t(context).about_publisher, style: Theme.of(context).textTheme.titleSmall), ), Padding( padding: const EdgeInsets.only(left: 10, right: 10, top: 16, bottom: 16), child: Text(buildConfig.publisherAddress, style: Theme.of(context).textTheme.bodyLarge), ), - I18nText( - 'moreInformation', - child: Text('', + Text( + t(context).about_moreInformation, style: Theme.of(context) .textTheme .bodyMedium ?.merge(TextStyle(color: Theme.of(context).colorScheme.secondary)), ), - ), ], ), ), @@ -92,7 +90,8 @@ class AboutPageState extends State { Navigator.push( context, AppRoute( - builder: (context) => ContentPage(title: t(context, 'publisher'), children: getPublisherText(context)), + builder: (context) => + ContentPage(title: t(context).about_publisher, children: getPublisherText(context)), ), ); }, @@ -102,20 +101,20 @@ class AboutPageState extends State { thickness: 1, ), const SizedBox(height: 20), - ContentTile(icon: Icons.copyright, title: t(context, 'license'), children: getCopyrightText(context)), + ContentTile(icon: Icons.copyright, title: t(context).about_license, children: getCopyrightText(context)), ListTile( leading: const Icon(Icons.privacy_tip_outlined), - title: I18nText('privacyDeclaration'), + title: Text(t(context).about_privacyDeclaration), onTap: () => launchUrlString(buildConfig.dataPrivacyPolicyUrl, mode: LaunchMode.externalApplication), ), ContentTile( icon: Icons.info_outline, - title: t(context, 'disclaimer'), + title: t(context).about_disclaimer, children: getDisclaimerText(context), ), ListTile( leading: const Icon(Icons.book_outlined), - title: I18nText('dependencies'), + title: Text(t(context).about_dependencies), onTap: () { Navigator.push( context, @@ -127,7 +126,7 @@ class AboutPageState extends State { ), ListTile( leading: const Icon(Icons.code_outlined), - title: I18nText('sourceCode'), + title: Text(t(context).about_sourceCode), onTap: () { launchUrlString( 'https://github.com/digitalfabrik/entitlementcard', @@ -138,11 +137,11 @@ class AboutPageState extends State { if (config.showDevSettings) ListTile( leading: const Icon(Icons.build), - title: I18nText('developmentOptions'), + title: Text(t(context).about_developmentOptions), onTap: () => showDialog( context: context, builder: (context) => - SimpleDialog(title: I18nText('developmentOptions'), children: [DevSettingsView()]), + SimpleDialog(title: Text(t(context).about_developmentOptions), children: [DevSettingsView()]), ), ) ]; diff --git a/frontend/lib/about/license_page.dart b/frontend/lib/about/license_page.dart index 1a004eaf0..84a8ac6eb 100644 --- a/frontend/lib/about/license_page.dart +++ b/frontend/lib/about/license_page.dart @@ -5,7 +5,6 @@ import 'package:ehrenamtskarte/widgets/error_message.dart'; import 'package:ehrenamtskarte/widgets/top_loading_spinner.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import '../util/i18n.dart'; @@ -54,7 +53,7 @@ class CustomLicensePage extends StatelessWidget { return CustomScrollView( slivers: [ - CustomSliverAppBar(title: t(context, 'licenses')), + CustomSliverAppBar(title: t(context).about_licenses), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { @@ -62,7 +61,7 @@ class CustomLicensePage extends StatelessWidget { final paragraphs = license.licenseParagraphs; return ListTile( title: Text(license.packageName), - subtitle: I18nPlural('numberLicenses', paragraphs.length), + subtitle: Text(t(context).about_numberLicenses(paragraphs.length)), onTap: () { Navigator.push( context, diff --git a/frontend/lib/app.dart b/frontend/lib/app.dart index 2feecef03..33c859d64 100644 --- a/frontend/lib/app.dart +++ b/frontend/lib/app.dart @@ -8,9 +8,9 @@ import 'package:ehrenamtskarte/intro_slides/intro_screen.dart'; import 'package:ehrenamtskarte/themes.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'home/home_page.dart'; @@ -68,11 +68,7 @@ class App extends StatelessWidget { themeMode: ThemeMode.system, debugShowCheckedModeBanner: false, localizationsDelegates: [ - FlutterI18nDelegate( - translationLoader: FileTranslationLoader(useCountryCode: false, fallbackFile: 'de', basePath: 'assets/bayern/translations'), - missingTranslationHandler: (key, locale) => - print('--- Missing Key: $key, languageCode: ${locale?.languageCode}'), - ), + AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, diff --git a/frontend/lib/home/home_page.dart b/frontend/lib/home/home_page.dart index 281ae01b1..69c97ac14 100644 --- a/frontend/lib/home/home_page.dart +++ b/frontend/lib/home/home_page.dart @@ -39,23 +39,24 @@ class HomePageState extends State { selectAcceptingStore: (id) => setState(() => selectedAcceptingStoreId = id), ), Icons.map_outlined, - t(context, 'card'), + t(context).map_title, GlobalKey(debugLabel: 'Map tab key'), ), AppFlow( const SearchPage(), Icons.search_outlined, - t(context, 'search'), + t(context).search_title, GlobalKey(debugLabel: 'Search tab key'), ), if (buildConfig.featureFlags.verification) AppFlow( IdentificationPage(), Icons.remove_red_eye_outlined, - t(context, 'authentication'), + t(context).identification_title, GlobalKey(debugLabel: 'Auth tab key'), ), - AppFlow(const AboutPage(), Icons.info_outline, t(context, 'about'), GlobalKey(debugLabel: 'About tab key')), + AppFlow(const AboutPage(), Icons.info_outline, t(context).about_title, + GlobalKey(debugLabel: 'About tab key')), ]; } diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 7270abf57..dfd84a169 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -53,25 +53,19 @@ class ActivationCodeScannerPage extends StatelessWidget { await _activateCode(context, activationCode); } on ActivationDidNotOverwriteExisting catch (_) { - await showError(t(context, 'cardAlreadyActivated'), null); + await showError(t(context).identification_cardAlreadyActivated, null); } on QrCodeFieldMissingException catch (e) { - await showError(t(context, 'codeInvalidMissing', translationParams: { 'missing': e.missingFieldName })); + await showError(t(context).identification_codeInvalidMissing(e.missingFieldName), null); } on QrCodeWrongTypeException catch (_) { - await showError(t(context, 'codeSavingFailed')); + await showError(t(context).identification_codeSavingFailed, null); } on CardExpiredException catch (e) { final expirationDate = DateFormat('dd.MM.yyyy').format(e.expiry); - await showError(t(context, 'codeExpired', translationParams: { 'expirationDate': expirationDate })); - } on ServerCardActivationException catch (e, stackTrace) { - String errorMessage = 'Der eingescannte Code konnte nicht aktiviert ' - 'werden, da die Kommunikation mit dem Server fehlschlug. ' - 'Bitte prüfen Sie Ihre Internetverbindung.'; - await ConnectionFailedDialog.show( - context, - t(context, 'codeVerificationFailedConnection') - ); + await showError(t(context).identification_codeExpired(expirationDate), null); + } on ServerCardActivationException catch (_) { + await ConnectionFailedDialog.show(context, t(context).identification_codeVerificationFailedConnection); } on Exception catch (e, stacktrace) { debugPrintStack(stackTrace: stacktrace, label: e.toString()); - await showError(t(context, 'codeUnknownError')); + await showError(t(context).identification_codeUnknownError, null); } } @@ -116,7 +110,7 @@ class ActivationCodeScannerPage extends StatelessWidget { case ActivationState.failed: await QrParsingErrorDialog.showErrorDialog( context, - t(context, 'codeInvalid'), + t(context).identification_codeInvalid, ); break; case ActivationState.didNotOverwriteExisting: @@ -130,7 +124,7 @@ class ActivationCodeScannerPage extends StatelessWidget { } break; default: - throw ServerCardActivationException(t(context, 'activationInvalidState')); + throw ServerCardActivationException(t(context).identification_activationInvalidState); } } } diff --git a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart index f32326e2f..cdb2402e3 100644 --- a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart +++ b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import '../../util/i18n.dart'; @@ -9,23 +8,23 @@ class ActivationOverwriteExistingDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(t(context, 'activateCardCurrentDevice'), style: TextStyle(fontSize: 18)), + title: Text(t(context).identification_activateCardCurrentDevice, style: TextStyle(fontSize: 18)), content: SingleChildScrollView( child: ListBody( children: [ - I18nText('activateCardCurrentDeviceRationale'), + Text(t(context).identification_activateCardCurrentDeviceRationale), ], ), ), actions: [ TextButton( - child: I18nText('cancel'), + child: Text(t(context).common_cancel), onPressed: () { Navigator.of(context).pop(false); }, ), TextButton( - child: I18nText('activate'), + child: Text(t(context).identification_activate), onPressed: () { Navigator.of(context).pop(true); }, diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index 63a596646..3667503e1 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -6,7 +6,6 @@ import 'package:ehrenamtskarte/identification/id_card/id_card.dart'; import 'package:ehrenamtskarte/identification/util/card_info_utils.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; @@ -189,36 +188,36 @@ class QrCodeAndStatus extends StatelessWidget { children: [ ...switch (status) { CardStatus.expired => [ - _PaddedText(t(context, 'cardExpired')) + _PaddedText(t(context).identification_cardExpired) ], CardStatus.notVerifiedLately => [ - _PaddedText(t(context, 'checkFailed')), + _PaddedText(t(context).identification_checkFailed), Flexible( child: TextButton.icon( icon: const Icon(Icons.refresh), onPressed: onSelfVerifyPressed, - label: I18nText('checkAgain'), + label: Text(t(context).identification_checkAgain), ), ), ], CardStatus.timeOutOfSync => [ - _PaddedText(t(context, 'timeIncorrect')), + _PaddedText(t(context).identification_timeIncorrect), Flexible( child: TextButton.icon( icon: const Icon(Icons.refresh), onPressed: onSelfVerifyPressed, - label: I18nText('checkAgain'), + label: Text(t(context).identification_checkAgain), )) ], CardStatus.invalid => [ - _PaddedText(t(context, 'cardInvalid')) + _PaddedText(t(context).identification_cardInvalid) ], CardStatus.valid => [ - _PaddedText(t(context, 'authenticationPossible')), + _PaddedText(t(context).identification_authenticationPossible), Flexible(child: VerificationCodeView(userCode: userCode)) ], CardStatus.notYetValid => [ - _PaddedText(t(context, 'cardNotYetValid')), + _PaddedText(t(context).identification_cardNotYetValid), ] }, Container( @@ -226,7 +225,7 @@ class QrCodeAndStatus extends StatelessWidget { child: TextButton( onPressed: onMoreActionsPressed, child: Text( - t(context, 'moreActions'), + t(context).common_moreActions, style: TextStyle(color: Theme.of(context).colorScheme.secondary), ), ), diff --git a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart index 24b2c31c2..7a682b75d 100644 --- a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart +++ b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart @@ -1,6 +1,7 @@ import 'package:ehrenamtskarte/build_config/build_config.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; + +import '../../util/i18n.dart'; class MoreActionsDialog extends StatelessWidget { final VoidCallback startActivation; @@ -19,7 +20,7 @@ class MoreActionsDialog extends StatelessWidget { final localization = buildConfig.localization.identification.moreActions; return AlertDialog( contentPadding: const EdgeInsets.only(top: 12), - title: I18nText('moreActions'), + title: Text(t(context).common_moreActions), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -54,7 +55,7 @@ class MoreActionsDialog extends StatelessWidget { ], ), ), - actions: [TextButton(onPressed: () => Navigator.pop(context), child: I18nText('cancel'))], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: Text(t(context).common_cancel))], ); } } diff --git a/frontend/lib/identification/connection_failed_dialog.dart b/frontend/lib/identification/connection_failed_dialog.dart index b6c5c6689..7241b3133 100644 --- a/frontend/lib/identification/connection_failed_dialog.dart +++ b/frontend/lib/identification/connection_failed_dialog.dart @@ -11,7 +11,7 @@ class ConnectionFailedDialog extends StatelessWidget { @override Widget build(BuildContext context) { return InfoDialog( - title: t(context, 'connectionFailed'), + title: t(context).common_connectionFailed, icon: Icons.signal_cellular_connected_no_internet_4_bar, iconColor: Theme.of(context).colorScheme.onBackground, child: Text(reason), diff --git a/frontend/lib/identification/id_card/card_content.dart b/frontend/lib/identification/id_card/card_content.dart index ee90632d1..7fa27c1a5 100644 --- a/frontend/lib/identification/id_card/card_content.dart +++ b/frontend/lib/identification/id_card/card_content.dart @@ -52,7 +52,7 @@ class CardContent extends StatelessWidget { final expirationDay = cardInfo.hasExpirationDay() ? cardInfo.expirationDay : null; return expirationDay != null ? DateFormat('dd.MM.yyyy').format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: expirationDay))) - : t(context, 'unlimited'); + : t(context).identification_unlimited; } String? get _formattedBirthday { @@ -77,8 +77,8 @@ class CardContent extends StatelessWidget { String _getCardValidityDate(BuildContext context, String? startDate, String expirationDate) { return startDate != null - ? t(context, 'validFromUntil', translationParams: {'startDate': startDate, 'expirationDate': expirationDate}) - : t(context, 'validUntil', translationParams: { 'expirationDate': expirationDate }); + ? t(context).identification_validFromUntil(expirationDate, startDate) + : t(context).identification_validUntil(expirationDate); } @override diff --git a/frontend/lib/identification/info_dialog.dart b/frontend/lib/identification/info_dialog.dart index 6c03f652a..b9228a4c2 100644 --- a/frontend/lib/identification/info_dialog.dart +++ b/frontend/lib/identification/info_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; + +import '../util/i18n.dart'; class InfoDialog extends StatelessWidget { final Widget child; @@ -24,7 +25,7 @@ class InfoDialog extends StatelessWidget { title: Text(title, style: theme.textTheme.headlineSmall), ), content: child, - actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: I18nText('ok'))], + actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(t(context).common_ok))], ); } } diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart index d6f6c2b21..176884ca4 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../util/i18n.dart'; @@ -10,23 +9,23 @@ class QrCodeCameraPermissionDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(t(context, 'cameraAccessRequired'), style: TextStyle(fontSize: 18)), + title: Text(t(context).identification_cameraAccessRequired, style: TextStyle(fontSize: 18)), content: SingleChildScrollView( child: ListBody( children: [ - I18nText('cameraAccessRequiredSettings'), + Text(t(context).identification_cameraAccessRequired), ], ), ), actions: [ TextButton( - child: I18nText('cancel'), + child: Text(t(context).common_cancel), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( - child: I18nText('openSettings'), + child: Text(t(context).common_openSettings), onPressed: () { openAppSettings(); }, diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart index e310e2599..de99d00ad 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart @@ -4,9 +4,10 @@ import 'dart:typed_data'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_scanner_controls.dart'; import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_overlay_shape.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import '../../util/i18n.dart'; + typedef OnCodeScannedCallback = Future Function(Uint8List code); class QrCodeScanner extends StatefulWidget { @@ -76,7 +77,7 @@ class _QRViewState extends State { children: [ Container( margin: const EdgeInsets.all(8), - child: I18nText('scanQRCode'), + child: Text(t(context).identification_scanQRCode), ), QrCodeScannerControls(controller: controller) ], diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart index 018349787..b5850e4be 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart @@ -21,7 +21,7 @@ class QrCodeScannerControls extends StatelessWidget { child: ValueListenableBuilder( valueListenable: controller.torchState, builder: (ctx, state, child) => Text( - t(context, state == TorchState.on ? 'flashOff' : 'flashOn'), + state == TorchState.on ? t(context).identification_flashOff : t(context).identification_flashOn, style: const TextStyle(fontSize: 16), ), ), @@ -34,7 +34,7 @@ class QrCodeScannerControls extends StatelessWidget { child: ValueListenableBuilder( valueListenable: controller.cameraFacingState, builder: (ctx, state, child) => Text( - t(context, state == CameraFacing.back ? 'selfieCamera' : 'standardCamera'), + state == CameraFacing.back ? t(context).identification_selfieCamera : t(context).identification_standardCamera, style: const TextStyle(fontSize: 16), ), ), diff --git a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart index 4d6028205..e349e5f3c 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; + +import '../../util/i18n.dart'; class QrParsingErrorDialog extends StatelessWidget { final String message; @@ -9,7 +10,7 @@ class QrParsingErrorDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: I18nText('scanningFailed'), + title: Text(t(context).identification_scanningFailed), content: SingleChildScrollView( child: ListBody( children: [ @@ -19,7 +20,7 @@ class QrParsingErrorDialog extends StatelessWidget { ), actions: [ TextButton( - child: I18nText('ok'), + child: Text(t(context).common_ok), onPressed: () { Navigator.of(context).pop(); }, diff --git a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart index c94a062c1..664361358 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart @@ -11,7 +11,7 @@ class NegativeVerificationResultDialog extends StatelessWidget { @override Widget build(BuildContext context) { return InfoDialog( - title: t(context, 'notVerified'), + title: t(context).identification_notVerified, icon: Icons.error, iconColor: Colors.red, child: Text(reason), diff --git a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart index 7dc7bcce7..bd9219482 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart @@ -5,7 +5,6 @@ import 'package:ehrenamtskarte/identification/id_card/id_card.dart'; import 'package:ehrenamtskarte/identification/info_dialog.dart'; import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import '../../../util/i18n.dart'; @@ -58,7 +57,7 @@ class PositiveVerificationResultDialogState extends State _onDone(context), ) ], diff --git a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart index 886bc2951..d18b16c70 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart @@ -77,7 +77,7 @@ class VerificationQrScannerPage extends StatelessWidget { if (cardInfo == null) { await _onError( context, - t(context, 'codeVerificationFailed'), + t(context).identification_codeVerificationFailed, ); } else { await _onSuccess(context, cardInfo, qrcode.hasStaticVerificationCode()); @@ -85,32 +85,32 @@ class VerificationQrScannerPage extends StatelessWidget { } on ServerVerificationException catch (e) { await _onConnectionError( context, - t(context, 'codeVerificationFailedConnection'), + t(context).identification_codeVerificationFailedConnection, e, ); } on QrCodeFieldMissingException catch (e) { await _onError( context, - t(context, 'codeInvalidMissing'), + t(context).identification_codeInvalidMissing(e.missingFieldName), e, ); } on CardExpiredException catch (e) { final expirationDate = DateFormat('dd.MM.yyyy').format(e.expiry); await _onError( context, - t(context, 'codeExpired', translationParams: {'expirationDate': expirationDate}), + t(context).identification_codeExpired(expirationDate), e, ); } on QrCodeParseException catch (e) { await _onError( context, - t(context, 'codeInvalid'), + t(context).identification_codeInvalid, e, ); } on Exception catch (e) { await _onError( context, - t(context, 'codeUnknownError'), + t(context).identification_codeUnknownError, e, ); } finally { diff --git a/frontend/lib/intro_slides/intro_screen.dart b/frontend/lib/intro_slides/intro_screen.dart index 9460fee17..854721d13 100644 --- a/frontend/lib/intro_slides/intro_screen.dart +++ b/frontend/lib/intro_slides/intro_screen.dart @@ -2,9 +2,10 @@ import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/app.dart'; import 'package:ehrenamtskarte/intro_slides/location_request_button.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:intro_slider/intro_slider.dart'; +import '../util/i18n.dart'; + typedef OnFinishedCallback = void Function(); class IntroScreen extends StatelessWidget { @@ -22,9 +23,9 @@ class IntroScreen extends StatelessWidget { final theme = Theme.of(context); return IntroSlider( onDonePress: () => onDonePress(context), - renderDoneBtn: I18nText('done'), - renderNextBtn: I18nText('next'), - renderPrevBtn: I18nText('previous'), + renderDoneBtn: Text(t(context).common_done), + renderNextBtn: Text(t(context).common_next), + renderPrevBtn: Text(t(context).common_previous), doneButtonStyle: Theme.of(context).textButtonTheme.style, indicatorConfig: IndicatorConfig( colorActiveIndicator: theme.colorScheme.primary, diff --git a/frontend/lib/intro_slides/location_request_button.dart b/frontend/lib/intro_slides/location_request_button.dart index 77a34911f..3ed03b7a3 100644 --- a/frontend/lib/intro_slides/location_request_button.dart +++ b/frontend/lib/intro_slides/location_request_button.dart @@ -1,9 +1,10 @@ import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/location/determine_position.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:provider/provider.dart'; +import '../util/i18n.dart'; + class LocationRequestButton extends StatefulWidget { const LocationRequestButton({super.key}); @@ -52,7 +53,7 @@ class _LocationRequestButtonState extends State { if (status == null) { return ElevatedButton( onPressed: null, - child: I18nText('checkSettings'), + child: Text(t(context).location_checkSettings), ); } switch (status) { @@ -60,18 +61,18 @@ class _LocationRequestButtonState extends State { case LocationStatus.notSupported: return ElevatedButton( onPressed: () => _onLocationButtonClicked(settings), - child: I18nText('grantLocation'), + child: Text(t(context).location_grantLocation), ); case LocationStatus.whileInUse: case LocationStatus.always: return ElevatedButton( onPressed: null, - child: I18nText('locationGranted'), + child: Text(t(context).location_locationGranted), ); case LocationStatus.deniedForever: return ElevatedButton( onPressed: null, - child: I18nText('locationDeactivated'), + child: Text(t(context).location_locationDeactivated), ); } } diff --git a/frontend/lib/l10n/app_de.arb b/frontend/lib/l10n/app_de.arb new file mode 100644 index 000000000..18c327456 --- /dev/null +++ b/frontend/lib/l10n/app_de.arb @@ -0,0 +1,133 @@ +{ + "about_dependencies": "Software-Bibliotheken", + "about_developmentOptions": "Entwickleroptionen", + "about_disclaimer": "Haftung, Haftungsausschluss und Impressum", + "about_license": "Lizenz", + "about_licenses": "Lizenzen", + "about_moreInformation": "Mehr Informationen", + "about_numberLicenses": "{count, plural, =0{Keine Lizenzen} =1{1 Lizenz} other{{count} Lizenzen}}", + "about_privacyDeclaration": "Datenschutzerklärung", + "about_publisher": "Herausgeber", + "about_sourceCode": "Quellcode der App", + "about_title": "Über", + + "category_clothing": "Kleidung", + "category_clothingLong": "Kleidung/Gebrauchtes", + "category_culture": "Kultur", + "category_cultureLong": "Bildung/Kultur/Unterhaltung", + "category_cultureLongAlternative": "Kultur/Museen/Freizeit", + "category_digitalParticipation": "Digitale Teilhabe", + "category_education": "Bildung", + "category_fashion": "Mode", + "category_fashionLong": "Mode/Beauty", + "category_food": "Gastronomie", + "category_foodLong": "Essen/Trinken/Gastronomie", + "category_health": "Gesundheit", + "category_healthLong": "Gesundheit/Sport/Wellness", + "category_leisure": "Freizeit", + "category_leisureLong": "Freizeit/Reise/Unterkünfte", + "category_living": "Einrichtung", + "category_livingLong": "Wohnen/Haus/Garten", + "category_lunchTables": "Mittagstische", + "category_media": "Multimedia", + "category_mobility": "Mobilität", + "category_mobilityLong": "Auto/Zweirad", + "category_movies": "Schauspiel", + "category_moviesLong": "Kinos/Theater/Konzerte", + "category_other": "Anderes", + "category_pharmacies": "Apotheken", + "category_pharmaciesLong": "Apotheken/Gesundheit", + "category_services": "Dienstleistung", + "category_servicesLong": "Dienstleistungen/Finanzen", + "category_sports": "Sport", + "category_sportsLong": "Sport/Bewegung/Tanz", + + "common_cancel": "Abbrechen", + "common_checkConnection": "Bitte Internetverbindung prüfen.", + "common_connectionFailed": "Keine Verbindung möglich", + "common_done": "Fertig", + "common_moreActions": "Weitere Aktionen", + "common_next": "Weiter", + "common_ok": "OK", + "common_openSettings": "Einstellungen öffnen", + "common_previous": "Zurück", + "common_settings": "Einstellungen", + "common_tryAgain": "Erneut versuchen", + + "identification_activate": "Aktivieren", + "identification_activateCardCurrentDevice": "Karte auf diesem Gerät aktivieren?", + "identification_activateCardCurrentDeviceRationale": "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", + "identification_activationInvalidState": "Die Aktivierung befindet sich in einem ungültigen Zustand.", + "identification_authenticationPossible": "Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:", + "identification_cameraAccessRequired": "Zugriff auf Kamera erforderlich", + "identification_cameraAccessRequiredSettings": "Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera.\nIn den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.", + "identification_cardAlreadyActivated": "Der eingescannte QRCode wurde bereits aktiviert.", + "identification_cardExpired": "Ihre Karte ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", + "identification_cardInvalid": "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", + "identification_cardNotYetValid": "Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen.", + "identification_checkAgain": "Erneut prüfen", + "identification_checkFailed": "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Verbindung mit dem Internet besteht und prüfen Sie erneut.", + "identification_checkRequired": "Prüfung nötig", + "identification_checkingCode": "Der QR-Code wird durch eine Server-Anfrage geprüft.", + "identification_codeExpired": "Der eingescannte Code ist bereits am {expirationDate} abgelaufen.", + "identification_codeInvalid": "Der Inhalt des eingescannten Codes kann nicht verstanden werden. Vermutlich handelt es sich um einen QR-Code, der nicht für diese App generiert wurde.", + "identification_codeInvalidMissing": "Der Inhalt des eingescannten Codes ist unvollständig. (Fehlercode: {missing}Missing)", + "identification_codeSavingFailed": "Der eingescannte Code kann nicht in der App gespeichert werden.", + "identification_codeUnknownError": "Beim Einlesen des QR-Codes ist ein unbekannter Fehler aufgetreten.", + "identification_codeVerificationFailed": "Der eingescannte Code konnte vom Server nicht verifiziert werden!", + "identification_codeVerificationFailedConnection": "Der eingescannte Code konnte nicht verifiziert werden, da die Kommunikation mit dem Server fehlschlug. Bitte prüfen Sie Ihre Internetverbindung.", + "identification_compareWithID": "Gleichen Sie die angezeigten Daten mit einem amtlichen Lichtbildausweis ab.", + "identification_comparedWithID": "Ich habe die Daten mit einem amtlichen Lichtbildausweis abgeglichen.", + "identification_flashOff": "Blitz aus", + "identification_flashOn": "Blitz an", + "identification_generatingTotpSecretFailed": "", + "identification_internetRequired": "Eine Internetverbindung wird benötigt.", + "identification_notVerified": "Nicht verifiziert", + "identification_scanCode": "Scannen Sie den QR-Code, der auf dem \"Ausweisen\"-Tab Ihres Gegenübers angezeigt wird.", + "identification_scanQRCode": "Halten Sie die Kamera auf den QR Code.", + "identification_scanningFailed": "Fehler beim Lesen des Codes", + "identification_selfieCamera": "Frontkamera", + "identification_standardCamera": "Standard-Kamera", + "identification_stopShowing": "Nicht mehr anzeigen", + "identification_timeIncorrect": "Die Uhrzeit Ihres Geräts scheint nicht zu stimmen. Bitte synchronisieren Sie die Uhrzeit in den Systemeinstellungen.", + "identification_title": "Ausweisen", + "identification_unlimited": "unbegrenzt", + "identification_validFromUntil": "Gültig: {startDate} bis {expirationDate}", + "identification_validUntil": "Gültig bis: {expirationDate}", + + "location_activateLocationAccess": "Standortermittlung aktivieren", + "location_activateLocationAccessRationale": "Erlauben Sie der App Ihren Standort zu benutzen, um Akzeptanzstellen in Ihrer Umgebung anzuzeigen.", + "location_activateLocationAccessSettings": "Aktivieren Sie die Standortermittlung in den Einstellungen.", + "location_askPermissionsAgain": "Soll nocheinmal nach der Berechtigung gefragt werden?", + "location_grantLocation": "Ich möchte meinen Standort freigeben.", + "location_grantPermission": "Berechtigung erteilen", + "location_locationAccessDeactivated": "Die Standortfreigabe ist deaktiviert.", + "location_locationDeactivated": "Standortfreigabe ist deaktiviert.", + "location_locationGranted": "Standort ist freigegeben.", + "location_locationPermission": "Standortberechtigung", + "location_checkSettings": "Prüfe Einstellungen...", + + "map_mapData": "Kartendaten", + "map_osmContributors": "OpenStreetMap Mitwirkende", + "map_showMapCopyright": "Zeige Infos über das Urheberrecht der Kartendaten", + "map_title": "Karte", + + "search_filterByCategories": "Nach Kategorien filtern", + "search_findCloseBy": "In meiner Nähe suchen", + "search_noAcceptingStoresFound": "Auf diese Suche trifft keine Akzeptanzstelle zu.", + "search_searchHint": "Tippen, um zu suchen …", + "search_searchResults": "Suchresultate", + "search_title": "Suche", + + "store_acceptingStore": "Akzeptanzstelle", + "store_acceptingStoreNotFound": "Akzeptanzstelle nicht gefunden.", + "store_address": "Adresse", + "store_email": "E-Mail", + "store_loadingDataFailed": "Fehler beim Laden der Daten.", + "store_loadingInformationFailed": "Fehler beim Laden der Infos.", + "store_noDescriptionAvailable": "Keine Beschreibung verfügbar", + "store_phone": "Telefon", + "store_showOnMap": "Auf Karte zeigen", + "store_unknownCategory": "Unbekannte Kategorie", + "store_website": "Website" +} diff --git a/frontend/lib/l10n/app_en.arb b/frontend/lib/l10n/app_en.arb new file mode 100644 index 000000000..18c327456 --- /dev/null +++ b/frontend/lib/l10n/app_en.arb @@ -0,0 +1,133 @@ +{ + "about_dependencies": "Software-Bibliotheken", + "about_developmentOptions": "Entwickleroptionen", + "about_disclaimer": "Haftung, Haftungsausschluss und Impressum", + "about_license": "Lizenz", + "about_licenses": "Lizenzen", + "about_moreInformation": "Mehr Informationen", + "about_numberLicenses": "{count, plural, =0{Keine Lizenzen} =1{1 Lizenz} other{{count} Lizenzen}}", + "about_privacyDeclaration": "Datenschutzerklärung", + "about_publisher": "Herausgeber", + "about_sourceCode": "Quellcode der App", + "about_title": "Über", + + "category_clothing": "Kleidung", + "category_clothingLong": "Kleidung/Gebrauchtes", + "category_culture": "Kultur", + "category_cultureLong": "Bildung/Kultur/Unterhaltung", + "category_cultureLongAlternative": "Kultur/Museen/Freizeit", + "category_digitalParticipation": "Digitale Teilhabe", + "category_education": "Bildung", + "category_fashion": "Mode", + "category_fashionLong": "Mode/Beauty", + "category_food": "Gastronomie", + "category_foodLong": "Essen/Trinken/Gastronomie", + "category_health": "Gesundheit", + "category_healthLong": "Gesundheit/Sport/Wellness", + "category_leisure": "Freizeit", + "category_leisureLong": "Freizeit/Reise/Unterkünfte", + "category_living": "Einrichtung", + "category_livingLong": "Wohnen/Haus/Garten", + "category_lunchTables": "Mittagstische", + "category_media": "Multimedia", + "category_mobility": "Mobilität", + "category_mobilityLong": "Auto/Zweirad", + "category_movies": "Schauspiel", + "category_moviesLong": "Kinos/Theater/Konzerte", + "category_other": "Anderes", + "category_pharmacies": "Apotheken", + "category_pharmaciesLong": "Apotheken/Gesundheit", + "category_services": "Dienstleistung", + "category_servicesLong": "Dienstleistungen/Finanzen", + "category_sports": "Sport", + "category_sportsLong": "Sport/Bewegung/Tanz", + + "common_cancel": "Abbrechen", + "common_checkConnection": "Bitte Internetverbindung prüfen.", + "common_connectionFailed": "Keine Verbindung möglich", + "common_done": "Fertig", + "common_moreActions": "Weitere Aktionen", + "common_next": "Weiter", + "common_ok": "OK", + "common_openSettings": "Einstellungen öffnen", + "common_previous": "Zurück", + "common_settings": "Einstellungen", + "common_tryAgain": "Erneut versuchen", + + "identification_activate": "Aktivieren", + "identification_activateCardCurrentDevice": "Karte auf diesem Gerät aktivieren?", + "identification_activateCardCurrentDeviceRationale": "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", + "identification_activationInvalidState": "Die Aktivierung befindet sich in einem ungültigen Zustand.", + "identification_authenticationPossible": "Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:", + "identification_cameraAccessRequired": "Zugriff auf Kamera erforderlich", + "identification_cameraAccessRequiredSettings": "Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera.\nIn den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.", + "identification_cardAlreadyActivated": "Der eingescannte QRCode wurde bereits aktiviert.", + "identification_cardExpired": "Ihre Karte ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", + "identification_cardInvalid": "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", + "identification_cardNotYetValid": "Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen.", + "identification_checkAgain": "Erneut prüfen", + "identification_checkFailed": "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Verbindung mit dem Internet besteht und prüfen Sie erneut.", + "identification_checkRequired": "Prüfung nötig", + "identification_checkingCode": "Der QR-Code wird durch eine Server-Anfrage geprüft.", + "identification_codeExpired": "Der eingescannte Code ist bereits am {expirationDate} abgelaufen.", + "identification_codeInvalid": "Der Inhalt des eingescannten Codes kann nicht verstanden werden. Vermutlich handelt es sich um einen QR-Code, der nicht für diese App generiert wurde.", + "identification_codeInvalidMissing": "Der Inhalt des eingescannten Codes ist unvollständig. (Fehlercode: {missing}Missing)", + "identification_codeSavingFailed": "Der eingescannte Code kann nicht in der App gespeichert werden.", + "identification_codeUnknownError": "Beim Einlesen des QR-Codes ist ein unbekannter Fehler aufgetreten.", + "identification_codeVerificationFailed": "Der eingescannte Code konnte vom Server nicht verifiziert werden!", + "identification_codeVerificationFailedConnection": "Der eingescannte Code konnte nicht verifiziert werden, da die Kommunikation mit dem Server fehlschlug. Bitte prüfen Sie Ihre Internetverbindung.", + "identification_compareWithID": "Gleichen Sie die angezeigten Daten mit einem amtlichen Lichtbildausweis ab.", + "identification_comparedWithID": "Ich habe die Daten mit einem amtlichen Lichtbildausweis abgeglichen.", + "identification_flashOff": "Blitz aus", + "identification_flashOn": "Blitz an", + "identification_generatingTotpSecretFailed": "", + "identification_internetRequired": "Eine Internetverbindung wird benötigt.", + "identification_notVerified": "Nicht verifiziert", + "identification_scanCode": "Scannen Sie den QR-Code, der auf dem \"Ausweisen\"-Tab Ihres Gegenübers angezeigt wird.", + "identification_scanQRCode": "Halten Sie die Kamera auf den QR Code.", + "identification_scanningFailed": "Fehler beim Lesen des Codes", + "identification_selfieCamera": "Frontkamera", + "identification_standardCamera": "Standard-Kamera", + "identification_stopShowing": "Nicht mehr anzeigen", + "identification_timeIncorrect": "Die Uhrzeit Ihres Geräts scheint nicht zu stimmen. Bitte synchronisieren Sie die Uhrzeit in den Systemeinstellungen.", + "identification_title": "Ausweisen", + "identification_unlimited": "unbegrenzt", + "identification_validFromUntil": "Gültig: {startDate} bis {expirationDate}", + "identification_validUntil": "Gültig bis: {expirationDate}", + + "location_activateLocationAccess": "Standortermittlung aktivieren", + "location_activateLocationAccessRationale": "Erlauben Sie der App Ihren Standort zu benutzen, um Akzeptanzstellen in Ihrer Umgebung anzuzeigen.", + "location_activateLocationAccessSettings": "Aktivieren Sie die Standortermittlung in den Einstellungen.", + "location_askPermissionsAgain": "Soll nocheinmal nach der Berechtigung gefragt werden?", + "location_grantLocation": "Ich möchte meinen Standort freigeben.", + "location_grantPermission": "Berechtigung erteilen", + "location_locationAccessDeactivated": "Die Standortfreigabe ist deaktiviert.", + "location_locationDeactivated": "Standortfreigabe ist deaktiviert.", + "location_locationGranted": "Standort ist freigegeben.", + "location_locationPermission": "Standortberechtigung", + "location_checkSettings": "Prüfe Einstellungen...", + + "map_mapData": "Kartendaten", + "map_osmContributors": "OpenStreetMap Mitwirkende", + "map_showMapCopyright": "Zeige Infos über das Urheberrecht der Kartendaten", + "map_title": "Karte", + + "search_filterByCategories": "Nach Kategorien filtern", + "search_findCloseBy": "In meiner Nähe suchen", + "search_noAcceptingStoresFound": "Auf diese Suche trifft keine Akzeptanzstelle zu.", + "search_searchHint": "Tippen, um zu suchen …", + "search_searchResults": "Suchresultate", + "search_title": "Suche", + + "store_acceptingStore": "Akzeptanzstelle", + "store_acceptingStoreNotFound": "Akzeptanzstelle nicht gefunden.", + "store_address": "Adresse", + "store_email": "E-Mail", + "store_loadingDataFailed": "Fehler beim Laden der Daten.", + "store_loadingInformationFailed": "Fehler beim Laden der Infos.", + "store_noDescriptionAvailable": "Keine Beschreibung verfügbar", + "store_phone": "Telefon", + "store_showOnMap": "Auf Karte zeigen", + "store_unknownCategory": "Unbekannte Kategorie", + "store_website": "Website" +} diff --git a/frontend/lib/location/determine_position.dart b/frontend/lib/location/determine_position.dart index 8c8ba75ed..8ff8541c5 100644 --- a/frontend/lib/location/determine_position.dart +++ b/frontend/lib/location/determine_position.dart @@ -145,7 +145,7 @@ Future checkAndRequestLocationPermission( // returned true. According to Android guidelines // your App should show an explanatory UI now. - final result = await showDialog(context: context, builder: (context) => RationaleDialog(rationale: rationale ?? t(context, 'activateLocationAccessRationale'))); + final result = await showDialog(context: context, builder: (context) => RationaleDialog(rationale: rationale ?? t(context).location_activateLocationAccessRationale)); if (result == true) { return checkAndRequestLocationPermission( diff --git a/frontend/lib/location/dialogs.dart b/frontend/lib/location/dialogs.dart index 2d576f7ce..6c75847ed 100644 --- a/frontend/lib/location/dialogs.dart +++ b/frontend/lib/location/dialogs.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; + +import '../util/i18n.dart'; class LocationServiceDialog extends StatelessWidget { const LocationServiceDialog({super.key}); @@ -7,11 +8,11 @@ class LocationServiceDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: I18nText('activateLocationAccess'), - content: I18nText('activateLocationAccessSettings'), + title: Text(t(context).location_activateLocationAccess), + content: Text(t(context).location_activateLocationAccessSettings), actions: [ - TextButton(child: I18nText('cancel'), onPressed: () => Navigator.of(context).pop(false)), - TextButton(child: I18nText('openSettings'), onPressed: () => Navigator.of(context).pop(true)) + TextButton(child: Text(t(context).common_cancel), onPressed: () => Navigator.of(context).pop(false)), + TextButton(child: Text(t(context).common_openSettings), onPressed: () => Navigator.of(context).pop(true)) ], ); } @@ -25,15 +26,15 @@ class RationaleDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: I18nText('locationPermission'), + title: Text(t(context).location_locationPermission), content: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: [Text(_rationale), I18nText('askPermissionsAgain')], + children: [Text(_rationale), Text(t(context).location_askPermissionsAgain)], ), actions: [ - TextButton(child: I18nText('grantPermission'), onPressed: () => Navigator.of(context).pop(true)), - TextButton(child: I18nText('cancel'), onPressed: () => Navigator.of(context).pop(false)) + TextButton(child: Text(t(context).location_grantPermission), onPressed: () => Navigator.of(context).pop(true)), + TextButton(child: Text(t(context).common_cancel), onPressed: () => Navigator.of(context).pop(false)) ], ); } diff --git a/frontend/lib/map/location_button.dart b/frontend/lib/map/location_button.dart index 782482d7b..5cf9f3dc0 100644 --- a/frontend/lib/map/location_button.dart +++ b/frontend/lib/map/location_button.dart @@ -2,7 +2,6 @@ import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/location/determine_position.dart'; import 'package:ehrenamtskarte/widgets/small_button_spinner.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:provider/provider.dart'; import '../util/i18n.dart'; @@ -55,9 +54,9 @@ class _LocationButtonState extends State { messengerState.showSnackBar( SnackBar( behavior: SnackBarBehavior.floating, - content: I18nText('locationAccessDeactivated'), + content: Text(t(context).location_locationAccessDeactivated), action: SnackBarAction( - label: t(context, 'settings'), + label: t(context).common_settings, onPressed: () async { await openSettingsToGrantPermissions(context); }, diff --git a/frontend/lib/map/map/attribution_dialog.dart b/frontend/lib/map/map/attribution_dialog.dart index e6a8af023..7907ac2e7 100644 --- a/frontend/lib/map/map/attribution_dialog.dart +++ b/frontend/lib/map/map/attribution_dialog.dart @@ -1,6 +1,5 @@ import 'package:ehrenamtskarte/map/map/attribution_dialog_item.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../util/i18n.dart'; @@ -12,12 +11,12 @@ class AttributionDialog extends StatelessWidget { Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.primary; return SimpleDialog( - title: I18nText('mapData'), + title: Text(t(context).map_mapData), children: [ AttributionDialogItem( icon: Icons.copyright, color: color, - text: t(context, 'osmContributors'), + text: t(context).map_osmContributors, onPressed: () { launchUrlString('https://www.openstreetmap.org/copyright', mode: LaunchMode.externalApplication); }, diff --git a/frontend/lib/map/map/map.dart b/frontend/lib/map/map/map.dart index 004a7b4a2..3ae0ab0d6 100644 --- a/frontend/lib/map/map/map.dart +++ b/frontend/lib/map/map/map.dart @@ -98,7 +98,7 @@ class _MapContainerState extends State implements MapController { color: mapboxColor, iconSize: 20, icon: const Icon(Icons.info_outline), - tooltip: t(context, 'showMapCopyright'), + tooltip: t(context).map_showMapCopyright, onPressed: () { showDialog( context: context, diff --git a/frontend/lib/map/preview/accepting_store_preview_card.dart b/frontend/lib/map/preview/accepting_store_preview_card.dart index 1b5d42025..c73a91bfe 100644 --- a/frontend/lib/map/preview/accepting_store_preview_card.dart +++ b/frontend/lib/map/preview/accepting_store_preview_card.dart @@ -18,7 +18,7 @@ class AcceptingStorePreviewError extends StatelessWidget { child: Container( height: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16), - child: ErrorMessage(t(context, 'loadingInformationFailed')), + child: ErrorMessage(t(context).store_loadingInformationFailed) ), ); } diff --git a/frontend/lib/search/filter_bar.dart b/frontend/lib/search/filter_bar.dart index a95916189..15a70b507 100644 --- a/frontend/lib/search/filter_bar.dart +++ b/frontend/lib/search/filter_bar.dart @@ -25,7 +25,7 @@ class FilterBar extends StatelessWidget { padding: const EdgeInsets.all(8), child: Row( children: [ - Text(t(context, 'filterByCategories').toUpperCase(), maxLines: 1, style: const TextStyle(color: Colors.grey)), + Text(t(context).search_filterByCategories.toUpperCase(), maxLines: 1, style: const TextStyle(color: Colors.grey)), const Expanded(child: Padding(padding: EdgeInsets.only(left: 8), child: Divider(thickness: 0.7))) ], ), diff --git a/frontend/lib/search/location_button.dart b/frontend/lib/search/location_button.dart index e032ee499..fb865857c 100644 --- a/frontend/lib/search/location_button.dart +++ b/frontend/lib/search/location_button.dart @@ -2,10 +2,11 @@ import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/location/determine_position.dart'; import 'package:ehrenamtskarte/widgets/small_button_spinner.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:geolocator/geolocator.dart'; import 'package:provider/provider.dart'; +import '../util/i18n.dart'; + class LocationButton extends StatefulWidget { final void Function(Position position) setCoordinates; @@ -55,11 +56,10 @@ class _LocationButtonState extends State { color: Theme.of(context).colorScheme.secondary, ), ), - label: I18nText('findCloseBy', - child: Text( - '', - style: TextStyle(color: Theme.of(context).hintColor), - )), + label: Text( + t(context).search_findCloseBy, + style: TextStyle(color: Theme.of(context).hintColor), + ), ), ); } else { diff --git a/frontend/lib/search/results_loader.dart b/frontend/lib/search/results_loader.dart index 2e72d3850..e04e2ea61 100644 --- a/frontend/lib/search/results_loader.dart +++ b/frontend/lib/search/results_loader.dart @@ -3,10 +3,11 @@ import 'package:ehrenamtskarte/graphql/graphql_api.dart'; import 'package:ehrenamtskarte/map/preview/models.dart'; import 'package:ehrenamtskarte/store_widgets/accepting_store_summary.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import '../util/i18n.dart'; + class ResultsLoader extends StatefulWidget { final CoordinatesInput? coordinates; final String? searchText; @@ -147,10 +148,10 @@ class ResultsLoaderState extends State { mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.warning, size: 60, color: Colors.orange), - I18nText('checkConnection'), + Text(t(context).common_checkConnection), OutlinedButton( onPressed: _pagingController.retryLastFailedRequest, - child: I18nText('tryAgain'), + child: Text(t(context).common_tryAgain), ) ], ), @@ -161,7 +162,7 @@ class ResultsLoaderState extends State { mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.search_off, size: 60, color: Theme.of(context).disabledColor), - I18nText('noAcceptingStoresFound'), + Text(t(context).search_noAcceptingStoresFound), ], ), ); diff --git a/frontend/lib/search/search_page.dart b/frontend/lib/search/search_page.dart index d8ca4eb10..06df5c7e3 100644 --- a/frontend/lib/search/search_page.dart +++ b/frontend/lib/search/search_page.dart @@ -39,7 +39,7 @@ class _SearchPageState extends State { child: Row( children: [ Text( - t(context, 'searchResults').toUpperCase(), + t(context).search_searchResults.toUpperCase(), style: const TextStyle(color: Colors.grey), ), const Expanded(child: Padding(padding: EdgeInsets.only(left: 8), child: Divider())) diff --git a/frontend/lib/store_widgets/accepting_store_summary.dart b/frontend/lib/store_widgets/accepting_store_summary.dart index a3a019984..739b5314d 100644 --- a/frontend/lib/store_widgets/accepting_store_summary.dart +++ b/frontend/lib/store_widgets/accepting_store_summary.dart @@ -39,7 +39,7 @@ class AcceptingStoreSummary extends StatelessWidget { @override Widget build(BuildContext context) { final itemCategoryAsset = store.categoryId < categoryAssets.length ? categoryAssets[store.categoryId] : null; - final categoryName = itemCategoryAsset?.name ?? t(context, 'unknownCategory'); + final categoryName = itemCategoryAsset?.name ?? t(context).store_unknownCategory; final categoryColor = itemCategoryAsset?.color; final useWideDepiction = MediaQuery.of(context).size.width > 400; @@ -154,14 +154,14 @@ class StoreTextOverview extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - store.name ?? t(context, 'acceptingStore'), + store.name ?? t(context).store_acceptingStore, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 4), Text( - store.description ?? t(context, 'noDescriptionAvailable'), + store.description ?? t(context).store_noDescriptionAvailable, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium, diff --git a/frontend/lib/store_widgets/detail/detail_app_bar.dart b/frontend/lib/store_widgets/detail/detail_app_bar.dart index 990003ed4..b14118702 100644 --- a/frontend/lib/store_widgets/detail/detail_app_bar.dart +++ b/frontend/lib/store_widgets/detail/detail_app_bar.dart @@ -89,7 +89,7 @@ class DetailAppBar extends StatelessWidget { final accentColor = getDarkenedColorForCategory(categoryId); final categoryName = matchingStore.store.category.name; - final title = matchingStore.store.name ?? t(context, 'acceptingStore'); + final title = matchingStore.store.name ?? t(context).store_acceptingStore; final backgroundColor = accentColor ?? Theme.of(context).colorScheme.primary; final textColor = getReadableOnColor(backgroundColor); diff --git a/frontend/lib/store_widgets/detail/detail_content.dart b/frontend/lib/store_widgets/detail/detail_content.dart index 59e864f57..388ebd2b5 100644 --- a/frontend/lib/store_widgets/detail/detail_content.dart +++ b/frontend/lib/store_widgets/detail/detail_content.dart @@ -7,7 +7,6 @@ import 'package:ehrenamtskarte/store_widgets/detail/contact_info_row.dart'; import 'package:ehrenamtskarte/util/color_utils.dart'; import 'package:ehrenamtskarte/util/sanitize_contact_details.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:maplibre_gl/mapbox_gl.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -62,7 +61,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.location_on, addressString, - t(context, 'address'), + t(context).store_address, onTap: () => _launchMap(mapQueryString), iconColor: readableOnAccentColor, iconFillColor: accentColor, @@ -71,7 +70,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.language, prepareWebsiteUrlForDisplay(website), - t(context, 'website'), + t(context).store_website, onTap: () => launchUrlString(prepareWebsiteUrlForLaunch(website), mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, @@ -81,7 +80,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.phone, telephone, - t(context, 'phone'), + t(context).store_phone, onTap: () => launchUrlString('tel:${sanitizePhoneNumber(telephone)}', mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, @@ -91,7 +90,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.alternate_email, email, - t(context, 'email'), + t(context).store_email, onTap: () => launchUrlString('mailto:${email.trim()}', mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, iconFillColor: accentColor, @@ -108,7 +107,7 @@ class DetailContent extends StatelessWidget { alignment: MainAxisAlignment.center, children: [ OutlinedButton( - child: I18nText('showOnMap'), + child: Text(t(context).store_showOnMap), onPressed: () => _showOnMap(context), ), ], diff --git a/frontend/lib/store_widgets/detail/detail_page.dart b/frontend/lib/store_widgets/detail/detail_page.dart index ea3ba6b29..a2197e382 100644 --- a/frontend/lib/store_widgets/detail/detail_page.dart +++ b/frontend/lib/store_widgets/detail/detail_page.dart @@ -29,15 +29,15 @@ class DetailPage extends StatelessWidget { final data = result.data; if (result.hasException && exception != null) { - return DetailErrorMessage(message: t(context, 'loadingDataFailed'), refetch: refetch); + return DetailErrorMessage(message: t(context).store_loadingDataFailed, refetch: refetch); } else if (result.isNotLoading && data != null) { final matchingStores = byIdQuery.parse(data).physicalStoresByIdInProject; if (matchingStores.length != 1) { - return DetailErrorMessage(message: t(context, 'loadingDataFailed'), refetch: refetch); + return DetailErrorMessage(message: t(context).store_loadingDataFailed, refetch: refetch); } final matchingStore = matchingStores.first; if (matchingStore == null) { - return DetailErrorMessage(message: t(context, 'acceptingStoreNotFound')); + return DetailErrorMessage(message: t(context).store_acceptingStoreNotFound); } final categoryId = matchingStore.store.category.id; final accentColor = getDarkenedColorForCategory(categoryId); diff --git a/frontend/lib/util/i18n.dart b/frontend/lib/util/i18n.dart index 15833853b..0e62eeede 100644 --- a/frontend/lib/util/i18n.dart +++ b/frontend/lib/util/i18n.dart @@ -1,4 +1,6 @@ -import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -final t = FlutterI18n.translate; -final plural = FlutterI18n.plural; +AppLocalizations t (BuildContext context) { + return AppLocalizations.of(context)!; +} diff --git a/frontend/lib/widgets/app_bars.dart b/frontend/lib/widgets/app_bars.dart index 36d3fbd04..6e728578d 100644 --- a/frontend/lib/widgets/app_bars.dart +++ b/frontend/lib/widgets/app_bars.dart @@ -93,7 +93,7 @@ class SearchSliverAppBarState extends State { controller: textEditingController, focusNode: focusNode, decoration: InputDecoration.collapsed( - hintText: t(context, 'searchHint'), + hintText: t(context).search_searchHint, hintStyle: TextStyle(color: foregroundColor?.withOpacity(0.8)), ), cursorColor: foregroundColor, diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index d4ce06606..2510f9b47 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -308,14 +308,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.6" - flutter_i18n: - dependency: "direct main" - description: - name: flutter_i18n - sha256: b71fe887697686368c93e4d2257ecdb3c35fe38686a6fbf3902d6355c3f20363 - url: "https://pub.dev" - source: hosted - version: "0.33.0" flutter_localizations: dependency: "direct main" description: flutter @@ -1268,14 +1260,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" - toml: - dependency: transitive - description: - name: toml - sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" - url: "https://pub.dev" - source: hosted - version: "0.14.0" tuple: dependency: "direct main" description: @@ -1452,14 +1436,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.3.0" - xml2json: - dependency: transitive - description: - name: xml2json - sha256: c8cb35b83cce879c2ea86951fd257f4e765b0030a0298b35cf94f2b3d0f32095 - url: "https://pub.dev" - source: hosted - version: "5.3.6" yaml: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 066d65f8a..a4a35f09c 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -34,7 +34,6 @@ dependencies: provider: ^6.0.3 graphql_flutter: ^5.1.2 url_launcher: ^6.1.6 - flutter_i18n: ^0.33.0 flutter_svg: ^2.0.2 permission_handler: ^10.2.0 package_info_plus: ^4.0.1 # for about dialog @@ -90,6 +89,9 @@ dev_dependencies: # The following section is specific to Flutter. flutter: + # Used for localization (l10n) + 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. @@ -102,12 +104,10 @@ flutter: - assets/bayern/header-logo.png - assets/bayern/body-logo.png - assets/bayern/icon.png - - assets/bayern/translations/ - assets/nuernberg/header-logo.png - assets/nuernberg/body-logo.png - assets/nuernberg/background.png - assets/nuernberg/intro_slides/ - - assets/nuernberg/translations/ # An image asset can refer to one or more resolution-specific 'variants', see # https://flutter.dev/assets-and-images/#resolution-aware. From 453e3cbc89d32f8824d0f3d8984a7a446580b397 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Tue, 17 Oct 2023 10:52:51 +0200 Subject: [PATCH 31/60] 904: Use context extension for l10n --- frontend/lib/about/about_page.dart | 22 +- frontend/lib/about/license_page.dart | 6 +- frontend/lib/category_assets.dart | 315 +++++++++--------- frontend/lib/home/home_page.dart | 10 +- .../activation_code_scanner_page.dart | 18 +- .../activation_overwrite_existing_dialog.dart | 10 +- .../card_detail_view/card_detail_view.dart | 20 +- .../card_detail_view/more_actions_dialog.dart | 6 +- .../connection_failed_dialog.dart | 4 +- .../identification/id_card/card_content.dart | 8 +- frontend/lib/identification/info_dialog.dart | 4 +- .../qr_code_camera_permission_dialog.dart | 10 +- .../qr_code_scanner/qr_code_scanner.dart | 4 +- .../qr_code_scanner_controls.dart | 6 +- .../qr_parsing_error_dialog.dart | 6 +- .../negative_verification_result_dialog.dart | 4 +- .../positive_verification_result_dialog.dart | 6 +- .../dialogs/verification_info_dialog.dart | 14 +- .../verification_qr_scanner_page.dart | 14 +- frontend/lib/intro_slides/intro_screen.dart | 8 +- .../intro_slides/location_request_button.dart | 10 +- frontend/lib/location/determine_position.dart | 4 +- frontend/lib/location/dialogs.dart | 18 +- frontend/lib/map/location_button.dart | 6 +- frontend/lib/map/map/attribution_dialog.dart | 6 +- frontend/lib/map/map/map.dart | 4 +- .../preview/accepting_store_preview_card.dart | 4 +- frontend/lib/search/filter_bar.dart | 6 +- frontend/lib/search/location_button.dart | 4 +- frontend/lib/search/results_loader.dart | 8 +- frontend/lib/search/search_page.dart | 4 +- .../accepting_store_summary.dart | 11 +- .../store_widgets/detail/detail_app_bar.dart | 15 +- .../store_widgets/detail/detail_content.dart | 12 +- .../lib/store_widgets/detail/detail_page.dart | 10 +- frontend/lib/util/color_utils.dart | 4 +- frontend/lib/util/{i18n.dart => l10n.dart} | 4 +- frontend/lib/widgets/app_bars.dart | 4 +- 38 files changed, 316 insertions(+), 313 deletions(-) rename frontend/lib/util/{i18n.dart => l10n.dart} (53%) diff --git a/frontend/lib/about/about_page.dart b/frontend/lib/about/about_page.dart index 9005c7cf2..d91d87a11 100644 --- a/frontend/lib/about/about_page.dart +++ b/frontend/lib/about/about_page.dart @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class AboutPage extends StatefulWidget { final countToEnableSwitch = 10; @@ -70,14 +70,14 @@ class AboutPageState extends State { child: Column( children: [ Center( - child: Text(t(context).about_publisher, style: Theme.of(context).textTheme.titleSmall), + child: Text(context.l10n.about_publisher, style: Theme.of(context).textTheme.titleSmall), ), Padding( padding: const EdgeInsets.only(left: 10, right: 10, top: 16, bottom: 16), child: Text(buildConfig.publisherAddress, style: Theme.of(context).textTheme.bodyLarge), ), Text( - t(context).about_moreInformation, + context.l10n.about_moreInformation, style: Theme.of(context) .textTheme .bodyMedium @@ -91,7 +91,7 @@ class AboutPageState extends State { context, AppRoute( builder: (context) => - ContentPage(title: t(context).about_publisher, children: getPublisherText(context)), + ContentPage(title: context.l10n.about_publisher, children: getPublisherText(context)), ), ); }, @@ -101,20 +101,20 @@ class AboutPageState extends State { thickness: 1, ), const SizedBox(height: 20), - ContentTile(icon: Icons.copyright, title: t(context).about_license, children: getCopyrightText(context)), + ContentTile(icon: Icons.copyright, title: context.l10n.about_license, children: getCopyrightText(context)), ListTile( leading: const Icon(Icons.privacy_tip_outlined), - title: Text(t(context).about_privacyDeclaration), + title: Text(context.l10n.about_privacyDeclaration), onTap: () => launchUrlString(buildConfig.dataPrivacyPolicyUrl, mode: LaunchMode.externalApplication), ), ContentTile( icon: Icons.info_outline, - title: t(context).about_disclaimer, + title: context.l10n.about_disclaimer, children: getDisclaimerText(context), ), ListTile( leading: const Icon(Icons.book_outlined), - title: Text(t(context).about_dependencies), + title: Text(context.l10n.about_dependencies), onTap: () { Navigator.push( context, @@ -126,7 +126,7 @@ class AboutPageState extends State { ), ListTile( leading: const Icon(Icons.code_outlined), - title: Text(t(context).about_sourceCode), + title: Text(context.l10n.about_sourceCode), onTap: () { launchUrlString( 'https://github.com/digitalfabrik/entitlementcard', @@ -137,11 +137,11 @@ class AboutPageState extends State { if (config.showDevSettings) ListTile( leading: const Icon(Icons.build), - title: Text(t(context).about_developmentOptions), + title: Text(context.l10n.about_developmentOptions), onTap: () => showDialog( context: context, builder: (context) => - SimpleDialog(title: Text(t(context).about_developmentOptions), children: [DevSettingsView()]), + SimpleDialog(title: Text(context.l10n.about_developmentOptions), children: [DevSettingsView()]), ), ) ]; diff --git a/frontend/lib/about/license_page.dart b/frontend/lib/about/license_page.dart index 84a8ac6eb..e84b41742 100644 --- a/frontend/lib/about/license_page.dart +++ b/frontend/lib/about/license_page.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/widgets/top_loading_spinner.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class CustomLicenseEntry { final String packageName; @@ -53,7 +53,7 @@ class CustomLicensePage extends StatelessWidget { return CustomScrollView( slivers: [ - CustomSliverAppBar(title: t(context).about_licenses), + CustomSliverAppBar(title: context.l10n.about_licenses), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { @@ -61,7 +61,7 @@ class CustomLicensePage extends StatelessWidget { final paragraphs = license.licenseParagraphs; return ListTile( title: Text(license.packageName), - subtitle: Text(t(context).about_numberLicenses(paragraphs.length)), + subtitle: Text(context.l10n.about_numberLicenses(paragraphs.length)), onTap: () { Navigator.push( context, diff --git a/frontend/lib/category_assets.dart b/frontend/lib/category_assets.dart index 455140581..2d402ac44 100644 --- a/frontend/lib/category_assets.dart +++ b/frontend/lib/category_assets.dart @@ -1,6 +1,5 @@ -import 'dart:ui'; - -import 'package:meta/meta.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:flutter/material.dart'; const test = Color(0xff5f5384); @@ -30,157 +29,159 @@ class CategoryAsset { int get hashCode => id.hashCode; } -const List categoryAssets = [ - CategoryAsset( - id: 0, - name: 'categories.mobilityLong', - shortName: 'categories.mobility', - icon: 'assets/category_icons/0.svg', - detailIcon: 'assets/detail_headers/0_auto.svg', - color: Color(0xffE89600), - ), - CategoryAsset( - id: 1, - name: 'categories.media', - shortName: 'categories.media', - icon: 'assets/category_icons/1.svg', - detailIcon: 'assets/detail_headers/1_multimedia.svg', - color: Color(0xFFFA0000), - ), - CategoryAsset( - id: 2, - name: 'categories.healthLong', - shortName: 'categories.health', - icon: 'assets/category_icons/2.svg', - detailIcon: 'assets/detail_headers/2_sport.svg', - color: Color(0xFFE500D3), - ), - CategoryAsset( - id: 3, - name: 'categories.cultureLong', - shortName: 'categories.culture', - icon: 'assets/category_icons/3.svg', - detailIcon: 'assets/detail_headers/3_kultur.svg', - color: Color(0xFF7500EB), - ), - CategoryAsset( - id: 4, - name: 'categories.servicesLong', - shortName: 'categories.services', - icon: 'assets/category_icons/4.svg', - detailIcon: 'assets/detail_headers/4_finanzen.svg', - color: Color(0xFF515151), - ), - CategoryAsset( - id: 5, - name: 'categories.fashionLong', - shortName: 'categories.fashion', - icon: 'assets/category_icons/5.svg', - detailIcon: 'assets/detail_headers/5_mode.svg', - color: Color(0xFF6EBE00), - ), - CategoryAsset( - id: 6, - name: 'categories.livingLong', - shortName: 'categories.living', - icon: 'assets/category_icons/6.svg', - detailIcon: 'assets/detail_headers/6_haus.svg', - color: Color(0xFF00D0C7), - ), - CategoryAsset( - id: 7, - name: 'categories.leisureLong', - shortName: 'categories.leisure', - icon: 'assets/category_icons/7.svg', - detailIcon: 'assets/detail_headers/7_freizeit.svg', - color: Color(0xFF007CE8), - ), - CategoryAsset( - id: 8, - name: 'categories.foodLong', - shortName: 'categories.food', - icon: 'assets/category_icons/8.svg', - detailIcon: 'assets/detail_headers/8_essen.svg', - color: Color(0xFF197489), - ), - CategoryAsset( - id: 9, - name: 'categories.other', - shortName: 'categories.other', - icon: 'assets/category_icons/9.svg', - detailIcon: null, - color: Color(0xFFc51162), - ), - CategoryAsset( - id: 10, - name: 'categories.lunchTables', - shortName: 'categories.lunchTables', - icon: 'assets/category_icons/10.svg', - detailIcon: 'assets/detail_headers/10_mittagstische.svg', - color: Color(0xFF197489), - ), - CategoryAsset( - id: 11, - name: 'categories.clothingLong', - shortName: 'categories.clothing', - icon: 'assets/category_icons/11.svg', - detailIcon: 'assets/detail_headers/11_kleidung.svg', - color: Color(0xFF6EBE00), - ), - CategoryAsset( - id: 12, - name: 'categories.cultureLongAlternative', - shortName: 'categories.culture', - icon: 'assets/category_icons/12.svg', - detailIcon: 'assets/detail_headers/12_kultur.svg', - color: Color(0xFF7500EB), - ), - CategoryAsset( - id: 13, - name: 'categories.education', - shortName: 'categories.education', - icon: 'assets/category_icons/13.svg', - detailIcon: null, - color: Color(0xFFd100cc), - ), - CategoryAsset( - id: 14, - name: 'categories.moviesLong', - shortName: 'categories.movies', - icon: 'assets/category_icons/14.svg', - detailIcon: null, - color: Color(0xFFc51162), - ), - CategoryAsset( - id: 15, - name: 'categories.pharmaciesLong', - shortName: 'categories.pharmacies', - icon: 'assets/category_icons/15.svg', - detailIcon: null, - color: Color(0xFF007be0), - ), - CategoryAsset( - id: 16, - name: 'categories.digitalParticipation', - shortName: 'categories.digitalParticipation', - icon: 'assets/category_icons/16.svg', - detailIcon: 'assets/detail_headers/16_teilhabe.svg', - color: Color(0xFFFA0000), - ), - CategoryAsset( - id: 17, - name: 'categories.sports', - shortName: 'categories.sports', - icon: 'assets/category_icons/17.svg', - detailIcon: 'assets/detail_headers/17_sport.svg', - color: Color(0xFF00ccc6), - ), - CategoryAsset( - id: 18, - name: 'categories.mobility', - shortName: 'categories.mobility', - icon: 'assets/category_icons/18.svg', - detailIcon: 'assets/detail_headers/18_mobilitaet.svg', - color: Color(0xffE89600), - ), -]; +List categoryAssets(BuildContext context) { + return [ + CategoryAsset( + id: 0, + name: context.l10n.category_mobilityLong, + shortName: context.l10n.category_mobility, + icon: 'assets/category_icons/0.svg', + detailIcon: 'assets/detail_headers/0_auto.svg', + color: Color(0xffE89600), + ), + CategoryAsset( + id: 1, + name: context.l10n.category_media, + shortName: context.l10n.category_media, + icon: 'assets/category_icons/1.svg', + detailIcon: 'assets/detail_headers/1_multimedia.svg', + color: Color(0xFFFA0000), + ), + CategoryAsset( + id: 2, + name: context.l10n.category_healthLong, + shortName: context.l10n.category_health, + icon: 'assets/category_icons/2.svg', + detailIcon: 'assets/detail_headers/2_sport.svg', + color: Color(0xFFE500D3), + ), + CategoryAsset( + id: 3, + name: context.l10n.category_cultureLong, + shortName: context.l10n.category_culture, + icon: 'assets/category_icons/3.svg', + detailIcon: 'assets/detail_headers/3_kultur.svg', + color: Color(0xFF7500EB), + ), + CategoryAsset( + id: 4, + name: context.l10n.category_servicesLong, + shortName: context.l10n.category_services, + icon: 'assets/category_icons/4.svg', + detailIcon: 'assets/detail_headers/4_finanzen.svg', + color: Color(0xFF515151), + ), + CategoryAsset( + id: 5, + name: context.l10n.category_fashionLong, + shortName: context.l10n.category_fashion, + icon: 'assets/category_icons/5.svg', + detailIcon: 'assets/detail_headers/5_mode.svg', + color: Color(0xFF6EBE00), + ), + CategoryAsset( + id: 6, + name: context.l10n.category_livingLong, + shortName: context.l10n.category_living, + icon: 'assets/category_icons/6.svg', + detailIcon: 'assets/detail_headers/6_haus.svg', + color: Color(0xFF00D0C7), + ), + CategoryAsset( + id: 7, + name: context.l10n.category_leisureLong, + shortName: context.l10n.category_leisure, + icon: 'assets/category_icons/7.svg', + detailIcon: 'assets/detail_headers/7_freizeit.svg', + color: Color(0xFF007CE8), + ), + CategoryAsset( + id: 8, + name: context.l10n.category_foodLong, + shortName: context.l10n.category_food, + icon: 'assets/category_icons/8.svg', + detailIcon: 'assets/detail_headers/8_essen.svg', + color: Color(0xFF197489), + ), + CategoryAsset( + id: 9, + name: context.l10n.category_other, + shortName: context.l10n.category_other, + icon: 'assets/category_icons/9.svg', + detailIcon: null, + color: Color(0xFFc51162), + ), + CategoryAsset( + id: 10, + name: context.l10n.category_lunchTables, + shortName: context.l10n.category_lunchTables, + icon: 'assets/category_icons/10.svg', + detailIcon: 'assets/detail_headers/10_mittagstische.svg', + color: Color(0xFF197489), + ), + CategoryAsset( + id: 11, + name: context.l10n.category_clothingLong, + shortName: context.l10n.category_clothing, + icon: 'assets/category_icons/11.svg', + detailIcon: 'assets/detail_headers/11_kleidung.svg', + color: Color(0xFF6EBE00), + ), + CategoryAsset( + id: 12, + name: context.l10n.category_cultureLongAlternative, + shortName: context.l10n.category_culture, + icon: 'assets/category_icons/12.svg', + detailIcon: 'assets/detail_headers/12_kultur.svg', + color: Color(0xFF7500EB), + ), + CategoryAsset( + id: 13, + name: context.l10n.category_education, + shortName: context.l10n.category_education, + icon: 'assets/category_icons/13.svg', + detailIcon: null, + color: Color(0xFFd100cc), + ), + CategoryAsset( + id: 14, + name: context.l10n.category_moviesLong, + shortName: context.l10n.category_movies, + icon: 'assets/category_icons/14.svg', + detailIcon: null, + color: Color(0xFFc51162), + ), + CategoryAsset( + id: 15, + name: context.l10n.category_pharmaciesLong, + shortName: context.l10n.category_pharmacies, + icon: 'assets/category_icons/15.svg', + detailIcon: null, + color: Color(0xFF007be0), + ), + CategoryAsset( + id: 16, + name: context.l10n.category_digitalParticipation, + shortName: context.l10n.category_digitalParticipation, + icon: 'assets/category_icons/16.svg', + detailIcon: 'assets/detail_headers/16_teilhabe.svg', + color: Color(0xFFFA0000), + ), + CategoryAsset( + id: 17, + name: context.l10n.category_sports, + shortName: context.l10n.category_sports, + icon: 'assets/category_icons/17.svg', + detailIcon: 'assets/detail_headers/17_sport.svg', + color: Color(0xFF00ccc6), + ), + CategoryAsset( + id: 18, + name: context.l10n.category_mobility, + shortName: context.l10n.category_mobility, + icon: 'assets/category_icons/18.svg', + detailIcon: 'assets/detail_headers/18_mobilitaet.svg', + color: Color(0xffE89600), + ), + ]; +} diff --git a/frontend/lib/home/home_page.dart b/frontend/lib/home/home_page.dart index 69c97ac14..c00b57c39 100644 --- a/frontend/lib/home/home_page.dart +++ b/frontend/lib/home/home_page.dart @@ -11,7 +11,7 @@ import 'package:ehrenamtskarte/search/search_page.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; const mapTabIndex = 0; @@ -39,23 +39,23 @@ class HomePageState extends State { selectAcceptingStore: (id) => setState(() => selectedAcceptingStoreId = id), ), Icons.map_outlined, - t(context).map_title, + context.l10n.map_title, GlobalKey(debugLabel: 'Map tab key'), ), AppFlow( const SearchPage(), Icons.search_outlined, - t(context).search_title, + context.l10n.search_title, GlobalKey(debugLabel: 'Search tab key'), ), if (buildConfig.featureFlags.verification) AppFlow( IdentificationPage(), Icons.remove_red_eye_outlined, - t(context).identification_title, + context.l10n.identification_title, GlobalKey(debugLabel: 'Auth tab key'), ), - AppFlow(const AboutPage(), Icons.info_outline, t(context).about_title, + AppFlow(const AboutPage(), Icons.info_outline, context.l10n.about_title, GlobalKey(debugLabel: 'About tab key')), ]; } diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index dfd84a169..c816d0b4a 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -24,7 +24,7 @@ import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; class ActivationCodeScannerPage extends StatelessWidget { const ActivationCodeScannerPage({super.key}); @@ -53,19 +53,19 @@ class ActivationCodeScannerPage extends StatelessWidget { await _activateCode(context, activationCode); } on ActivationDidNotOverwriteExisting catch (_) { - await showError(t(context).identification_cardAlreadyActivated, null); + await showError(context.l10n.identification_cardAlreadyActivated, null); } on QrCodeFieldMissingException catch (e) { - await showError(t(context).identification_codeInvalidMissing(e.missingFieldName), null); + await showError(context.l10n.identification_codeInvalidMissing(e.missingFieldName), null); } on QrCodeWrongTypeException catch (_) { - await showError(t(context).identification_codeSavingFailed, null); + await showError(context.l10n.identification_codeSavingFailed, null); } on CardExpiredException catch (e) { final expirationDate = DateFormat('dd.MM.yyyy').format(e.expiry); - await showError(t(context).identification_codeExpired(expirationDate), null); + await showError(context.l10n.identification_codeExpired(expirationDate), null); } on ServerCardActivationException catch (_) { - await ConnectionFailedDialog.show(context, t(context).identification_codeVerificationFailedConnection); + await ConnectionFailedDialog.show(context, context.l10n.identification_codeVerificationFailedConnection); } on Exception catch (e, stacktrace) { debugPrintStack(stackTrace: stacktrace, label: e.toString()); - await showError(t(context).identification_codeUnknownError, null); + await showError(context.l10n.identification_codeUnknownError, null); } } @@ -110,7 +110,7 @@ class ActivationCodeScannerPage extends StatelessWidget { case ActivationState.failed: await QrParsingErrorDialog.showErrorDialog( context, - t(context).identification_codeInvalid, + context.l10n.identification_codeInvalid, ); break; case ActivationState.didNotOverwriteExisting: @@ -124,7 +124,7 @@ class ActivationCodeScannerPage extends StatelessWidget { } break; default: - throw ServerCardActivationException(t(context).identification_activationInvalidState); + throw ServerCardActivationException(context.l10n.identification_activationInvalidState); } } } diff --git a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart index cdb2402e3..6ea418e0c 100644 --- a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart +++ b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; class ActivationOverwriteExistingDialog extends StatelessWidget { const ActivationOverwriteExistingDialog({super.key}); @@ -8,23 +8,23 @@ class ActivationOverwriteExistingDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(t(context).identification_activateCardCurrentDevice, style: TextStyle(fontSize: 18)), + title: Text(context.l10n.identification_activateCardCurrentDevice, style: TextStyle(fontSize: 18)), content: SingleChildScrollView( child: ListBody( children: [ - Text(t(context).identification_activateCardCurrentDeviceRationale), + Text(context.l10n.identification_activateCardCurrentDeviceRationale), ], ), ), actions: [ TextButton( - child: Text(t(context).common_cancel), + child: Text(context.l10n.common_cancel), onPressed: () { Navigator.of(context).pop(false); }, ), TextButton( - child: Text(t(context).identification_activate), + child: Text(context.l10n.identification_activate), onPressed: () { Navigator.of(context).pop(true); }, diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index 3667503e1..39d32c401 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; import '../user_code_model.dart'; import 'verification_code_view.dart'; @@ -188,36 +188,36 @@ class QrCodeAndStatus extends StatelessWidget { children: [ ...switch (status) { CardStatus.expired => [ - _PaddedText(t(context).identification_cardExpired) + _PaddedText(context.l10n.identification_cardExpired) ], CardStatus.notVerifiedLately => [ - _PaddedText(t(context).identification_checkFailed), + _PaddedText(context.l10n.identification_checkFailed), Flexible( child: TextButton.icon( icon: const Icon(Icons.refresh), onPressed: onSelfVerifyPressed, - label: Text(t(context).identification_checkAgain), + label: Text(context.l10n.identification_checkAgain), ), ), ], CardStatus.timeOutOfSync => [ - _PaddedText(t(context).identification_timeIncorrect), + _PaddedText(context.l10n.identification_timeIncorrect), Flexible( child: TextButton.icon( icon: const Icon(Icons.refresh), onPressed: onSelfVerifyPressed, - label: Text(t(context).identification_checkAgain), + label: Text(context.l10n.identification_checkAgain), )) ], CardStatus.invalid => [ - _PaddedText(t(context).identification_cardInvalid) + _PaddedText(context.l10n.identification_cardInvalid) ], CardStatus.valid => [ - _PaddedText(t(context).identification_authenticationPossible), + _PaddedText(context.l10n.identification_authenticationPossible), Flexible(child: VerificationCodeView(userCode: userCode)) ], CardStatus.notYetValid => [ - _PaddedText(t(context).identification_cardNotYetValid), + _PaddedText(context.l10n.identification_cardNotYetValid), ] }, Container( @@ -225,7 +225,7 @@ class QrCodeAndStatus extends StatelessWidget { child: TextButton( onPressed: onMoreActionsPressed, child: Text( - t(context).common_moreActions, + context.l10n.common_moreActions, style: TextStyle(color: Theme.of(context).colorScheme.secondary), ), ), diff --git a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart index 7a682b75d..67e399008 100644 --- a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart +++ b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart @@ -1,7 +1,7 @@ import 'package:ehrenamtskarte/build_config/build_config.dart'; import 'package:flutter/material.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; class MoreActionsDialog extends StatelessWidget { final VoidCallback startActivation; @@ -20,7 +20,7 @@ class MoreActionsDialog extends StatelessWidget { final localization = buildConfig.localization.identification.moreActions; return AlertDialog( contentPadding: const EdgeInsets.only(top: 12), - title: Text(t(context).common_moreActions), + title: Text(context.l10n.common_moreActions), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -55,7 +55,7 @@ class MoreActionsDialog extends StatelessWidget { ], ), ), - actions: [TextButton(onPressed: () => Navigator.pop(context), child: Text(t(context).common_cancel))], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: Text(context.l10n.common_cancel))], ); } } diff --git a/frontend/lib/identification/connection_failed_dialog.dart b/frontend/lib/identification/connection_failed_dialog.dart index 7241b3133..a49c442f2 100644 --- a/frontend/lib/identification/connection_failed_dialog.dart +++ b/frontend/lib/identification/connection_failed_dialog.dart @@ -1,7 +1,7 @@ import 'package:ehrenamtskarte/identification/info_dialog.dart'; import 'package:flutter/material.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class ConnectionFailedDialog extends StatelessWidget { final String reason; @@ -11,7 +11,7 @@ class ConnectionFailedDialog extends StatelessWidget { @override Widget build(BuildContext context) { return InfoDialog( - title: t(context).common_connectionFailed, + title: context.l10n.common_connectionFailed, icon: Icons.signal_cellular_connected_no_internet_4_bar, iconColor: Theme.of(context).colorScheme.onBackground, child: Text(reason), diff --git a/frontend/lib/identification/id_card/card_content.dart b/frontend/lib/identification/id_card/card_content.dart index 7fa27c1a5..b7d301a83 100644 --- a/frontend/lib/identification/id_card/card_content.dart +++ b/frontend/lib/identification/id_card/card_content.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/util/color_utils.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; Color standardCardColor = getColorFromHex(buildConfig.cardBranding.colorStandard); Color premiumCardColor = getColorFromHex(buildConfig.cardBranding.colorPremium); @@ -52,7 +52,7 @@ class CardContent extends StatelessWidget { final expirationDay = cardInfo.hasExpirationDay() ? cardInfo.expirationDay : null; return expirationDay != null ? DateFormat('dd.MM.yyyy').format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: expirationDay))) - : t(context).identification_unlimited; + : context.l10n.identification_unlimited; } String? get _formattedBirthday { @@ -77,8 +77,8 @@ class CardContent extends StatelessWidget { String _getCardValidityDate(BuildContext context, String? startDate, String expirationDate) { return startDate != null - ? t(context).identification_validFromUntil(expirationDate, startDate) - : t(context).identification_validUntil(expirationDate); + ? context.l10n.identification_validFromUntil(expirationDate, startDate) + : context.l10n.identification_validUntil(expirationDate); } @override diff --git a/frontend/lib/identification/info_dialog.dart b/frontend/lib/identification/info_dialog.dart index b9228a4c2..c63f8fef8 100644 --- a/frontend/lib/identification/info_dialog.dart +++ b/frontend/lib/identification/info_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class InfoDialog extends StatelessWidget { final Widget child; @@ -25,7 +25,7 @@ class InfoDialog extends StatelessWidget { title: Text(title, style: theme.textTheme.headlineSmall), ), content: child, - actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(t(context).common_ok))], + actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.l10n.common_ok))], ); } } diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart index 176884ca4..1e0b2c00b 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; class QrCodeCameraPermissionDialog extends StatelessWidget { const QrCodeCameraPermissionDialog(); @@ -9,23 +9,23 @@ class QrCodeCameraPermissionDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(t(context).identification_cameraAccessRequired, style: TextStyle(fontSize: 18)), + title: Text(context.l10n.identification_cameraAccessRequired, style: TextStyle(fontSize: 18)), content: SingleChildScrollView( child: ListBody( children: [ - Text(t(context).identification_cameraAccessRequired), + Text(context.l10n.identification_cameraAccessRequired), ], ), ), actions: [ TextButton( - child: Text(t(context).common_cancel), + child: Text(context.l10n.common_cancel), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( - child: Text(t(context).common_openSettings), + child: Text(context.l10n.common_openSettings), onPressed: () { openAppSettings(); }, diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart index de99d00ad..684ca3c36 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_overlay_shape.d import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; typedef OnCodeScannedCallback = Future Function(Uint8List code); @@ -77,7 +77,7 @@ class _QRViewState extends State { children: [ Container( margin: const EdgeInsets.all(8), - child: Text(t(context).identification_scanQRCode), + child: Text(context.l10n.identification_scanQRCode), ), QrCodeScannerControls(controller: controller) ], diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart index b5850e4be..3a7d9cc12 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; class QrCodeScannerControls extends StatelessWidget { final MobileScannerController controller; @@ -21,7 +21,7 @@ class QrCodeScannerControls extends StatelessWidget { child: ValueListenableBuilder( valueListenable: controller.torchState, builder: (ctx, state, child) => Text( - state == TorchState.on ? t(context).identification_flashOff : t(context).identification_flashOn, + state == TorchState.on ? context.l10n.identification_flashOff : context.l10n.identification_flashOn, style: const TextStyle(fontSize: 16), ), ), @@ -34,7 +34,7 @@ class QrCodeScannerControls extends StatelessWidget { child: ValueListenableBuilder( valueListenable: controller.cameraFacingState, builder: (ctx, state, child) => Text( - state == CameraFacing.back ? t(context).identification_selfieCamera : t(context).identification_standardCamera, + state == CameraFacing.back ? context.l10n.identification_selfieCamera : context.l10n.identification_standardCamera, style: const TextStyle(fontSize: 16), ), ), diff --git a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart index e349e5f3c..7f28dcc74 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; class QrParsingErrorDialog extends StatelessWidget { final String message; @@ -10,7 +10,7 @@ class QrParsingErrorDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(t(context).identification_scanningFailed), + title: Text(context.l10n.identification_scanningFailed), content: SingleChildScrollView( child: ListBody( children: [ @@ -20,7 +20,7 @@ class QrParsingErrorDialog extends StatelessWidget { ), actions: [ TextButton( - child: Text(t(context).common_ok), + child: Text(context.l10n.common_ok), onPressed: () { Navigator.of(context).pop(); }, diff --git a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart index 664361358..a683db84a 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart @@ -1,7 +1,7 @@ import 'package:ehrenamtskarte/identification/info_dialog.dart'; import 'package:flutter/material.dart'; -import '../../../util/i18n.dart'; +import '../../../util/l10n.dart'; class NegativeVerificationResultDialog extends StatelessWidget { final String reason; @@ -11,7 +11,7 @@ class NegativeVerificationResultDialog extends StatelessWidget { @override Widget build(BuildContext context) { return InfoDialog( - title: t(context).identification_notVerified, + title: context.l10n.identification_notVerified, icon: Icons.error, iconColor: Colors.red, child: Text(reason), diff --git a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart index bd9219482..cb0502160 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart @@ -7,7 +7,7 @@ import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; -import '../../../util/i18n.dart'; +import '../../../util/l10n.dart'; class PositiveVerificationResultDialog extends StatefulWidget { final CardInfo cardInfo; @@ -57,7 +57,7 @@ class PositiveVerificationResultDialogState extends State _onDone(context), ) ], diff --git a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart index d18b16c70..9d8cd5334 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart @@ -20,7 +20,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; class VerificationQrScannerPage extends StatelessWidget { const VerificationQrScannerPage({super.key}); @@ -77,7 +77,7 @@ class VerificationQrScannerPage extends StatelessWidget { if (cardInfo == null) { await _onError( context, - t(context).identification_codeVerificationFailed, + context.l10n.identification_codeVerificationFailed, ); } else { await _onSuccess(context, cardInfo, qrcode.hasStaticVerificationCode()); @@ -85,32 +85,32 @@ class VerificationQrScannerPage extends StatelessWidget { } on ServerVerificationException catch (e) { await _onConnectionError( context, - t(context).identification_codeVerificationFailedConnection, + context.l10n.identification_codeVerificationFailedConnection, e, ); } on QrCodeFieldMissingException catch (e) { await _onError( context, - t(context).identification_codeInvalidMissing(e.missingFieldName), + context.l10n.identification_codeInvalidMissing(e.missingFieldName), e, ); } on CardExpiredException catch (e) { final expirationDate = DateFormat('dd.MM.yyyy').format(e.expiry); await _onError( context, - t(context).identification_codeExpired(expirationDate), + context.l10n.identification_codeExpired(expirationDate), e, ); } on QrCodeParseException catch (e) { await _onError( context, - t(context).identification_codeInvalid, + context.l10n.identification_codeInvalid, e, ); } on Exception catch (e) { await _onError( context, - t(context).identification_codeUnknownError, + context.l10n.identification_codeUnknownError, e, ); } finally { diff --git a/frontend/lib/intro_slides/intro_screen.dart b/frontend/lib/intro_slides/intro_screen.dart index 854721d13..4aa5579d1 100644 --- a/frontend/lib/intro_slides/intro_screen.dart +++ b/frontend/lib/intro_slides/intro_screen.dart @@ -4,7 +4,7 @@ import 'package:ehrenamtskarte/intro_slides/location_request_button.dart'; import 'package:flutter/material.dart'; import 'package:intro_slider/intro_slider.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; typedef OnFinishedCallback = void Function(); @@ -23,9 +23,9 @@ class IntroScreen extends StatelessWidget { final theme = Theme.of(context); return IntroSlider( onDonePress: () => onDonePress(context), - renderDoneBtn: Text(t(context).common_done), - renderNextBtn: Text(t(context).common_next), - renderPrevBtn: Text(t(context).common_previous), + renderDoneBtn: Text(context.l10n.common_done), + renderNextBtn: Text(context.l10n.common_next), + renderPrevBtn: Text(context.l10n.common_previous), doneButtonStyle: Theme.of(context).textButtonTheme.style, indicatorConfig: IndicatorConfig( colorActiveIndicator: theme.colorScheme.primary, diff --git a/frontend/lib/intro_slides/location_request_button.dart b/frontend/lib/intro_slides/location_request_button.dart index 3ed03b7a3..314891dab 100644 --- a/frontend/lib/intro_slides/location_request_button.dart +++ b/frontend/lib/intro_slides/location_request_button.dart @@ -3,7 +3,7 @@ import 'package:ehrenamtskarte/location/determine_position.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class LocationRequestButton extends StatefulWidget { const LocationRequestButton({super.key}); @@ -53,7 +53,7 @@ class _LocationRequestButtonState extends State { if (status == null) { return ElevatedButton( onPressed: null, - child: Text(t(context).location_checkSettings), + child: Text(context.l10n.location_checkSettings), ); } switch (status) { @@ -61,18 +61,18 @@ class _LocationRequestButtonState extends State { case LocationStatus.notSupported: return ElevatedButton( onPressed: () => _onLocationButtonClicked(settings), - child: Text(t(context).location_grantLocation), + child: Text(context.l10n.location_grantLocation), ); case LocationStatus.whileInUse: case LocationStatus.always: return ElevatedButton( onPressed: null, - child: Text(t(context).location_locationGranted), + child: Text(context.l10n.location_locationGranted), ); case LocationStatus.deniedForever: return ElevatedButton( onPressed: null, - child: Text(t(context).location_locationDeactivated), + child: Text(context.l10n.location_locationDeactivated), ); } } diff --git a/frontend/lib/location/determine_position.dart b/frontend/lib/location/determine_position.dart index 8ff8541c5..0630931d4 100644 --- a/frontend/lib/location/determine_position.dart +++ b/frontend/lib/location/determine_position.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:maplibre_gl/mapbox_gl.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; enum LocationStatus { /// This is the initial state on both Android and iOS, but on Android the @@ -145,7 +145,7 @@ Future checkAndRequestLocationPermission( // returned true. According to Android guidelines // your App should show an explanatory UI now. - final result = await showDialog(context: context, builder: (context) => RationaleDialog(rationale: rationale ?? t(context).location_activateLocationAccessRationale)); + final result = await showDialog(context: context, builder: (context) => RationaleDialog(rationale: rationale ?? context.l10n.location_activateLocationAccessRationale)); if (result == true) { return checkAndRequestLocationPermission( diff --git a/frontend/lib/location/dialogs.dart b/frontend/lib/location/dialogs.dart index 6c75847ed..c402a84ab 100644 --- a/frontend/lib/location/dialogs.dart +++ b/frontend/lib/location/dialogs.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class LocationServiceDialog extends StatelessWidget { const LocationServiceDialog({super.key}); @@ -8,11 +8,11 @@ class LocationServiceDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(t(context).location_activateLocationAccess), - content: Text(t(context).location_activateLocationAccessSettings), + title: Text(context.l10n.location_activateLocationAccess), + content: Text(context.l10n.location_activateLocationAccessSettings), actions: [ - TextButton(child: Text(t(context).common_cancel), onPressed: () => Navigator.of(context).pop(false)), - TextButton(child: Text(t(context).common_openSettings), onPressed: () => Navigator.of(context).pop(true)) + TextButton(child: Text(context.l10n.common_cancel), onPressed: () => Navigator.of(context).pop(false)), + TextButton(child: Text(context.l10n.common_openSettings), onPressed: () => Navigator.of(context).pop(true)) ], ); } @@ -26,15 +26,15 @@ class RationaleDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(t(context).location_locationPermission), + title: Text(context.l10n.location_locationPermission), content: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: [Text(_rationale), Text(t(context).location_askPermissionsAgain)], + children: [Text(_rationale), Text(context.l10n.location_askPermissionsAgain)], ), actions: [ - TextButton(child: Text(t(context).location_grantPermission), onPressed: () => Navigator.of(context).pop(true)), - TextButton(child: Text(t(context).common_cancel), onPressed: () => Navigator.of(context).pop(false)) + TextButton(child: Text(context.l10n.location_grantPermission), onPressed: () => Navigator.of(context).pop(true)), + TextButton(child: Text(context.l10n.common_cancel), onPressed: () => Navigator.of(context).pop(false)) ], ); } diff --git a/frontend/lib/map/location_button.dart b/frontend/lib/map/location_button.dart index 5cf9f3dc0..f68dd12c5 100644 --- a/frontend/lib/map/location_button.dart +++ b/frontend/lib/map/location_button.dart @@ -4,7 +4,7 @@ import 'package:ehrenamtskarte/widgets/small_button_spinner.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class LocationButton extends StatefulWidget { final Future Function(RequestedPosition) bringCameraToUser; @@ -54,9 +54,9 @@ class _LocationButtonState extends State { messengerState.showSnackBar( SnackBar( behavior: SnackBarBehavior.floating, - content: Text(t(context).location_locationAccessDeactivated), + content: Text(context.l10n.location_locationAccessDeactivated), action: SnackBarAction( - label: t(context).common_settings, + label: context.l10n.common_settings, onPressed: () async { await openSettingsToGrantPermissions(context); }, diff --git a/frontend/lib/map/map/attribution_dialog.dart b/frontend/lib/map/map/attribution_dialog.dart index 7907ac2e7..4f056650a 100644 --- a/frontend/lib/map/map/attribution_dialog.dart +++ b/frontend/lib/map/map/attribution_dialog.dart @@ -2,7 +2,7 @@ import 'package:ehrenamtskarte/map/map/attribution_dialog_item.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; class AttributionDialog extends StatelessWidget { const AttributionDialog({super.key}); @@ -11,12 +11,12 @@ class AttributionDialog extends StatelessWidget { Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.primary; return SimpleDialog( - title: Text(t(context).map_mapData), + title: Text(context.l10n.map_mapData), children: [ AttributionDialogItem( icon: Icons.copyright, color: color, - text: t(context).map_osmContributors, + text: context.l10n.map_osmContributors, onPressed: () { launchUrlString('https://www.openstreetmap.org/copyright', mode: LaunchMode.externalApplication); }, diff --git a/frontend/lib/map/map/map.dart b/frontend/lib/map/map/map.dart index 3ae0ab0d6..5db2af7e7 100644 --- a/frontend/lib/map/map/map.dart +++ b/frontend/lib/map/map/map.dart @@ -12,7 +12,7 @@ import 'package:geolocator/geolocator.dart'; import 'package:maplibre_gl/mapbox_gl.dart'; import 'package:tuple/tuple.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; typedef OnFeatureClickCallback = void Function(dynamic feature); typedef OnNoFeatureClickCallback = void Function(); @@ -98,7 +98,7 @@ class _MapContainerState extends State implements MapController { color: mapboxColor, iconSize: 20, icon: const Icon(Icons.info_outline), - tooltip: t(context).map_showMapCopyright, + tooltip: context.l10n.map_showMapCopyright, onPressed: () { showDialog( context: context, diff --git a/frontend/lib/map/preview/accepting_store_preview_card.dart b/frontend/lib/map/preview/accepting_store_preview_card.dart index c73a91bfe..55ed6bd14 100644 --- a/frontend/lib/map/preview/accepting_store_preview_card.dart +++ b/frontend/lib/map/preview/accepting_store_preview_card.dart @@ -4,7 +4,7 @@ import 'package:ehrenamtskarte/store_widgets/accepting_store_summary.dart'; import 'package:ehrenamtskarte/widgets/error_message.dart'; import 'package:flutter/material.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; class AcceptingStorePreviewError extends StatelessWidget { final void Function()? refetch; @@ -18,7 +18,7 @@ class AcceptingStorePreviewError extends StatelessWidget { child: Container( height: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16), - child: ErrorMessage(t(context).store_loadingInformationFailed) + child: ErrorMessage(context.l10n.store_loadingInformationFailed) ), ); } diff --git a/frontend/lib/search/filter_bar.dart b/frontend/lib/search/filter_bar.dart index 15a70b507..fe548a2e8 100644 --- a/frontend/lib/search/filter_bar.dart +++ b/frontend/lib/search/filter_bar.dart @@ -4,7 +4,7 @@ import 'package:ehrenamtskarte/category_assets.dart'; import 'package:ehrenamtskarte/search/filter_bar_button.dart'; import 'package:flutter/material.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class FilterBar extends StatelessWidget { final Function(CategoryAsset, bool) onCategoryPress; @@ -13,7 +13,7 @@ class FilterBar extends StatelessWidget { @override Widget build(BuildContext context) { - final sortedCategories = [...categoryAssets]; + final sortedCategories = [...categoryAssets(context)]; sortedCategories.removeWhere((category) => category.id == 9); sortedCategories.sort((a, b) => a.shortName.length.compareTo(b.shortName.length)); final filteredCategories = sortedCategories.where((element) => buildConfig.categories.contains(element.id)); @@ -25,7 +25,7 @@ class FilterBar extends StatelessWidget { padding: const EdgeInsets.all(8), child: Row( children: [ - Text(t(context).search_filterByCategories.toUpperCase(), maxLines: 1, style: const TextStyle(color: Colors.grey)), + Text(context.l10n.search_filterByCategories.toUpperCase(), maxLines: 1, style: const TextStyle(color: Colors.grey)), const Expanded(child: Padding(padding: EdgeInsets.only(left: 8), child: Divider(thickness: 0.7))) ], ), diff --git a/frontend/lib/search/location_button.dart b/frontend/lib/search/location_button.dart index fb865857c..326876224 100644 --- a/frontend/lib/search/location_button.dart +++ b/frontend/lib/search/location_button.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:provider/provider.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class LocationButton extends StatefulWidget { final void Function(Position position) setCoordinates; @@ -57,7 +57,7 @@ class _LocationButtonState extends State { ), ), label: Text( - t(context).search_findCloseBy, + context.l10n.search_findCloseBy, style: TextStyle(color: Theme.of(context).hintColor), ), ), diff --git a/frontend/lib/search/results_loader.dart b/frontend/lib/search/results_loader.dart index e04e2ea61..04df2df45 100644 --- a/frontend/lib/search/results_loader.dart +++ b/frontend/lib/search/results_loader.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class ResultsLoader extends StatefulWidget { final CoordinatesInput? coordinates; @@ -148,10 +148,10 @@ class ResultsLoaderState extends State { mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.warning, size: 60, color: Colors.orange), - Text(t(context).common_checkConnection), + Text(context.l10n.common_checkConnection), OutlinedButton( onPressed: _pagingController.retryLastFailedRequest, - child: Text(t(context).common_tryAgain), + child: Text(context.l10n.common_tryAgain), ) ], ), @@ -162,7 +162,7 @@ class ResultsLoaderState extends State { mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.search_off, size: 60, color: Theme.of(context).disabledColor), - Text(t(context).search_noAcceptingStoresFound), + Text(context.l10n.search_noAcceptingStoresFound), ], ), ); diff --git a/frontend/lib/search/search_page.dart b/frontend/lib/search/search_page.dart index 06df5c7e3..f2fffcaec 100644 --- a/frontend/lib/search/search_page.dart +++ b/frontend/lib/search/search_page.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/search/results_loader.dart'; import 'package:ehrenamtskarte/widgets/app_bars.dart'; import 'package:flutter/material.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class SearchPage extends StatefulWidget { const SearchPage({super.key}); @@ -39,7 +39,7 @@ class _SearchPageState extends State { child: Row( children: [ Text( - t(context).search_searchResults.toUpperCase(), + context.l10n.search_searchResults.toUpperCase(), style: const TextStyle(color: Colors.grey), ), const Expanded(child: Padding(padding: EdgeInsets.only(left: 8), child: Divider())) diff --git a/frontend/lib/store_widgets/accepting_store_summary.dart b/frontend/lib/store_widgets/accepting_store_summary.dart index 739b5314d..fb07acb4a 100644 --- a/frontend/lib/store_widgets/accepting_store_summary.dart +++ b/frontend/lib/store_widgets/accepting_store_summary.dart @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class AcceptingStoreSummary extends StatelessWidget { final AcceptingStoreSummaryModel store; @@ -38,8 +38,9 @@ class AcceptingStoreSummary extends StatelessWidget { @override Widget build(BuildContext context) { - final itemCategoryAsset = store.categoryId < categoryAssets.length ? categoryAssets[store.categoryId] : null; - final categoryName = itemCategoryAsset?.name ?? t(context).store_unknownCategory; + final categories = categoryAssets(context); + final itemCategoryAsset = store.categoryId < categories.length ? categories[store.categoryId] : null; + final categoryName = itemCategoryAsset?.name ?? context.l10n.store_unknownCategory; final categoryColor = itemCategoryAsset?.color; final useWideDepiction = MediaQuery.of(context).size.width > 400; @@ -154,14 +155,14 @@ class StoreTextOverview extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - store.name ?? t(context).store_acceptingStore, + store.name ?? context.l10n.store_acceptingStore, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 4), Text( - store.description ?? t(context).store_noDescriptionAvailable, + store.description ?? context.l10n.store_noDescriptionAvailable, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium, diff --git a/frontend/lib/store_widgets/detail/detail_app_bar.dart b/frontend/lib/store_widgets/detail/detail_app_bar.dart index b14118702..4652deb64 100644 --- a/frontend/lib/store_widgets/detail/detail_app_bar.dart +++ b/frontend/lib/store_widgets/detail/detail_app_bar.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/widgets/app_bars.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; const double bottomSize = 100; @@ -18,9 +18,10 @@ class DetailAppBarHeaderImage extends StatelessWidget { @override Widget build(BuildContext context) { final currentCategoryId = categoryId; + final categories = categoryAssets(context); - if (currentCategoryId != null && currentCategoryId <= categoryAssets.length) { - final currentDetailIcon = categoryAssets[currentCategoryId].detailIcon; + if (currentCategoryId != null && currentCategoryId <= categories.length) { + final currentDetailIcon = categories[currentCategoryId].detailIcon; if (currentDetailIcon != null) { return SvgPicture.asset( currentDetailIcon, @@ -86,10 +87,10 @@ class DetailAppBar extends StatelessWidget { @override Widget build(BuildContext context) { final categoryId = matchingStore.store.category.id; + final category = categoryAssets(context)[categoryId]; - final accentColor = getDarkenedColorForCategory(categoryId); - final categoryName = matchingStore.store.category.name; - final title = matchingStore.store.name ?? t(context).store_acceptingStore; + final accentColor = getDarkenedColorForCategory(context, categoryId); + final title = matchingStore.store.name ?? context.l10n.store_acceptingStore; final backgroundColor = accentColor ?? Theme.of(context).colorScheme.primary; final textColor = getReadableOnColor(backgroundColor); @@ -107,7 +108,7 @@ class DetailAppBar extends StatelessWidget { child: DetailAppBarBottom( title: title, categoryId: categoryId, - categoryName: categoryName, + categoryName: category.name, accentColor: accentColor, textColorGrey: textColorGrey, textColor: textColor, diff --git a/frontend/lib/store_widgets/detail/detail_content.dart b/frontend/lib/store_widgets/detail/detail_content.dart index 388ebd2b5..59d3d3e73 100644 --- a/frontend/lib/store_widgets/detail/detail_content.dart +++ b/frontend/lib/store_widgets/detail/detail_content.dart @@ -11,7 +11,7 @@ import 'package:maplibre_gl/mapbox_gl.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; class DetailContent extends StatelessWidget { final AcceptingStoreById$Query$PhysicalStore acceptingStore; @@ -61,7 +61,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.location_on, addressString, - t(context).store_address, + context.l10n.store_address, onTap: () => _launchMap(mapQueryString), iconColor: readableOnAccentColor, iconFillColor: accentColor, @@ -70,7 +70,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.language, prepareWebsiteUrlForDisplay(website), - t(context).store_website, + context.l10n.store_website, onTap: () => launchUrlString(prepareWebsiteUrlForLaunch(website), mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, @@ -80,7 +80,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.phone, telephone, - t(context).store_phone, + context.l10n.store_phone, onTap: () => launchUrlString('tel:${sanitizePhoneNumber(telephone)}', mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, @@ -90,7 +90,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.alternate_email, email, - t(context).store_email, + context.l10n.store_email, onTap: () => launchUrlString('mailto:${email.trim()}', mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, iconFillColor: accentColor, @@ -107,7 +107,7 @@ class DetailContent extends StatelessWidget { alignment: MainAxisAlignment.center, children: [ OutlinedButton( - child: Text(t(context).store_showOnMap), + child: Text(context.l10n.store_showOnMap), onPressed: () => _showOnMap(context), ), ], diff --git a/frontend/lib/store_widgets/detail/detail_page.dart b/frontend/lib/store_widgets/detail/detail_page.dart index a2197e382..e133ee2b5 100644 --- a/frontend/lib/store_widgets/detail/detail_page.dart +++ b/frontend/lib/store_widgets/detail/detail_page.dart @@ -9,7 +9,7 @@ import 'package:ehrenamtskarte/widgets/top_loading_spinner.dart'; import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; -import '../../util/i18n.dart'; +import '../../util/l10n.dart'; class DetailPage extends StatelessWidget { final int _acceptingStoreId; @@ -29,18 +29,18 @@ class DetailPage extends StatelessWidget { final data = result.data; if (result.hasException && exception != null) { - return DetailErrorMessage(message: t(context).store_loadingDataFailed, refetch: refetch); + return DetailErrorMessage(message: context.l10n.store_loadingDataFailed, refetch: refetch); } else if (result.isNotLoading && data != null) { final matchingStores = byIdQuery.parse(data).physicalStoresByIdInProject; if (matchingStores.length != 1) { - return DetailErrorMessage(message: t(context).store_loadingDataFailed, refetch: refetch); + return DetailErrorMessage(message: context.l10n.store_loadingDataFailed, refetch: refetch); } final matchingStore = matchingStores.first; if (matchingStore == null) { - return DetailErrorMessage(message: t(context).store_acceptingStoreNotFound); + return DetailErrorMessage(message: context.l10n.store_acceptingStoreNotFound); } final categoryId = matchingStore.store.category.id; - final accentColor = getDarkenedColorForCategory(categoryId); + final accentColor = getDarkenedColorForCategory(context, categoryId); return Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/frontend/lib/util/color_utils.dart b/frontend/lib/util/color_utils.dart index 9c4a9bc2c..dfd40578c 100644 --- a/frontend/lib/util/color_utils.dart +++ b/frontend/lib/util/color_utils.dart @@ -10,8 +10,8 @@ Color getReadableOnColorSecondary(Color backgroundColor) { return backgroundColor.computeLuminance() > 0.5 ? Colors.black54 : Colors.white54; } -Color? getDarkenedColorForCategory(int categoryId) { - final categoryColor = categoryAssets[categoryId].color; +Color? getDarkenedColorForCategory(BuildContext context, int categoryId) { + final categoryColor = categoryAssets(context)[categoryId].color; Color? categoryColorDark; if (categoryColor != null) { categoryColorDark = TinyColor.fromColor(categoryColor).darken().color; diff --git a/frontend/lib/util/i18n.dart b/frontend/lib/util/l10n.dart similarity index 53% rename from frontend/lib/util/i18n.dart rename to frontend/lib/util/l10n.dart index 0e62eeede..e381b76e6 100644 --- a/frontend/lib/util/i18n.dart +++ b/frontend/lib/util/l10n.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -AppLocalizations t (BuildContext context) { - return AppLocalizations.of(context)!; +extension L10n on BuildContext { + AppLocalizations get l10n => AppLocalizations.of(this)!; } diff --git a/frontend/lib/widgets/app_bars.dart b/frontend/lib/widgets/app_bars.dart index 6e728578d..8d3cf9941 100644 --- a/frontend/lib/widgets/app_bars.dart +++ b/frontend/lib/widgets/app_bars.dart @@ -5,7 +5,7 @@ library navigation_bars; import 'package:ehrenamtskarte/debouncer.dart'; import 'package:flutter/material.dart'; -import '../util/i18n.dart'; +import '../util/l10n.dart'; class CustomAppBar extends StatelessWidget { final String title; @@ -93,7 +93,7 @@ class SearchSliverAppBarState extends State { controller: textEditingController, focusNode: focusNode, decoration: InputDecoration.collapsed( - hintText: t(context).search_searchHint, + hintText: context.l10n.search_searchHint, hintStyle: TextStyle(color: foregroundColor?.withOpacity(0.8)), ), cursorColor: foregroundColor, From b506ddb2360f95ae8425f40731c1180f5e41c3da Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Tue, 17 Oct 2023 11:47:31 +0200 Subject: [PATCH 32/60] 904: Fixes --- .../identification/card_detail_view/card_detail_view.dart | 8 ++------ .../qr_code_scanner/qr_code_scanner_controls.dart | 4 +++- .../dialogs/positive_verification_result_dialog.dart | 4 +++- frontend/lib/location/determine_position.dart | 6 +++--- frontend/lib/location/dialogs.dart | 3 ++- .../lib/map/preview/accepting_store_preview_card.dart | 7 +++---- frontend/lib/search/filter_bar.dart | 3 ++- 7 files changed, 18 insertions(+), 17 deletions(-) diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index 39d32c401..fbcac7294 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -187,9 +187,7 @@ class QrCodeAndStatus extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ...switch (status) { - CardStatus.expired => [ - _PaddedText(context.l10n.identification_cardExpired) - ], + CardStatus.expired => [_PaddedText(context.l10n.identification_cardExpired)], CardStatus.notVerifiedLately => [ _PaddedText(context.l10n.identification_checkFailed), Flexible( @@ -209,9 +207,7 @@ class QrCodeAndStatus extends StatelessWidget { label: Text(context.l10n.identification_checkAgain), )) ], - CardStatus.invalid => [ - _PaddedText(context.l10n.identification_cardInvalid) - ], + CardStatus.invalid => [_PaddedText(context.l10n.identification_cardInvalid)], CardStatus.valid => [ _PaddedText(context.l10n.identification_authenticationPossible), Flexible(child: VerificationCodeView(userCode: userCode)) diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart index 3a7d9cc12..1599584ba 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart @@ -34,7 +34,9 @@ class QrCodeScannerControls extends StatelessWidget { child: ValueListenableBuilder( valueListenable: controller.cameraFacingState, builder: (ctx, state, child) => Text( - state == CameraFacing.back ? context.l10n.identification_selfieCamera : context.l10n.identification_standardCamera, + state == CameraFacing.back + ? context.l10n.identification_selfieCamera + : context.l10n.identification_standardCamera, style: const TextStyle(fontSize: 16), ), ), diff --git a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart index cb0502160..6401d7b30 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart @@ -57,7 +57,9 @@ class PositiveVerificationResultDialogState extends State determinePosition( Future checkAndRequestLocationPermission( BuildContext context, { bool requestIfNotGranted = true, - String? rationale, Future Function()? onDisableFeature, Future Function()? onEnableFeature, }) async { @@ -145,13 +144,14 @@ Future checkAndRequestLocationPermission( // returned true. According to Android guidelines // your App should show an explanatory UI now. - final result = await showDialog(context: context, builder: (context) => RationaleDialog(rationale: rationale ?? context.l10n.location_activateLocationAccessRationale)); + final result = await showDialog( + context: context, + builder: (context) => RationaleDialog(rationale: context.l10n.location_activateLocationAccessRationale)); if (result == true) { return checkAndRequestLocationPermission( context, requestIfNotGranted: requestIfNotGranted, - rationale: rationale, ); } diff --git a/frontend/lib/location/dialogs.dart b/frontend/lib/location/dialogs.dart index c402a84ab..bfb10ba68 100644 --- a/frontend/lib/location/dialogs.dart +++ b/frontend/lib/location/dialogs.dart @@ -33,7 +33,8 @@ class RationaleDialog extends StatelessWidget { children: [Text(_rationale), Text(context.l10n.location_askPermissionsAgain)], ), actions: [ - TextButton(child: Text(context.l10n.location_grantPermission), onPressed: () => Navigator.of(context).pop(true)), + TextButton( + child: Text(context.l10n.location_grantPermission), onPressed: () => Navigator.of(context).pop(true)), TextButton(child: Text(context.l10n.common_cancel), onPressed: () => Navigator.of(context).pop(false)) ], ); diff --git a/frontend/lib/map/preview/accepting_store_preview_card.dart b/frontend/lib/map/preview/accepting_store_preview_card.dart index 55ed6bd14..08e7e5396 100644 --- a/frontend/lib/map/preview/accepting_store_preview_card.dart +++ b/frontend/lib/map/preview/accepting_store_preview_card.dart @@ -16,10 +16,9 @@ class AcceptingStorePreviewError extends StatelessWidget { return InkWell( onTap: refetch, child: Container( - height: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ErrorMessage(context.l10n.store_loadingInformationFailed) - ), + height: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ErrorMessage(context.l10n.store_loadingInformationFailed)), ); } } diff --git a/frontend/lib/search/filter_bar.dart b/frontend/lib/search/filter_bar.dart index fe548a2e8..35849d602 100644 --- a/frontend/lib/search/filter_bar.dart +++ b/frontend/lib/search/filter_bar.dart @@ -25,7 +25,8 @@ class FilterBar extends StatelessWidget { padding: const EdgeInsets.all(8), child: Row( children: [ - Text(context.l10n.search_filterByCategories.toUpperCase(), maxLines: 1, style: const TextStyle(color: Colors.grey)), + Text(context.l10n.search_filterByCategories.toUpperCase(), + maxLines: 1, style: const TextStyle(color: Colors.grey)), const Expanded(child: Padding(padding: EdgeInsets.only(left: 8), child: Divider(thickness: 0.7))) ], ), From e59e40109771442002be516919d8869f802f3225 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Tue, 17 Oct 2023 12:11:58 +0200 Subject: [PATCH 33/60] 904: Add en locale --- frontend/lib/app.dart | 2 +- .../qr_code_scanner/qr_code_camera_permission_dialog.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/lib/app.dart b/frontend/lib/app.dart index 33c859d64..913960f83 100644 --- a/frontend/lib/app.dart +++ b/frontend/lib/app.dart @@ -73,7 +73,7 @@ class App extends StatelessWidget { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: const [Locale('de')], + supportedLocales: const [Locale('de'), Locale('en')], initialRoute: initialRoute, routes: routes, ), diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart index 1e0b2c00b..ebb0aa450 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart @@ -13,7 +13,7 @@ class QrCodeCameraPermissionDialog extends StatelessWidget { content: SingleChildScrollView( child: ListBody( children: [ - Text(context.l10n.identification_cameraAccessRequired), + Text(context.l10n.identification_cameraAccessRequiredSettings), ], ), ), From 51d818d43e5be4ccab35ff0c73c992124813a6e3 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Wed, 18 Oct 2023 10:57:14 +0200 Subject: [PATCH 34/60] 904: Fix crash --- frontend/lib/home/app_flow.dart | 4 ++-- frontend/lib/home/home_page.dart | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/lib/home/app_flow.dart b/frontend/lib/home/app_flow.dart index 613dc5597..18a99a942 100644 --- a/frontend/lib/home/app_flow.dart +++ b/frontend/lib/home/app_flow.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class AppFlow { final Widget widget; final IconData iconData; - final String title; + final String Function(BuildContext) getTitle; final GlobalKey navigatorKey; - AppFlow(this.widget, this.iconData, this.title, this.navigatorKey); + AppFlow(this.widget, this.iconData, this.getTitle, this.navigatorKey); } diff --git a/frontend/lib/home/home_page.dart b/frontend/lib/home/home_page.dart index c00b57c39..3038e9909 100644 --- a/frontend/lib/home/home_page.dart +++ b/frontend/lib/home/home_page.dart @@ -39,23 +39,23 @@ class HomePageState extends State { selectAcceptingStore: (id) => setState(() => selectedAcceptingStoreId = id), ), Icons.map_outlined, - context.l10n.map_title, + (BuildContext context) => context.l10n.map_title, GlobalKey(debugLabel: 'Map tab key'), ), AppFlow( const SearchPage(), Icons.search_outlined, - context.l10n.search_title, + (BuildContext context) => context.l10n.search_title, GlobalKey(debugLabel: 'Search tab key'), ), if (buildConfig.featureFlags.verification) AppFlow( IdentificationPage(), Icons.remove_red_eye_outlined, - context.l10n.identification_title, + (BuildContext context) => context.l10n.identification_title, GlobalKey(debugLabel: 'Auth tab key'), ), - AppFlow(const AboutPage(), Icons.info_outline, context.l10n.about_title, + AppFlow(const AboutPage(), Icons.info_outline, (BuildContext context) => context.l10n.about_title, GlobalKey(debugLabel: 'About tab key')), ]; } @@ -98,7 +98,7 @@ class HomePageState extends State { currentIndex: _currentTabIndex, backgroundColor: theme.colorScheme.surfaceVariant, items: appFlows - .map((appFlow) => BottomNavigationBarItem(icon: Icon(appFlow.iconData), label: appFlow.title)) + .map((appFlow) => BottomNavigationBarItem(icon: Icon(appFlow.iconData), label: appFlow.getTitle(context))) .toList(), onTap: _onTabTapped, type: BottomNavigationBarType.fixed, From 3b57f5882f8fd8ba0620d7f7060687aac9390d0c Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Wed, 18 Oct 2023 12:33:16 +0200 Subject: [PATCH 35/60] 1178: Translate constants to english --- .../card_detail_view/card_detail_view.dart | 4 +- .../intro_slides/location_request_button.dart | 2 +- frontend/lib/l10n/app_de.arb | 14 +- frontend/lib/l10n/app_en.arb | 236 +++++++++--------- .../preview/accepting_store_preview_card.dart | 2 +- 5 files changed, 125 insertions(+), 133 deletions(-) diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index fbcac7294..411157221 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -194,7 +194,7 @@ class QrCodeAndStatus extends StatelessWidget { child: TextButton.icon( icon: const Icon(Icons.refresh), onPressed: onSelfVerifyPressed, - label: Text(context.l10n.identification_checkAgain), + label: Text(context.l10n.common_tryAgain), ), ), ], @@ -204,7 +204,7 @@ class QrCodeAndStatus extends StatelessWidget { child: TextButton.icon( icon: const Icon(Icons.refresh), onPressed: onSelfVerifyPressed, - label: Text(context.l10n.identification_checkAgain), + label: Text(context.l10n.common_tryAgain), )) ], CardStatus.invalid => [_PaddedText(context.l10n.identification_cardInvalid)], diff --git a/frontend/lib/intro_slides/location_request_button.dart b/frontend/lib/intro_slides/location_request_button.dart index 314891dab..cb566f250 100644 --- a/frontend/lib/intro_slides/location_request_button.dart +++ b/frontend/lib/intro_slides/location_request_button.dart @@ -72,7 +72,7 @@ class _LocationRequestButtonState extends State { case LocationStatus.deniedForever: return ElevatedButton( onPressed: null, - child: Text(context.l10n.location_locationDeactivated), + child: Text(context.l10n.location_locationAccessDeactivated), ); } } diff --git a/frontend/lib/l10n/app_de.arb b/frontend/lib/l10n/app_de.arb index 18c327456..a0afa2032 100644 --- a/frontend/lib/l10n/app_de.arb +++ b/frontend/lib/l10n/app_de.arb @@ -65,22 +65,20 @@ "identification_cardExpired": "Ihre Karte ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", "identification_cardInvalid": "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", "identification_cardNotYetValid": "Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen.", - "identification_checkAgain": "Erneut prüfen", - "identification_checkFailed": "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Verbindung mit dem Internet besteht und prüfen Sie erneut.", + "identification_checkFailed": "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Internetverbindung besteht und prüfen Sie erneut.", "identification_checkRequired": "Prüfung nötig", "identification_checkingCode": "Der QR-Code wird durch eine Server-Anfrage geprüft.", "identification_codeExpired": "Der eingescannte Code ist bereits am {expirationDate} abgelaufen.", "identification_codeInvalid": "Der Inhalt des eingescannten Codes kann nicht verstanden werden. Vermutlich handelt es sich um einen QR-Code, der nicht für diese App generiert wurde.", "identification_codeInvalidMissing": "Der Inhalt des eingescannten Codes ist unvollständig. (Fehlercode: {missing}Missing)", "identification_codeSavingFailed": "Der eingescannte Code kann nicht in der App gespeichert werden.", - "identification_codeUnknownError": "Beim Einlesen des QR-Codes ist ein unbekannter Fehler aufgetreten.", - "identification_codeVerificationFailed": "Der eingescannte Code konnte vom Server nicht verifiziert werden!", - "identification_codeVerificationFailedConnection": "Der eingescannte Code konnte nicht verifiziert werden, da die Kommunikation mit dem Server fehlschlug. Bitte prüfen Sie Ihre Internetverbindung.", + "identification_codeUnknownError": "Beim Scannen des QR-Codes ist ein unbekannter Fehler aufgetreten.", + "identification_codeVerificationFailed": "Der eingescannte Code konnte vom Server nicht verifiziert werden.", + "identification_codeVerificationFailedConnection": "Der eingescannte Code konnte nicht verifiziert werden. Bitte stellen Sie sicher, dass eine Internetverbindung besteht und prüfen Sie erneut.", "identification_compareWithID": "Gleichen Sie die angezeigten Daten mit einem amtlichen Lichtbildausweis ab.", "identification_comparedWithID": "Ich habe die Daten mit einem amtlichen Lichtbildausweis abgeglichen.", "identification_flashOff": "Blitz aus", "identification_flashOn": "Blitz an", - "identification_generatingTotpSecretFailed": "", "identification_internetRequired": "Eine Internetverbindung wird benötigt.", "identification_notVerified": "Nicht verifiziert", "identification_scanCode": "Scannen Sie den QR-Code, der auf dem \"Ausweisen\"-Tab Ihres Gegenübers angezeigt wird.", @@ -102,7 +100,6 @@ "location_grantLocation": "Ich möchte meinen Standort freigeben.", "location_grantPermission": "Berechtigung erteilen", "location_locationAccessDeactivated": "Die Standortfreigabe ist deaktiviert.", - "location_locationDeactivated": "Standortfreigabe ist deaktiviert.", "location_locationGranted": "Standort ist freigegeben.", "location_locationPermission": "Standortberechtigung", "location_checkSettings": "Prüfe Einstellungen...", @@ -115,7 +112,7 @@ "search_filterByCategories": "Nach Kategorien filtern", "search_findCloseBy": "In meiner Nähe suchen", "search_noAcceptingStoresFound": "Auf diese Suche trifft keine Akzeptanzstelle zu.", - "search_searchHint": "Tippen, um zu suchen …", + "search_searchHint": "Tippen, um zu suchen...", "search_searchResults": "Suchresultate", "search_title": "Suche", @@ -124,7 +121,6 @@ "store_address": "Adresse", "store_email": "E-Mail", "store_loadingDataFailed": "Fehler beim Laden der Daten.", - "store_loadingInformationFailed": "Fehler beim Laden der Infos.", "store_noDescriptionAvailable": "Keine Beschreibung verfügbar", "store_phone": "Telefon", "store_showOnMap": "Auf Karte zeigen", diff --git a/frontend/lib/l10n/app_en.arb b/frontend/lib/l10n/app_en.arb index 18c327456..f0bab2f68 100644 --- a/frontend/lib/l10n/app_en.arb +++ b/frontend/lib/l10n/app_en.arb @@ -1,133 +1,129 @@ { - "about_dependencies": "Software-Bibliotheken", - "about_developmentOptions": "Entwickleroptionen", - "about_disclaimer": "Haftung, Haftungsausschluss und Impressum", - "about_license": "Lizenz", - "about_licenses": "Lizenzen", - "about_moreInformation": "Mehr Informationen", - "about_numberLicenses": "{count, plural, =0{Keine Lizenzen} =1{1 Lizenz} other{{count} Lizenzen}}", - "about_privacyDeclaration": "Datenschutzerklärung", - "about_publisher": "Herausgeber", - "about_sourceCode": "Quellcode der App", - "about_title": "Über", + "about_dependencies": "Libraries", + "about_developmentOptions": "Developer options", + "about_disclaimer": "Liability, disclaimer and imprint", + "about_license": "License", + "about_licenses": "Licenses", + "about_moreInformation": "More information", + "about_numberLicenses": "{count, plural, =0{No licenses} =1{1 License} other{{count} Licenses}}", + "about_privacyDeclaration": "Privacy policy", + "about_publisher": "Publisher", + "about_sourceCode": "Source code", + "about_title": "About", - "category_clothing": "Kleidung", - "category_clothingLong": "Kleidung/Gebrauchtes", - "category_culture": "Kultur", - "category_cultureLong": "Bildung/Kultur/Unterhaltung", - "category_cultureLongAlternative": "Kultur/Museen/Freizeit", - "category_digitalParticipation": "Digitale Teilhabe", - "category_education": "Bildung", - "category_fashion": "Mode", - "category_fashionLong": "Mode/Beauty", - "category_food": "Gastronomie", - "category_foodLong": "Essen/Trinken/Gastronomie", - "category_health": "Gesundheit", - "category_healthLong": "Gesundheit/Sport/Wellness", - "category_leisure": "Freizeit", - "category_leisureLong": "Freizeit/Reise/Unterkünfte", - "category_living": "Einrichtung", - "category_livingLong": "Wohnen/Haus/Garten", - "category_lunchTables": "Mittagstische", + "category_clothing": "Clothing", + "category_clothingLong": "Clothing/Second hand", + "category_culture": "Culture", + "category_cultureLong": "Education/Culture/Entertainment", + "category_cultureLongAlternative": "Culture/Museums/Leisure", + "category_digitalParticipation": "Digital participation", + "category_education": "Education", + "category_fashion": "Fashion", + "category_fashionLong": "Fashion/Beauty", + "category_food": "Restaurants", + "category_foodLong": "Food/Drink/Restaurants", + "category_health": "Health", + "category_healthLong": "Health/Sports/Wellness", + "category_leisure": "Leisure", + "category_leisureLong": "Leisure/Travel/Accommodation", + "category_living": "Furnishing", + "category_livingLong": "Living/Home/Garden", + "category_lunchTables": "Lunch tables", "category_media": "Multimedia", - "category_mobility": "Mobilität", - "category_mobilityLong": "Auto/Zweirad", - "category_movies": "Schauspiel", - "category_moviesLong": "Kinos/Theater/Konzerte", - "category_other": "Anderes", - "category_pharmacies": "Apotheken", - "category_pharmaciesLong": "Apotheken/Gesundheit", - "category_services": "Dienstleistung", - "category_servicesLong": "Dienstleistungen/Finanzen", - "category_sports": "Sport", - "category_sportsLong": "Sport/Bewegung/Tanz", + "category_mobility": "Mobility", + "category_mobilityLong": "Car/Bicycle", + "category_movies": "Plays", + "category_moviesLong": "Cinema/Theater/Concerts", + "category_other": "Other", + "category_pharmacies": "Pharmacies", + "category_pharmaciesLong": "Pharmacies/Health", + "category_services": "Services", + "category_servicesLong": "Services/Finances", + "category_sports": "Sports", + "category_sportsLong": "Sports/Movement/Dance", - "common_cancel": "Abbrechen", - "common_checkConnection": "Bitte Internetverbindung prüfen.", - "common_connectionFailed": "Keine Verbindung möglich", - "common_done": "Fertig", - "common_moreActions": "Weitere Aktionen", - "common_next": "Weiter", + "common_cancel": "Cancel", + "common_checkConnection": "Please check internet connection.", + "common_connectionFailed": "No connection possible", + "common_done": "Done", + "common_moreActions": "More actions", + "common_next": "Next", "common_ok": "OK", - "common_openSettings": "Einstellungen öffnen", - "common_previous": "Zurück", - "common_settings": "Einstellungen", - "common_tryAgain": "Erneut versuchen", + "common_openSettings": "Open settings", + "common_previous": "Back", + "common_settings": "Settings", + "common_tryAgain": "Try again", - "identification_activate": "Aktivieren", - "identification_activateCardCurrentDevice": "Karte auf diesem Gerät aktivieren?", - "identification_activateCardCurrentDeviceRationale": "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", - "identification_activationInvalidState": "Die Aktivierung befindet sich in einem ungültigen Zustand.", - "identification_authenticationPossible": "Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:", - "identification_cameraAccessRequired": "Zugriff auf Kamera erforderlich", - "identification_cameraAccessRequiredSettings": "Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera.\nIn den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.", - "identification_cardAlreadyActivated": "Der eingescannte QRCode wurde bereits aktiviert.", - "identification_cardExpired": "Ihre Karte ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", - "identification_cardInvalid": "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", - "identification_cardNotYetValid": "Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen.", - "identification_checkAgain": "Erneut prüfen", - "identification_checkFailed": "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Verbindung mit dem Internet besteht und prüfen Sie erneut.", - "identification_checkRequired": "Prüfung nötig", - "identification_checkingCode": "Der QR-Code wird durch eine Server-Anfrage geprüft.", - "identification_codeExpired": "Der eingescannte Code ist bereits am {expirationDate} abgelaufen.", - "identification_codeInvalid": "Der Inhalt des eingescannten Codes kann nicht verstanden werden. Vermutlich handelt es sich um einen QR-Code, der nicht für diese App generiert wurde.", - "identification_codeInvalidMissing": "Der Inhalt des eingescannten Codes ist unvollständig. (Fehlercode: {missing}Missing)", - "identification_codeSavingFailed": "Der eingescannte Code kann nicht in der App gespeichert werden.", - "identification_codeUnknownError": "Beim Einlesen des QR-Codes ist ein unbekannter Fehler aufgetreten.", - "identification_codeVerificationFailed": "Der eingescannte Code konnte vom Server nicht verifiziert werden!", - "identification_codeVerificationFailedConnection": "Der eingescannte Code konnte nicht verifiziert werden, da die Kommunikation mit dem Server fehlschlug. Bitte prüfen Sie Ihre Internetverbindung.", - "identification_compareWithID": "Gleichen Sie die angezeigten Daten mit einem amtlichen Lichtbildausweis ab.", - "identification_comparedWithID": "Ich habe die Daten mit einem amtlichen Lichtbildausweis abgeglichen.", - "identification_flashOff": "Blitz aus", - "identification_flashOn": "Blitz an", - "identification_generatingTotpSecretFailed": "", - "identification_internetRequired": "Eine Internetverbindung wird benötigt.", - "identification_notVerified": "Nicht verifiziert", - "identification_scanCode": "Scannen Sie den QR-Code, der auf dem \"Ausweisen\"-Tab Ihres Gegenübers angezeigt wird.", - "identification_scanQRCode": "Halten Sie die Kamera auf den QR Code.", - "identification_scanningFailed": "Fehler beim Lesen des Codes", - "identification_selfieCamera": "Frontkamera", - "identification_standardCamera": "Standard-Kamera", - "identification_stopShowing": "Nicht mehr anzeigen", - "identification_timeIncorrect": "Die Uhrzeit Ihres Geräts scheint nicht zu stimmen. Bitte synchronisieren Sie die Uhrzeit in den Systemeinstellungen.", - "identification_title": "Ausweisen", - "identification_unlimited": "unbegrenzt", - "identification_validFromUntil": "Gültig: {startDate} bis {expirationDate}", - "identification_validUntil": "Gültig bis: {expirationDate}", + "identification_activate": "Activate", + "identification_activateCardCurrentDevice": "Activate card on this device?", + "identification_activateCardCurrentDeviceRationale": "Your card is already activated on another device. If you activate your card on this device, it will be automatically deactivated on your other device.", + "identification_activationInvalidState": "The activation is in an invalid state.", + "identification_authenticationPossible": "You can use this QR code to identify yourself at acceptance points:", + "identification_cameraAccessRequired": "Access to camera required", + "identification_cameraAccessRequiredSettings": "To scan a QR code, the app needs access to the camera.\nYou can allow the app to access the camera in the settings.", + "identification_cardAlreadyActivated": "The scanned QR code has already been activated.", + "identification_cardExpired": "Your card has expired.\nUnder \"More actions\" you can apply for renewal.", + "identification_cardInvalid": "Your card is invalid.\nIt has either been revoked or activated on another device.", + "identification_cardNotYetValid": "The validity period of your card has not started yet.", + "identification_checkFailed": "Your card could not be verified. Please make sure you have an internet connection and try again.", + "identification_checkRequired": "Verification necessary", + "identification_checkingCode": "The QR code is verified by a server request.", + "identification_codeExpired": "The scanned code has already expired on {expirationDate}.", + "identification_codeInvalid": "The content of the scanned code cannot be understood. It is probably a QR code that was not generated for this app.", + "identification_codeInvalidMissing": "The content of the scanned code is incomplete. (Error code: {missing}Missing)", + "identification_codeSavingFailed": "The scanned code cannot be saved in the app.", + "identification_codeUnknownError": "An unknown error occurred while scanning the QR code.", + "identification_codeVerificationFailed": "The scanned code could not be verified by the server.", + "identification_codeVerificationFailedConnection": "The scanned code could not be verified. Please make sure you have an internet connection and try again.", + "identification_compareWithID": "Verify the displayed data against an official photo ID.", + "identification_comparedWithID": "I verified the data against an official photo ID.", + "identification_flashOff": "Flash off", + "identification_flashOn": "Flash on", + "identification_internetRequired": "An Internet connection is required.", + "identification_notVerified": "Not verified", + "identification_scanCode": "Scan the QR code that appears on the \"Identify\" tab of your counterpart.", + "identification_scanQRCode": "Hold the camera on the QR code.", + "identification_scanningFailed": "Error reading the code", + "identification_selfieCamera": "Front camera", + "identification_standardCamera": "Standard camera", + "identification_stopShowing": "Stop showing", + "identification_timeIncorrect": "The time of your device does not seem to be correct. Please synchronize the time in the system settings.", + "identification_title": "Identify", + "identification_unlimited": "unlimited", + "identification_validFromUntil": "Valid: {startDate} until {expirationDate}", + "identification_validUntil": "Valid until: {expirationDate}", - "location_activateLocationAccess": "Standortermittlung aktivieren", - "location_activateLocationAccessRationale": "Erlauben Sie der App Ihren Standort zu benutzen, um Akzeptanzstellen in Ihrer Umgebung anzuzeigen.", - "location_activateLocationAccessSettings": "Aktivieren Sie die Standortermittlung in den Einstellungen.", - "location_askPermissionsAgain": "Soll nocheinmal nach der Berechtigung gefragt werden?", - "location_grantLocation": "Ich möchte meinen Standort freigeben.", - "location_grantPermission": "Berechtigung erteilen", - "location_locationAccessDeactivated": "Die Standortfreigabe ist deaktiviert.", - "location_locationDeactivated": "Standortfreigabe ist deaktiviert.", - "location_locationGranted": "Standort ist freigegeben.", - "location_locationPermission": "Standortberechtigung", - "location_checkSettings": "Prüfe Einstellungen...", + "location_activateLocationAccess": "Activate location access", + "location_activateLocationAccessRationale": "Allow the app to use your location to display acceptance points in your area.", + "location_activateLocationAccessSettings": "Activate the location detection in the settings.", + "location_askPermissionsAgain": "Should permission be asked again?", + "location_grantLocation": "I would like to share my location.", + "location_grantPermission": "Grant permission", + "location_locationAccessDeactivated": "Location is disabled.", + "location_locationGranted": "Location is enabled.", + "location_locationPermission": "Location permission", + "location_checkSettings": "Checking settings...", - "map_mapData": "Kartendaten", - "map_osmContributors": "OpenStreetMap Mitwirkende", - "map_showMapCopyright": "Zeige Infos über das Urheberrecht der Kartendaten", - "map_title": "Karte", + "map_mapData": "Map data", + "map_osmContributors": "OpenStreetMap Contributors", + "map_showMapCopyright": "Show info about map data copyright", + "map_title": "Map", - "search_filterByCategories": "Nach Kategorien filtern", - "search_findCloseBy": "In meiner Nähe suchen", - "search_noAcceptingStoresFound": "Auf diese Suche trifft keine Akzeptanzstelle zu.", - "search_searchHint": "Tippen, um zu suchen …", - "search_searchResults": "Suchresultate", - "search_title": "Suche", + "search_filterByCategories": "Filter by categories", + "search_findCloseBy": "Search near me", + "search_noAcceptingStoresFound": "No accepting points found matching this search.", + "search_searchHint": "Tap to search...", + "search_searchResults": "Search results", + "search_title": "Search", - "store_acceptingStore": "Akzeptanzstelle", - "store_acceptingStoreNotFound": "Akzeptanzstelle nicht gefunden.", - "store_address": "Adresse", + "store_acceptingStore": "Accepting points", + "store_acceptingStoreNotFound": "Accepting point not found.", + "store_address": "Address", "store_email": "E-Mail", - "store_loadingDataFailed": "Fehler beim Laden der Daten.", - "store_loadingInformationFailed": "Fehler beim Laden der Infos.", - "store_noDescriptionAvailable": "Keine Beschreibung verfügbar", - "store_phone": "Telefon", - "store_showOnMap": "Auf Karte zeigen", - "store_unknownCategory": "Unbekannte Kategorie", + "store_loadingDataFailed": "Error loading the data.", + "store_noDescriptionAvailable": "No description available", + "store_phone": "Phone", + "store_showOnMap": "Show on map", + "store_unknownCategory": "Unknown category", "store_website": "Website" } diff --git a/frontend/lib/map/preview/accepting_store_preview_card.dart b/frontend/lib/map/preview/accepting_store_preview_card.dart index 08e7e5396..53a1c88e6 100644 --- a/frontend/lib/map/preview/accepting_store_preview_card.dart +++ b/frontend/lib/map/preview/accepting_store_preview_card.dart @@ -18,7 +18,7 @@ class AcceptingStorePreviewError extends StatelessWidget { child: Container( height: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16), - child: ErrorMessage(context.l10n.store_loadingInformationFailed)), + child: ErrorMessage(context.l10n.store_loadingDataFailed)), ); } } From 97bf0b730f0807b7a8717ba02efb8efee9df7f12 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 19 Oct 2023 12:01:45 +0200 Subject: [PATCH 36/60] 1084: add console log for base64 activation secret --- administration/src/cards/pdf/PdfQrCodeElement.ts | 7 +++++++ administration/src/util/helper.ts | 1 + 2 files changed, 8 insertions(+) create mode 100644 administration/src/util/helper.ts diff --git a/administration/src/cards/pdf/PdfQrCodeElement.ts b/administration/src/cards/pdf/PdfQrCodeElement.ts index 55cfa1e41..6eb78405a 100644 --- a/administration/src/cards/pdf/PdfQrCodeElement.ts +++ b/administration/src/cards/pdf/PdfQrCodeElement.ts @@ -1,6 +1,8 @@ import { PDFPage } from 'pdf-lib' import { QrCode } from '../../generated/card_pb' +import { uint8ArrayToBase64 } from '../../util/base64' +import { isDevMode } from '../../util/helper' import { drawQRCode } from '../../util/qrcode' import { Coordinates, PdfElement, mmToPt } from './PdfElements' @@ -27,6 +29,11 @@ const pdfQrCodeElement: PdfElement window.location.hostname === 'localhost' From b911d3a0e6cb36b4beb3cc6c159757497f111edb Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 19 Oct 2023 15:57:46 +0200 Subject: [PATCH 37/60] 1084: resolved minor issues --- frontend/build-configs/bayern/localization.ts | 2 +- frontend/build-configs/nuernberg/localization.ts | 4 ++-- .../activation_workflow/activation_code_scanner_page.dart | 4 +--- frontend/lib/identification/identification_page.dart | 6 +++++- .../dialogs/remove_card_confirmation_dialog.dart | 1 + 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/build-configs/bayern/localization.ts b/frontend/build-configs/bayern/localization.ts index c25c6c03f..888a70ed1 100644 --- a/frontend/build-configs/bayern/localization.ts +++ b/frontend/build-configs/bayern/localization.ts @@ -34,7 +34,7 @@ const localization: LocalizationType = { }, removeCardDialog: { title: "Diese Karte löschen?", - description: "Wenn diese Karte gelöscht wird, muss sie vor der Wiederverwendung neu aktiviert werden.", + description: "Wenn diese Karte gelöscht wird, muss diese für eine erneute Verwendung neu hinzugefügt werden.", } }, } diff --git a/frontend/build-configs/nuernberg/localization.ts b/frontend/build-configs/nuernberg/localization.ts index 76adc28f6..3d5573b64 100644 --- a/frontend/build-configs/nuernberg/localization.ts +++ b/frontend/build-configs/nuernberg/localization.ts @@ -33,8 +33,8 @@ const localization: LocalizationType = { removeCardDescription: "Nach der Auswahl wird der hinterlegte Pass vom Gerät gelöscht.", }, removeCardDialog: { - title: "Diese Karte löschen?", - description: "Wenn diese Karte gelöscht wird, muss sie vor der Wiederverwendung neu aktiviert werden.", + title: "Diesen Pass löschen?", + description: "Wenn dieser Pass gelöscht wird, muss dieser für eine erneute Verwendung neu hinzugefügt werden.", } }, } diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 90df43b6e..099b9b408 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -119,9 +119,7 @@ class ActivationCodeScannerPage extends StatelessWidget { ..verificationTimeStamp = secondsSinceEpoch(DateTime.parse(activationResult.activationTimeStamp))); userCodesModel.insertCode(userCode); - if (userCodesModel.userCodes.length > 1) { - moveToLastCard(); - } + moveToLastCard(); debugPrint('Card Activation: Successfully activated.'); break; diff --git a/frontend/lib/identification/identification_page.dart b/frontend/lib/identification/identification_page.dart index 24b55a3b1..6f5e2d4ec 100644 --- a/frontend/lib/identification/identification_page.dart +++ b/frontend/lib/identification/identification_page.dart @@ -109,6 +109,10 @@ class IdentificationPageState extends State { void _moveCarouselToLastPosition() { final userCodeModel = Provider.of(context, listen: false); - carouselController.jumpToPage(userCodeModel.userCodes.length); + final int cardAmount = userCodeModel.userCodes.length; + // the carousel controller causes an error if you try to move if there is only one item + if (cardAmount > 1) { + carouselController.jumpToPage(cardAmount); + } } } diff --git a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart index 4a23af32d..0a1aec480 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart @@ -81,6 +81,7 @@ class RemoveCardConfirmationDialogState extends State(context, listen: false); + // ensures that the store will be reset to empty list if (provider.userCodes.length == 1) { provider.removeCodes(); } else { From 8933b81f4788d6a9228bd6fbe3252b1b92c30bc7 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 23 Oct 2023 13:16:01 +0200 Subject: [PATCH 38/60] 904: Fix imports --- .../activation_workflow/activation_code_scanner_page.dart | 2 +- .../activation_overwrite_existing_dialog.dart | 2 +- .../lib/identification/card_detail_view/card_detail_view.dart | 2 +- .../identification/card_detail_view/more_actions_dialog.dart | 2 +- frontend/lib/identification/id_card/card_content.dart | 2 +- .../qr_code_scanner/qr_code_camera_permission_dialog.dart | 2 +- .../lib/identification/qr_code_scanner/qr_code_scanner.dart | 2 +- .../qr_code_scanner/qr_code_scanner_controls.dart | 2 +- .../identification/qr_code_scanner/qr_parsing_error_dialog.dart | 2 +- .../dialogs/negative_verification_result_dialog.dart | 2 +- .../dialogs/positive_verification_result_dialog.dart | 2 +- .../verification_workflow/dialogs/verification_info_dialog.dart | 2 +- .../verification_workflow/verification_qr_scanner_page.dart | 2 +- frontend/lib/map/map/attribution_dialog.dart | 2 +- frontend/lib/map/map/map.dart | 2 +- frontend/lib/map/preview/accepting_store_preview_card.dart | 2 +- frontend/lib/store_widgets/detail/detail_app_bar.dart | 2 +- frontend/lib/store_widgets/detail/detail_content.dart | 2 +- frontend/lib/store_widgets/detail/detail_page.dart | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index c816d0b4a..8084bbb51 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -24,7 +24,7 @@ import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class ActivationCodeScannerPage extends StatelessWidget { const ActivationCodeScannerPage({super.key}); diff --git a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart index 6ea418e0c..2232d3df7 100644 --- a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart +++ b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class ActivationOverwriteExistingDialog extends StatelessWidget { const ActivationOverwriteExistingDialog({super.key}); diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index fbcac7294..4a6900207 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; import '../user_code_model.dart'; import 'verification_code_view.dart'; diff --git a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart index 67e399008..9032f1dce 100644 --- a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart +++ b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart @@ -1,7 +1,7 @@ import 'package:ehrenamtskarte/build_config/build_config.dart'; import 'package:flutter/material.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class MoreActionsDialog extends StatelessWidget { final VoidCallback startActivation; diff --git a/frontend/lib/identification/id_card/card_content.dart b/frontend/lib/identification/id_card/card_content.dart index b7d301a83..bd295ace3 100644 --- a/frontend/lib/identification/id_card/card_content.dart +++ b/frontend/lib/identification/id_card/card_content.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/util/color_utils.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; Color standardCardColor = getColorFromHex(buildConfig.cardBranding.colorStandard); Color premiumCardColor = getColorFromHex(buildConfig.cardBranding.colorPremium); diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart index ebb0aa450..3fd5f2cde 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class QrCodeCameraPermissionDialog extends StatelessWidget { const QrCodeCameraPermissionDialog(); diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart index 684ca3c36..661bc320a 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_overlay_shape.d import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; typedef OnCodeScannedCallback = Future Function(Uint8List code); diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart index 1599584ba..6d2c00fb3 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class QrCodeScannerControls extends StatelessWidget { final MobileScannerController controller; diff --git a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart index 7f28dcc74..89bbdba0f 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class QrParsingErrorDialog extends StatelessWidget { final String message; diff --git a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart index a683db84a..6e71db0b4 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart @@ -1,7 +1,7 @@ import 'package:ehrenamtskarte/identification/info_dialog.dart'; import 'package:flutter/material.dart'; -import '../../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class NegativeVerificationResultDialog extends StatelessWidget { final String reason; diff --git a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart index 6401d7b30..77f48e2c3 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart @@ -7,7 +7,7 @@ import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; -import '../../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class PositiveVerificationResultDialog extends StatefulWidget { final CardInfo cardInfo; diff --git a/frontend/lib/identification/verification_workflow/dialogs/verification_info_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/verification_info_dialog.dart index b831fd14f..d8e86803e 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/verification_info_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/verification_info_dialog.dart @@ -3,7 +3,7 @@ import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class VerificationInfoDialog extends StatelessWidget { const VerificationInfoDialog({super.key}); diff --git a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart index 9d8cd5334..03a9e94ff 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart @@ -20,7 +20,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class VerificationQrScannerPage extends StatelessWidget { const VerificationQrScannerPage({super.key}); diff --git a/frontend/lib/map/map/attribution_dialog.dart b/frontend/lib/map/map/attribution_dialog.dart index 4f056650a..66551c4b3 100644 --- a/frontend/lib/map/map/attribution_dialog.dart +++ b/frontend/lib/map/map/attribution_dialog.dart @@ -2,7 +2,7 @@ import 'package:ehrenamtskarte/map/map/attribution_dialog_item.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class AttributionDialog extends StatelessWidget { const AttributionDialog({super.key}); diff --git a/frontend/lib/map/map/map.dart b/frontend/lib/map/map/map.dart index 5db2af7e7..a25960ac1 100644 --- a/frontend/lib/map/map/map.dart +++ b/frontend/lib/map/map/map.dart @@ -12,7 +12,7 @@ import 'package:geolocator/geolocator.dart'; import 'package:maplibre_gl/mapbox_gl.dart'; import 'package:tuple/tuple.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; typedef OnFeatureClickCallback = void Function(dynamic feature); typedef OnNoFeatureClickCallback = void Function(); diff --git a/frontend/lib/map/preview/accepting_store_preview_card.dart b/frontend/lib/map/preview/accepting_store_preview_card.dart index 08e7e5396..5c62e54e4 100644 --- a/frontend/lib/map/preview/accepting_store_preview_card.dart +++ b/frontend/lib/map/preview/accepting_store_preview_card.dart @@ -4,7 +4,7 @@ import 'package:ehrenamtskarte/store_widgets/accepting_store_summary.dart'; import 'package:ehrenamtskarte/widgets/error_message.dart'; import 'package:flutter/material.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class AcceptingStorePreviewError extends StatelessWidget { final void Function()? refetch; diff --git a/frontend/lib/store_widgets/detail/detail_app_bar.dart b/frontend/lib/store_widgets/detail/detail_app_bar.dart index 4652deb64..c7b3ecbe8 100644 --- a/frontend/lib/store_widgets/detail/detail_app_bar.dart +++ b/frontend/lib/store_widgets/detail/detail_app_bar.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/widgets/app_bars.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; const double bottomSize = 100; diff --git a/frontend/lib/store_widgets/detail/detail_content.dart b/frontend/lib/store_widgets/detail/detail_content.dart index 59d3d3e73..7158fa8d8 100644 --- a/frontend/lib/store_widgets/detail/detail_content.dart +++ b/frontend/lib/store_widgets/detail/detail_content.dart @@ -11,7 +11,7 @@ import 'package:maplibre_gl/mapbox_gl.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class DetailContent extends StatelessWidget { final AcceptingStoreById$Query$PhysicalStore acceptingStore; diff --git a/frontend/lib/store_widgets/detail/detail_page.dart b/frontend/lib/store_widgets/detail/detail_page.dart index e133ee2b5..07c502faf 100644 --- a/frontend/lib/store_widgets/detail/detail_page.dart +++ b/frontend/lib/store_widgets/detail/detail_page.dart @@ -9,7 +9,7 @@ import 'package:ehrenamtskarte/widgets/top_loading_spinner.dart'; import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; -import '../../util/l10n.dart'; +import 'package:ehrenamtskarte/util/l10n.dart'; class DetailPage extends StatelessWidget { final int _acceptingStoreId; From 9003bec41af1b8d57519433906d05daca4a0657d Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 23 Oct 2023 15:23:34 +0200 Subject: [PATCH 39/60] 904: Fixes --- frontend/lib/category_assets.dart | 2 +- .../activation_workflow/activation_code_scanner_page.dart | 6 ++++-- frontend/lib/l10n/app_de.arb | 1 + frontend/lib/l10n/app_en.arb | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/lib/category_assets.dart b/frontend/lib/category_assets.dart index 2d402ac44..5ec34d0a3 100644 --- a/frontend/lib/category_assets.dart +++ b/frontend/lib/category_assets.dart @@ -169,7 +169,7 @@ List categoryAssets(BuildContext context) { ), CategoryAsset( id: 17, - name: context.l10n.category_sports, + name: context.l10n.category_sportsLong, shortName: context.l10n.category_sports, icon: 'assets/category_icons/17.svg', detailIcon: 'assets/detail_headers/17_sport.svg', diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index fdb5595f2..83d95dcfb 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -64,7 +64,7 @@ class ActivationCodeScannerPage extends StatelessWidget { final expirationDate = DateFormat('dd.MM.yyyy').format(e.expiry); await showError(context.l10n.identification_codeExpired(expirationDate), null); } on ServerCardActivationException catch (_) { - await ConnectionFailedDialog.show(context, context.l10n.identification_codeVerificationFailedConnection); + await ConnectionFailedDialog.show(context, context.l10n.identification_codeActivationFailedConnection); } on Exception catch (e, stacktrace) { debugPrintStack(stackTrace: stacktrace, label: e.toString()); await showError(context.l10n.identification_codeUnknownError, null); @@ -134,7 +134,9 @@ class ActivationCodeScannerPage extends StatelessWidget { } break; default: - throw ServerCardActivationException(context.l10n.identification_activationInvalidState); + String errorMessage = context.l10n.identification_activationInvalidState; + reportError(errorMessage, null); + throw ServerCardActivationException(errorMessage); } } } diff --git a/frontend/lib/l10n/app_de.arb b/frontend/lib/l10n/app_de.arb index 18c327456..e9e0951b6 100644 --- a/frontend/lib/l10n/app_de.arb +++ b/frontend/lib/l10n/app_de.arb @@ -75,6 +75,7 @@ "identification_codeSavingFailed": "Der eingescannte Code kann nicht in der App gespeichert werden.", "identification_codeUnknownError": "Beim Einlesen des QR-Codes ist ein unbekannter Fehler aufgetreten.", "identification_codeVerificationFailed": "Der eingescannte Code konnte vom Server nicht verifiziert werden!", + "identification_codeActivationFailedConnection": "Der eingescannte Code konnte nicht aktiviert werden, da die Kommunikation mit dem Server fehlschlug. Bitte prüfen Sie Ihre Internetverbindung.", "identification_codeVerificationFailedConnection": "Der eingescannte Code konnte nicht verifiziert werden, da die Kommunikation mit dem Server fehlschlug. Bitte prüfen Sie Ihre Internetverbindung.", "identification_compareWithID": "Gleichen Sie die angezeigten Daten mit einem amtlichen Lichtbildausweis ab.", "identification_comparedWithID": "Ich habe die Daten mit einem amtlichen Lichtbildausweis abgeglichen.", diff --git a/frontend/lib/l10n/app_en.arb b/frontend/lib/l10n/app_en.arb index 18c327456..e9e0951b6 100644 --- a/frontend/lib/l10n/app_en.arb +++ b/frontend/lib/l10n/app_en.arb @@ -75,6 +75,7 @@ "identification_codeSavingFailed": "Der eingescannte Code kann nicht in der App gespeichert werden.", "identification_codeUnknownError": "Beim Einlesen des QR-Codes ist ein unbekannter Fehler aufgetreten.", "identification_codeVerificationFailed": "Der eingescannte Code konnte vom Server nicht verifiziert werden!", + "identification_codeActivationFailedConnection": "Der eingescannte Code konnte nicht aktiviert werden, da die Kommunikation mit dem Server fehlschlug. Bitte prüfen Sie Ihre Internetverbindung.", "identification_codeVerificationFailedConnection": "Der eingescannte Code konnte nicht verifiziert werden, da die Kommunikation mit dem Server fehlschlug. Bitte prüfen Sie Ihre Internetverbindung.", "identification_compareWithID": "Gleichen Sie die angezeigten Daten mit einem amtlichen Lichtbildausweis ab.", "identification_comparedWithID": "Ich habe die Daten mit einem amtlichen Lichtbildausweis abgeglichen.", From 1eda8c8152ecd97409484f4b934834fcba298205 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 23 Oct 2023 15:24:25 +0200 Subject: [PATCH 40/60] 904: Use german as default --- frontend/l10n.yaml | 2 +- frontend/lib/app.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/l10n.yaml b/frontend/l10n.yaml index 15338f2dd..86aa4f42a 100644 --- a/frontend/l10n.yaml +++ b/frontend/l10n.yaml @@ -1,3 +1,3 @@ arb-dir: lib/l10n -template-arb-file: app_en.arb +template-arb-file: app_de.arb output-localization-file: app_localizations.dart diff --git a/frontend/lib/app.dart b/frontend/lib/app.dart index 913960f83..33c859d64 100644 --- a/frontend/lib/app.dart +++ b/frontend/lib/app.dart @@ -73,7 +73,7 @@ class App extends StatelessWidget { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: const [Locale('de'), Locale('en')], + supportedLocales: const [Locale('de')], initialRoute: initialRoute, routes: routes, ), From 7bcc56e111b003412b3a0b087a483cfa74baaef0 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 23 Oct 2023 15:42:04 +0200 Subject: [PATCH 41/60] 904: Rename --- frontend/lib/category_assets.dart | 2 +- frontend/lib/l10n/app_de.arb | 2 +- frontend/lib/l10n/app_en.arb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/lib/category_assets.dart b/frontend/lib/category_assets.dart index 5ec34d0a3..36715e734 100644 --- a/frontend/lib/category_assets.dart +++ b/frontend/lib/category_assets.dart @@ -129,7 +129,7 @@ List categoryAssets(BuildContext context) { ), CategoryAsset( id: 12, - name: context.l10n.category_cultureLongAlternative, + name: context.l10n.category_cultureLongNuernberg, shortName: context.l10n.category_culture, icon: 'assets/category_icons/12.svg', detailIcon: 'assets/detail_headers/12_kultur.svg', diff --git a/frontend/lib/l10n/app_de.arb b/frontend/lib/l10n/app_de.arb index e9e0951b6..cf3da49da 100644 --- a/frontend/lib/l10n/app_de.arb +++ b/frontend/lib/l10n/app_de.arb @@ -15,7 +15,7 @@ "category_clothingLong": "Kleidung/Gebrauchtes", "category_culture": "Kultur", "category_cultureLong": "Bildung/Kultur/Unterhaltung", - "category_cultureLongAlternative": "Kultur/Museen/Freizeit", + "category_cultureLongNuernberg": "Kultur/Museen/Freizeit", "category_digitalParticipation": "Digitale Teilhabe", "category_education": "Bildung", "category_fashion": "Mode", diff --git a/frontend/lib/l10n/app_en.arb b/frontend/lib/l10n/app_en.arb index e9e0951b6..cf3da49da 100644 --- a/frontend/lib/l10n/app_en.arb +++ b/frontend/lib/l10n/app_en.arb @@ -15,7 +15,7 @@ "category_clothingLong": "Kleidung/Gebrauchtes", "category_culture": "Kultur", "category_cultureLong": "Bildung/Kultur/Unterhaltung", - "category_cultureLongAlternative": "Kultur/Museen/Freizeit", + "category_cultureLongNuernberg": "Kultur/Museen/Freizeit", "category_digitalParticipation": "Digitale Teilhabe", "category_education": "Bildung", "category_fashion": "Mode", From 09e7a40cb9902d2a94c9eddfed4d9a21f1bd9c0f Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Wed, 18 Oct 2023 13:10:19 +0200 Subject: [PATCH 42/60] 1176: Configure locales in build config --- frontend/build-configs/bayern/index.ts | 1 + frontend/build-configs/nuernberg/index.ts | 1 + frontend/build-configs/types.ts | 1 + frontend/lib/app.dart | 2 +- frontend/pubs/df_build_config/lib/builder.dart | 2 ++ 5 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/build-configs/bayern/index.ts b/frontend/build-configs/bayern/index.ts index 3765b0069..2811db5b4 100644 --- a/frontend/build-configs/bayern/index.ts +++ b/frontend/build-configs/bayern/index.ts @@ -31,6 +31,7 @@ export const bayernCommon: CommonBuildConfigType = { showcase: "https://api.entitlementcard.app", local: "http://localhost:8000", }, + appLocales: ['de'], cardBranding: { headerTextColor: "#008dc9", headerColor: "#F5F5FFF5", diff --git a/frontend/build-configs/nuernberg/index.ts b/frontend/build-configs/nuernberg/index.ts index ac2eb9db1..13c4e89bf 100644 --- a/frontend/build-configs/nuernberg/index.ts +++ b/frontend/build-configs/nuernberg/index.ts @@ -31,6 +31,7 @@ export const nuernbergCommon: CommonBuildConfigType = { showcase: "https://api.entitlementcard.app", local: "http://localhost:8000", }, + appLocales: ['de', 'en'], cardBranding: { headerTextColor: "#000000", headerTextFontSize: 9, diff --git a/frontend/build-configs/types.ts b/frontend/build-configs/types.ts index 40e526fcd..4ee273919 100644 --- a/frontend/build-configs/types.ts +++ b/frontend/build-configs/types.ts @@ -78,6 +78,7 @@ export type CommonBuildConfigType = { production: string local: string } + appLocales: string[] cardBranding: { headerTextColor: string headerColor: string diff --git a/frontend/lib/app.dart b/frontend/lib/app.dart index 33c859d64..e6201fe42 100644 --- a/frontend/lib/app.dart +++ b/frontend/lib/app.dart @@ -73,7 +73,7 @@ class App extends StatelessWidget { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: const [Locale('de')], + supportedLocales: buildConfig.appLocales.map((locale) => Locale(locale)), initialRoute: initialRoute, routes: routes, ), diff --git a/frontend/pubs/df_build_config/lib/builder.dart b/frontend/pubs/df_build_config/lib/builder.dart index b7bbb632b..b24dbe3c4 100644 --- a/frontend/pubs/df_build_config/lib/builder.dart +++ b/frontend/pubs/df_build_config/lib/builder.dart @@ -54,6 +54,8 @@ void pairToField(String k, dynamic v, StringBuffer root, StringBuffer output) { if (element is int) { output.write(' List get $k => $v;\n'); + } else if (element is String) { + output.write(' List get $k => [\'${v.join('\', \'')}\'];\n'); } else { throw "invalid list ${v.runtimeType}"; } From 93ec9a8e6d00252c8ceb95e5551746bc3a8e160c Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 23 Oct 2023 16:19:06 +0200 Subject: [PATCH 43/60] 1178: Fix english translations --- .../activation_code_scanner_page.dart | 2 +- frontend/lib/l10n/app_de.arb | 3 +-- frontend/lib/l10n/app_en.arb | 25 +++++++++---------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 83d95dcfb..f88ff9fea 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -134,7 +134,7 @@ class ActivationCodeScannerPage extends StatelessWidget { } break; default: - String errorMessage = context.l10n.identification_activationInvalidState; + const errorMessage = 'Die Aktivierung befindet sich in einem ungültigen Zustand.'; reportError(errorMessage, null); throw ServerCardActivationException(errorMessage); } diff --git a/frontend/lib/l10n/app_de.arb b/frontend/lib/l10n/app_de.arb index 9ea4f9d19..45ffeb627 100644 --- a/frontend/lib/l10n/app_de.arb +++ b/frontend/lib/l10n/app_de.arb @@ -57,7 +57,6 @@ "identification_activate": "Aktivieren", "identification_activateCardCurrentDevice": "Karte auf diesem Gerät aktivieren?", "identification_activateCardCurrentDeviceRationale": "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", - "identification_activationInvalidState": "Die Aktivierung befindet sich in einem ungültigen Zustand.", "identification_authenticationPossible": "Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:", "identification_cameraAccessRequired": "Zugriff auf Kamera erforderlich", "identification_cameraAccessRequiredSettings": "Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera.\nIn den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.", @@ -113,7 +112,7 @@ "search_filterByCategories": "Nach Kategorien filtern", "search_findCloseBy": "In meiner Nähe suchen", "search_noAcceptingStoresFound": "Auf diese Suche trifft keine Akzeptanzstelle zu.", - "search_searchHint": "Tippen, um zu suchen...", + "search_searchHint": "Tippen, um zu suchen …", "search_searchResults": "Suchresultate", "search_title": "Suche", diff --git a/frontend/lib/l10n/app_en.arb b/frontend/lib/l10n/app_en.arb index 8269af5a0..04358dd91 100644 --- a/frontend/lib/l10n/app_en.arb +++ b/frontend/lib/l10n/app_en.arb @@ -20,8 +20,8 @@ "category_education": "Education", "category_fashion": "Fashion", "category_fashionLong": "Fashion/Beauty", - "category_food": "Restaurants", - "category_foodLong": "Food/Drink/Restaurants", + "category_food": "Gastronomy", + "category_foodLong": "Food/Drink/Gastronomy", "category_health": "Health", "category_healthLong": "Health/Sports/Wellness", "category_leisure": "Leisure", @@ -57,12 +57,11 @@ "identification_activate": "Activate", "identification_activateCardCurrentDevice": "Activate card on this device?", "identification_activateCardCurrentDeviceRationale": "Your card is already activated on another device. If you activate your card on this device, it will be automatically deactivated on your other device.", - "identification_activationInvalidState": "The activation is in an invalid state.", "identification_authenticationPossible": "You can use this QR code to identify yourself at acceptance points:", "identification_cameraAccessRequired": "Access to camera required", "identification_cameraAccessRequiredSettings": "To scan a QR code, the app needs access to the camera.\nYou can allow the app to access the camera in the settings.", "identification_cardAlreadyActivated": "The scanned QR code has already been activated.", - "identification_cardExpired": "Your card has expired.\nUnder \"More actions\" you can apply for renewal.", + "identification_cardExpired": "Your card has expired.\nYou can apply for renewal under \"More actions\"", "identification_cardInvalid": "Your card is invalid.\nIt has either been revoked or activated on another device.", "identification_cardNotYetValid": "The validity period of your card has not started yet.", "identification_checkFailed": "Your card could not be verified. Please make sure you have an internet connection and try again.", @@ -74,16 +73,16 @@ "identification_codeSavingFailed": "The scanned code cannot be saved in the app.", "identification_codeUnknownError": "An unknown error occurred while scanning the QR code.", "identification_codeVerificationFailed": "The scanned code could not be verified by the server.", - "identification_codeActivationFailedConnection": "Der eingescannte Code konnte nicht aktiviert werden, da die Kommunikation mit dem Server fehlschlug. Bitte prüfen Sie Ihre Internetverbindung.", + "identification_codeActivationFailedConnection": "The scanned code could not be activated. Please make sure you have an internet connection and try again.", "identification_codeVerificationFailedConnection": "The scanned code could not be verified. Please make sure you have an internet connection and try again.", "identification_compareWithID": "Verify the displayed data against an official photo ID.", "identification_comparedWithID": "I verified the data against an official photo ID.", "identification_flashOff": "Flash off", "identification_flashOn": "Flash on", - "identification_internetRequired": "An Internet connection is required.", + "identification_internetRequired": "An internet connection is required.", "identification_notVerified": "Not verified", - "identification_scanCode": "Scan the QR code that appears on the \"Identify\" tab of your counterpart.", - "identification_scanQRCode": "Hold the camera on the QR code.", + "identification_scanCode": "Scan the QR code that appears on the \"Identify\" tab of the other party.", + "identification_scanQRCode": "Point the camera at the QR code.", "identification_scanningFailed": "Error reading the code", "identification_selfieCamera": "Front camera", "identification_standardCamera": "Standard camera", @@ -96,7 +95,7 @@ "location_activateLocationAccess": "Activate location access", "location_activateLocationAccessRationale": "Allow the app to use your location to display acceptance points in your area.", - "location_activateLocationAccessSettings": "Activate the location detection in the settings.", + "location_activateLocationAccessSettings": "Enable location access in Settings.", "location_askPermissionsAgain": "Should permission be asked again?", "location_grantLocation": "I would like to share my location.", "location_grantPermission": "Grant permission", @@ -112,13 +111,13 @@ "search_filterByCategories": "Filter by categories", "search_findCloseBy": "Search near me", - "search_noAcceptingStoresFound": "No accepting points found matching this search.", - "search_searchHint": "Tap to search...", + "search_noAcceptingStoresFound": "No acceptance points found matching this search.", + "search_searchHint": "Tap to search …", "search_searchResults": "Search results", "search_title": "Search", - "store_acceptingStore": "Accepting points", - "store_acceptingStoreNotFound": "Accepting point not found.", + "store_acceptingStore": "Acceptance points", + "store_acceptingStoreNotFound": "Acceptance point not found.", "store_address": "Address", "store_email": "E-Mail", "store_loadingDataFailed": "Error loading the data.", From 32665c8ad56c0532665772c2a68ea6508e610897 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 23 Oct 2023 16:26:22 +0200 Subject: [PATCH 44/60] 1178: Fix lunch offers --- frontend/lib/l10n/app_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/lib/l10n/app_en.arb b/frontend/lib/l10n/app_en.arb index 04358dd91..b37938f8c 100644 --- a/frontend/lib/l10n/app_en.arb +++ b/frontend/lib/l10n/app_en.arb @@ -28,7 +28,7 @@ "category_leisureLong": "Leisure/Travel/Accommodation", "category_living": "Furnishing", "category_livingLong": "Living/Home/Garden", - "category_lunchTables": "Lunch tables", + "category_lunchTables": "Lunch offers", "category_media": "Multimedia", "category_mobility": "Mobility", "category_mobilityLong": "Car/Bicycle", From 957927c1fd13da708f3167f825c6e02a51e44232 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Thu, 26 Oct 2023 15:24:28 +0200 Subject: [PATCH 45/60] 1177: Use slang for l10n --- frontend/.gitignore | 1 + frontend/assets/l10n/main/app_de.json | 144 ++++++++++++++++++ frontend/assets/l10n/main/app_en.json | 144 ++++++++++++++++++ .../assets/l10n/nuernberg/overwrite_de.json | 5 + .../assets/l10n/nuernberg/overwrite_en.json | 5 + frontend/build-configs/bayern/index.ts | 1 + frontend/build-configs/nuernberg/index.ts | 1 + frontend/build-configs/types.ts | 1 + frontend/build.yaml | 12 +- frontend/l10n.yaml | 3 - frontend/lib/about/about_page.dart | 23 ++- frontend/lib/about/license_page.dart | 6 +- frontend/lib/app.dart | 4 +- frontend/lib/category_assets.dart | 78 +++++----- frontend/lib/home/home_page.dart | 10 +- .../activation_code_scanner_page.dart | 16 +- .../activation_overwrite_existing_dialog.dart | 10 +- .../card_detail_view/card_detail_view.dart | 20 +-- .../card_detail_view/more_actions_dialog.dart | 6 +- .../connection_failed_dialog.dart | 4 +- .../identification/id_card/card_content.dart | 8 +- frontend/lib/identification/info_dialog.dart | 4 +- .../qr_code_camera_permission_dialog.dart | 10 +- .../qr_code_scanner/qr_code_scanner.dart | 4 +- .../qr_code_scanner_controls.dart | 8 +- .../qr_parsing_error_dialog.dart | 6 +- .../negative_verification_result_dialog.dart | 4 +- .../positive_verification_result_dialog.dart | 9 +- .../dialogs/verification_info_dialog.dart | 14 +- .../verification_qr_scanner_page.dart | 14 +- frontend/lib/intro_slides/intro_screen.dart | 8 +- .../intro_slides/location_request_button.dart | 10 +- frontend/lib/l10n/app_de.arb | 129 ---------------- frontend/lib/l10n/app_en.arb | 129 ---------------- frontend/lib/location/determine_position.dart | 4 +- frontend/lib/location/dialogs.dart | 19 ++- frontend/lib/main.dart | 35 ++++- frontend/lib/map/location_button.dart | 6 +- frontend/lib/map/map/attribution_dialog.dart | 6 +- frontend/lib/map/map/map.dart | 4 +- .../preview/accepting_store_preview_card.dart | 4 +- frontend/lib/search/filter_bar.dart | 4 +- frontend/lib/search/location_button.dart | 4 +- frontend/lib/search/results_loader.dart | 8 +- frontend/lib/search/search_page.dart | 4 +- frontend/lib/sentry.dart | 7 +- .../accepting_store_summary.dart | 8 +- .../store_widgets/detail/detail_app_bar.dart | 4 +- .../store_widgets/detail/detail_content.dart | 12 +- .../lib/store_widgets/detail/detail_page.dart | 8 +- frontend/lib/util/l10n.dart | 6 - frontend/lib/widgets/app_bars.dart | 4 +- .../pubs/df_build_config/lib/builder.dart | 2 + frontend/pubspec.lock | 40 +++++ frontend/pubspec.yaml | 4 + 55 files changed, 579 insertions(+), 465 deletions(-) create mode 100644 frontend/assets/l10n/main/app_de.json create mode 100644 frontend/assets/l10n/main/app_en.json create mode 100644 frontend/assets/l10n/nuernberg/overwrite_de.json create mode 100644 frontend/assets/l10n/nuernberg/overwrite_en.json delete mode 100644 frontend/l10n.yaml delete mode 100644 frontend/lib/l10n/app_de.arb delete mode 100644 frontend/lib/l10n/app_en.arb delete mode 100644 frontend/lib/util/l10n.dart diff --git a/frontend/.gitignore b/frontend/.gitignore index ffa5cd2e9..a75aaaf91 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -36,3 +36,4 @@ lib/graphql/graphql_api.graphql.dart lib/graphql/graphql_api.graphql.g.dart lib/proto +/lib/l10n/translations.g.dart diff --git a/frontend/assets/l10n/main/app_de.json b/frontend/assets/l10n/main/app_de.json new file mode 100644 index 000000000..e95d4d3cf --- /dev/null +++ b/frontend/assets/l10n/main/app_de.json @@ -0,0 +1,144 @@ +{ + "about": { + "dependencies": "Software-Bibliotheken", + "developmentOptions": "Entwickleroptionen", + "disclaimer": "Haftung, Haftungsausschluss und Impressum", + "licenses": { + "one": "Lizenz", + "other": "Lizenzen" + }, + "moreInformation": "Mehr Informationen", + "numberLicenses": { + "zero": "Keine Lizenzen", + "one": "1 Lizenz", + "other": "$n Lizenzen" + }, + "privacyDeclaration": "Datenschutzerklärung", + "publisher": "Herausgeber", + "sourceCode": "Quellcode der App", + "title": "Über" + }, + "category": { + "clothing": "Kleidung", + "clothingLong": "Kleidung/Gebrauchtes", + "culture": "Kultur", + "cultureLong": "Bildung/Kultur/Unterhaltung", + "cultureLongNuernberg": "Kultur/Museen/Freizeit", + "digitalParticipation": "Digitale Teilhabe", + "education": "Bildung", + "fashion": "Mode", + "fashionLong": "Mode/Beauty", + "food": "Gastronomie", + "foodLong": "Essen/Trinken/Gastronomie", + "health": "Gesundheit", + "healthLong": "Gesundheit/Sport/Wellness", + "leisure": "Freizeit", + "leisureLong": "Freizeit/Reise/Unterkünfte", + "living": "Einrichtung", + "livingLong": "Wohnen/Haus/Garten", + "lunchTables": "Mittagstische", + "media": "Multimedia", + "mobility": "Mobilität", + "mobilityLong": "Auto/Zweirad", + "movies": "Schauspiel", + "moviesLong": "Kinos/Theater/Konzerte", + "other": "Anderes", + "pharmacies": "Apotheken", + "pharmaciesLong": "Apotheken/Gesundheit", + "services": "Dienstleistung", + "servicesLong": "Dienstleistungen/Finanzen", + "sports": "Sport", + "sportsLong": "Sport/Bewegung/Tanz" + }, + "common": { + "cancel": "Abbrechen", + "checkConnection": "Bitte Internetverbindung prüfen.", + "connectionFailed": "Keine Verbindung möglich", + "done": "Fertig", + "moreActions": "Weitere Aktionen", + "next": "Weiter", + "ok": "OK", + "openSettings": "Einstellungen öffnen", + "previous": "Zurück", + "settings": "Einstellungen", + "tryAgain": "Erneut versuchen" + }, + "identification": { + "activate": "Aktivieren", + "activateCardCurrentDevice": "Karte auf diesem Gerät aktivieren?", + "activateCardCurrentDeviceRationale": "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", + "authenticationPossible": "Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:", + "cameraAccessRequired": "Zugriff auf Kamera erforderlich", + "cameraAccessRequiredSettings": "Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera.\nIn den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.", + "cardAlreadyActivated": "Der eingescannte QRCode wurde bereits aktiviert.", + "cardExpired": "Ihre Karte ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", + "cardInvalid": "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", + "cardNotYetValid": "Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen.", + "checkFailed": "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Internetverbindung besteht und prüfen Sie erneut.", + "checkRequired": "Prüfung nötig", + "checkingCode": "Der QR-Code wird durch eine Server-Anfrage geprüft.", + "codeExpired": "Der eingescannte Code ist bereits am {{expirationDate}} abgelaufen.", + "codeInvalid": "Der Inhalt des eingescannten Codes kann nicht verstanden werden. Vermutlich handelt es sich um einen QR-Code, der nicht für diese App generiert wurde.", + "codeInvalidMissing": "Der Inhalt des eingescannten Codes ist unvollständig. (Fehlercode: {{missing}}Missing)", + "codeSavingFailed": "Der eingescannte Code kann nicht in der App gespeichert werden.", + "codeUnknownError": "Beim Scannen des QR-Codes ist ein unbekannter Fehler aufgetreten.", + "codeVerificationFailed": "Der eingescannte Code konnte vom Server nicht verifiziert werden.", + "codeActivationFailedConnection": "Der eingescannte Code konnte nicht aktiviert werden. Bitte stellen Sie sicher, dass eine Internetverbindung besteht und prüfen Sie erneut.", + "codeVerificationFailedConnection": "Der eingescannte Code konnte nicht verifiziert werden. Bitte stellen Sie sicher, dass eine Internetverbindung besteht und prüfen Sie erneut.", + "compareWithID": "Gleichen Sie die angezeigten Daten mit einem amtlichen Lichtbildausweis ab.", + "comparedWithID": "Ich habe die Daten mit einem amtlichen Lichtbildausweis abgeglichen.", + "flashOff": "Blitz aus", + "flashOn": "Blitz an", + "internetRequired": "Eine Internetverbindung wird benötigt.", + "notVerified": "Nicht verifiziert", + "scanCode": "Scannen Sie den QR-Code, der auf dem \"Ausweisen\"-Tab Ihres Gegenübers angezeigt wird.", + "scanQRCode": "Halten Sie die Kamera auf den QR Code.", + "scanningFailed": "Fehler beim Lesen des Codes", + "selfieCamera": "Frontkamera", + "standardCamera": "Standard-Kamera", + "stopShowing": "Nicht mehr anzeigen", + "timeIncorrect": "Die Uhrzeit Ihres Geräts scheint nicht zu stimmen. Bitte synchronisieren Sie die Uhrzeit in den Systemeinstellungen.", + "title": "Ausweisen", + "unlimited": "unbegrenzt", + "validFromUntil": "Gültig: {{startDate}} bis {{expirationDate}}", + "validUntil": "Gültig bis: {{expirationDate}}" + }, + "location": { + "activateLocationAccess": "Standortermittlung aktivieren", + "activateLocationAccessRationale": "Erlauben Sie der App Ihren Standort zu benutzen, um Akzeptanzstellen in Ihrer Umgebung anzuzeigen.", + "activateLocationAccessSettings": "Aktivieren Sie die Standortermittlung in den Einstellungen.", + "askPermissionsAgain": "Soll nocheinmal nach der Berechtigung gefragt werden?", + "grantLocation": "Ich möchte meinen Standort freigeben.", + "grantPermission": "Berechtigung erteilen", + "locationAccessDeactivated": "Die Standortfreigabe ist deaktiviert.", + "locationGranted": "Standort ist freigegeben.", + "locationPermission": "Standortberechtigung", + "checkSettings": "Prüfe Einstellungen..." + }, + "map": { + "mapData": "Kartendaten", + "osmContributors": "OpenStreetMap Mitwirkende", + "showMapCopyright": "Zeige Infos über das Urheberrecht der Kartendaten", + "title": "Karte" + }, + "search": { + "filterByCategories": "Nach Kategorien filtern", + "findCloseBy": "In meiner Nähe suchen", + "noAcceptingStoresFound": "Auf diese Suche trifft keine Akzeptanzstelle zu.", + "searchHint": "Tippen, um zu suchen …", + "searchResults": "Suchresultate", + "title": "Suche" + }, + "store": { + "acceptingStore": "Akzeptanzstelle", + "acceptingStoreNotFound": "Akzeptanzstelle nicht gefunden.", + "address": "Adresse", + "email": "E-Mail", + "loadingDataFailed": "Fehler beim Laden der Daten.", + "noDescriptionAvailable": "Keine Beschreibung verfügbar", + "phone": "Telefon", + "showOnMap": "Auf Karte zeigen", + "unknownCategory": "Unbekannte Kategorie", + "website": "Website" + } +} diff --git a/frontend/assets/l10n/main/app_en.json b/frontend/assets/l10n/main/app_en.json new file mode 100644 index 000000000..33cbaf4fe --- /dev/null +++ b/frontend/assets/l10n/main/app_en.json @@ -0,0 +1,144 @@ +{ + "about": { + "dependencies": "Libraries", + "developmentOptions": "Developer options", + "disclaimer": "Liability, disclaimer and imprint", + "licenses": { + "one": "License", + "other": "Licenses" + }, + "moreInformation": "More information", + "numberLicenses": { + "zero": "No licenses", + "one": "1 license", + "other": "$n licenses" + }, + "privacyDeclaration": "Privacy policy", + "publisher": "Publisher", + "sourceCode": "Source code", + "title": "About" + }, + "category": { + "clothing": "Clothing", + "clothingLong": "Clothing/Second hand", + "culture": "Culture", + "cultureLong": "Education/Culture/Entertainment", + "cultureLongNuernberg": "Culture/Museums/Leisure", + "digitalParticipation": "Digital participation", + "education": "Education", + "fashion": "Fashion", + "fashionLong": "Fashion/Beauty", + "food": "Gastronomy", + "foodLong": "Food/Drink/Gastronomy", + "health": "Health", + "healthLong": "Health/Sports/Wellness", + "leisure": "Leisure", + "leisureLong": "Leisure/Travel/Accommodation", + "living": "Furnishing", + "livingLong": "Living/Home/Garden", + "lunchTables": "Lunch offers", + "media": "Multimedia", + "mobility": "Mobility", + "mobilityLong": "Car/Bicycle", + "movies": "Plays", + "moviesLong": "Cinema/Theater/Concerts", + "other": "Other", + "pharmacies": "Pharmacies", + "pharmaciesLong": "Pharmacies/Health", + "services": "Services", + "servicesLong": "Services/Finances", + "sports": "Sports", + "sportsLong": "Sports/Movement/Dance" + }, + "common": { + "cancel": "Cancel", + "checkConnection": "Please check internet connection.", + "connectionFailed": "No connection possible", + "done": "Done", + "moreActions": "More actions", + "next": "Next", + "ok": "OK", + "openSettings": "Open settings", + "previous": "Back", + "settings": "Settings", + "tryAgain": "Try again" + }, + "identification": { + "activate": "Activate", + "activateCardCurrentDevice": "Activate card on this device?", + "activateCardCurrentDeviceRationale": "Your card is already activated on another device. If you activate your card on this device, it will be automatically deactivated on your other device.", + "authenticationPossible": "You can use this QR code to identify yourself at acceptance points:", + "cameraAccessRequired": "Access to camera required", + "cameraAccessRequiredSettings": "To scan a QR code, the app needs access to the camera.\nYou can allow the app to access the camera in the settings.", + "cardAlreadyActivated": "The scanned QR code has already been activated.", + "cardExpired": "Your card has expired.\nYou can apply for renewal under \"More actions\"", + "cardInvalid": "Your card is invalid.\nIt has either been revoked or activated on another device.", + "cardNotYetValid": "The validity period of your card has not started yet.", + "checkFailed": "Your card could not be verified. Please make sure you have an internet connection and try again.", + "checkRequired": "Verification necessary", + "checkingCode": "The QR code is verified by a server request.", + "codeExpired": "The scanned code has already expired on {{expirationDate}}.", + "codeInvalid": "The content of the scanned code cannot be understood. It is probably a QR code that was not generated for this app.", + "codeInvalidMissing": "The content of the scanned code is incomplete. (Error code: {{missing}}Missing)", + "codeSavingFailed": "The scanned code cannot be saved in the app.", + "codeUnknownError": "An unknown error occurred while scanning the QR code.", + "codeVerificationFailed": "The scanned code could not be verified by the server.", + "codeActivationFailedConnection": "The scanned code could not be activated. Please make sure you have an internet connection and try again.", + "codeVerificationFailedConnection": "The scanned code could not be verified. Please make sure you have an internet connection and try again.", + "compareWithID": "Verify the displayed data against an official photo ID.", + "comparedWithID": "I verified the data against an official photo ID.", + "flashOff": "Flash off", + "flashOn": "Flash on", + "internetRequired": "An internet connection is required.", + "notVerified": "Not verified", + "scanCode": "Scan the QR code that appears on the \"Identify\" tab of the other party.", + "scanQRCode": "Point the camera at the QR code.", + "scanningFailed": "Error reading the code", + "selfieCamera": "Front camera", + "standardCamera": "Standard camera", + "stopShowing": "Stop showing", + "timeIncorrect": "The time of your device does not seem to be correct. Please synchronize the time in the system settings.", + "title": "Identify", + "unlimited": "unlimited", + "validFromUntil": "Valid: {{startDate}} until {{expirationDate}}", + "validUntil": "Valid until: {{expirationDate}}" + }, + "location": { + "activateLocationAccess": "Activate location access", + "activateLocationAccessRationale": "Allow the app to use your location to display acceptance points in your area.", + "activateLocationAccessSettings": "Enable location access in Settings.", + "askPermissionsAgain": "Should permission be asked again?", + "grantLocation": "I would like to share my location.", + "grantPermission": "Grant permission", + "locationAccessDeactivated": "Location is disabled.", + "locationGranted": "Location is enabled.", + "locationPermission": "Location permission", + "checkSettings": "Checking settings..." + }, + "map": { + "mapData": "Map data", + "osmContributors": "OpenStreetMap Contributors", + "showMapCopyright": "Show info about map data copyright", + "title": "Map" + }, + "search": { + "filterByCategories": "Filter by categories", + "findCloseBy": "Search near me", + "noAcceptingStoresFound": "No acceptance points found matching this search.", + "searchHint": "Tap to search …", + "searchResults": "Search results", + "title": "Search" + }, + "store": { + "acceptingStore": "Acceptance points", + "acceptingStoreNotFound": "Acceptance point not found.", + "address": "Address", + "email": "E-Mail", + "loadingDataFailed": "Error loading the data.", + "noDescriptionAvailable": "No description available", + "phone": "Phone", + "showOnMap": "Show on map", + "unknownCategory": "Unknown category", + "website": "Website" + } +} diff --git a/frontend/assets/l10n/nuernberg/overwrite_de.json b/frontend/assets/l10n/nuernberg/overwrite_de.json new file mode 100644 index 000000000..6a50940ba --- /dev/null +++ b/frontend/assets/l10n/nuernberg/overwrite_de.json @@ -0,0 +1,5 @@ +{ + "about": { + "title": "Nürnberg" + } +} diff --git a/frontend/assets/l10n/nuernberg/overwrite_en.json b/frontend/assets/l10n/nuernberg/overwrite_en.json new file mode 100644 index 000000000..6a50940ba --- /dev/null +++ b/frontend/assets/l10n/nuernberg/overwrite_en.json @@ -0,0 +1,5 @@ +{ + "about": { + "title": "Nürnberg" + } +} diff --git a/frontend/build-configs/bayern/index.ts b/frontend/build-configs/bayern/index.ts index 2811db5b4..dcf2c4d52 100644 --- a/frontend/build-configs/bayern/index.ts +++ b/frontend/build-configs/bayern/index.ts @@ -32,6 +32,7 @@ export const bayernCommon: CommonBuildConfigType = { local: "http://localhost:8000", }, appLocales: ['de'], + localeOverwritePath: null, cardBranding: { headerTextColor: "#008dc9", headerColor: "#F5F5FFF5", diff --git a/frontend/build-configs/nuernberg/index.ts b/frontend/build-configs/nuernberg/index.ts index 13c4e89bf..5a865a0ac 100644 --- a/frontend/build-configs/nuernberg/index.ts +++ b/frontend/build-configs/nuernberg/index.ts @@ -32,6 +32,7 @@ export const nuernbergCommon: CommonBuildConfigType = { local: "http://localhost:8000", }, appLocales: ['de', 'en'], + localeOverwritePath: 'assets/l10n/nuernberg', cardBranding: { headerTextColor: "#000000", headerTextFontSize: 9, diff --git a/frontend/build-configs/types.ts b/frontend/build-configs/types.ts index 4ee273919..13d07865f 100644 --- a/frontend/build-configs/types.ts +++ b/frontend/build-configs/types.ts @@ -79,6 +79,7 @@ export type CommonBuildConfigType = { local: string } appLocales: string[] + localeOverwritePath: string | null cardBranding: { headerTextColor: string headerColor: string diff --git a/frontend/build.yaml b/frontend/build.yaml index a5f599b57..4d9f53c9d 100644 --- a/frontend/build.yaml +++ b/frontend/build.yaml @@ -8,10 +8,11 @@ targets: - schema.graphql - card.proto - lib/build_config/build_config.yaml + - assets/l10n/main/** builders: df_build_config: generate_for: - include: + include: - "lib/build_config/build_config.yaml" enabled: True df_protobuf: @@ -34,3 +35,12 @@ targets: name: MultipartFile imports: - 'package:http/http.dart' + slang_build_runner: + options: + base_locale: de + input_directory: assets/l10n/main + input_file_pattern: .json + output_file_name: translations.g.dart + output_directory: lib/l10n + translation_overrides: true + string_interpolation: double_braces diff --git a/frontend/l10n.yaml b/frontend/l10n.yaml deleted file mode 100644 index 86aa4f42a..000000000 --- a/frontend/l10n.yaml +++ /dev/null @@ -1,3 +0,0 @@ -arb-dir: lib/l10n -template-arb-file: app_de.arb -output-localization-file: app_localizations.dart diff --git a/frontend/lib/about/about_page.dart b/frontend/lib/about/about_page.dart index d91d87a11..10e942cdb 100644 --- a/frontend/lib/about/about_page.dart +++ b/frontend/lib/about/about_page.dart @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class AboutPage extends StatefulWidget { final countToEnableSwitch = 10; @@ -70,14 +70,14 @@ class AboutPageState extends State { child: Column( children: [ Center( - child: Text(context.l10n.about_publisher, style: Theme.of(context).textTheme.titleSmall), + child: Text(t.about.publisher, style: Theme.of(context).textTheme.titleSmall), ), Padding( padding: const EdgeInsets.only(left: 10, right: 10, top: 16, bottom: 16), child: Text(buildConfig.publisherAddress, style: Theme.of(context).textTheme.bodyLarge), ), Text( - context.l10n.about_moreInformation, + t.about.moreInformation, style: Theme.of(context) .textTheme .bodyMedium @@ -90,8 +90,7 @@ class AboutPageState extends State { Navigator.push( context, AppRoute( - builder: (context) => - ContentPage(title: context.l10n.about_publisher, children: getPublisherText(context)), + builder: (context) => ContentPage(title: t.about.publisher, children: getPublisherText(context)), ), ); }, @@ -101,20 +100,20 @@ class AboutPageState extends State { thickness: 1, ), const SizedBox(height: 20), - ContentTile(icon: Icons.copyright, title: context.l10n.about_license, children: getCopyrightText(context)), + ContentTile(icon: Icons.copyright, title: t.about.licenses(n: 1), children: getCopyrightText(context)), ListTile( leading: const Icon(Icons.privacy_tip_outlined), - title: Text(context.l10n.about_privacyDeclaration), + title: Text(t.about.privacyDeclaration), onTap: () => launchUrlString(buildConfig.dataPrivacyPolicyUrl, mode: LaunchMode.externalApplication), ), ContentTile( icon: Icons.info_outline, - title: context.l10n.about_disclaimer, + title: t.about.disclaimer, children: getDisclaimerText(context), ), ListTile( leading: const Icon(Icons.book_outlined), - title: Text(context.l10n.about_dependencies), + title: Text(t.about.dependencies), onTap: () { Navigator.push( context, @@ -126,7 +125,7 @@ class AboutPageState extends State { ), ListTile( leading: const Icon(Icons.code_outlined), - title: Text(context.l10n.about_sourceCode), + title: Text(t.about.sourceCode), onTap: () { launchUrlString( 'https://github.com/digitalfabrik/entitlementcard', @@ -137,11 +136,11 @@ class AboutPageState extends State { if (config.showDevSettings) ListTile( leading: const Icon(Icons.build), - title: Text(context.l10n.about_developmentOptions), + title: Text(t.about.developmentOptions), onTap: () => showDialog( context: context, builder: (context) => - SimpleDialog(title: Text(context.l10n.about_developmentOptions), children: [DevSettingsView()]), + SimpleDialog(title: Text(t.about.developmentOptions), children: [DevSettingsView()]), ), ) ]; diff --git a/frontend/lib/about/license_page.dart b/frontend/lib/about/license_page.dart index e84b41742..6c9c85182 100644 --- a/frontend/lib/about/license_page.dart +++ b/frontend/lib/about/license_page.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/widgets/top_loading_spinner.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class CustomLicenseEntry { final String packageName; @@ -53,7 +53,7 @@ class CustomLicensePage extends StatelessWidget { return CustomScrollView( slivers: [ - CustomSliverAppBar(title: context.l10n.about_licenses), + CustomSliverAppBar(title: t.about.licenses(n: licenses.length)), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { @@ -61,7 +61,7 @@ class CustomLicensePage extends StatelessWidget { final paragraphs = license.licenseParagraphs; return ListTile( title: Text(license.packageName), - subtitle: Text(context.l10n.about_numberLicenses(paragraphs.length)), + subtitle: Text(t.about.numberLicenses(n: paragraphs.length)), onTap: () { Navigator.push( context, diff --git a/frontend/lib/app.dart b/frontend/lib/app.dart index e6201fe42..ac8ebaecd 100644 --- a/frontend/lib/app.dart +++ b/frontend/lib/app.dart @@ -5,12 +5,12 @@ import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/graphql/configured_graphql_provider.dart'; import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:ehrenamtskarte/intro_slides/intro_screen.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; import 'package:ehrenamtskarte/themes.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'home/home_page.dart'; @@ -68,12 +68,12 @@ class App extends StatelessWidget { themeMode: ThemeMode.system, debugShowCheckedModeBanner: false, localizationsDelegates: [ - AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: buildConfig.appLocales.map((locale) => Locale(locale)), + locale: TranslationProvider.of(context).flutterLocale, initialRoute: initialRoute, routes: routes, ), diff --git a/frontend/lib/category_assets.dart b/frontend/lib/category_assets.dart index 36715e734..49981506c 100644 --- a/frontend/lib/category_assets.dart +++ b/frontend/lib/category_assets.dart @@ -1,4 +1,4 @@ -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; import 'package:flutter/material.dart'; const test = Color(0xff5f5384); @@ -33,152 +33,152 @@ List categoryAssets(BuildContext context) { return [ CategoryAsset( id: 0, - name: context.l10n.category_mobilityLong, - shortName: context.l10n.category_mobility, + name: t.category.mobilityLong, + shortName: t.category.mobility, icon: 'assets/category_icons/0.svg', detailIcon: 'assets/detail_headers/0_auto.svg', color: Color(0xffE89600), ), CategoryAsset( id: 1, - name: context.l10n.category_media, - shortName: context.l10n.category_media, + name: t.category.media, + shortName: t.category.media, icon: 'assets/category_icons/1.svg', detailIcon: 'assets/detail_headers/1_multimedia.svg', color: Color(0xFFFA0000), ), CategoryAsset( id: 2, - name: context.l10n.category_healthLong, - shortName: context.l10n.category_health, + name: t.category.healthLong, + shortName: t.category.health, icon: 'assets/category_icons/2.svg', detailIcon: 'assets/detail_headers/2_sport.svg', color: Color(0xFFE500D3), ), CategoryAsset( id: 3, - name: context.l10n.category_cultureLong, - shortName: context.l10n.category_culture, + name: t.category.cultureLong, + shortName: t.category.culture, icon: 'assets/category_icons/3.svg', detailIcon: 'assets/detail_headers/3_kultur.svg', color: Color(0xFF7500EB), ), CategoryAsset( id: 4, - name: context.l10n.category_servicesLong, - shortName: context.l10n.category_services, + name: t.category.servicesLong, + shortName: t.category.services, icon: 'assets/category_icons/4.svg', detailIcon: 'assets/detail_headers/4_finanzen.svg', color: Color(0xFF515151), ), CategoryAsset( id: 5, - name: context.l10n.category_fashionLong, - shortName: context.l10n.category_fashion, + name: t.category.fashionLong, + shortName: t.category.fashion, icon: 'assets/category_icons/5.svg', detailIcon: 'assets/detail_headers/5_mode.svg', color: Color(0xFF6EBE00), ), CategoryAsset( id: 6, - name: context.l10n.category_livingLong, - shortName: context.l10n.category_living, + name: t.category.livingLong, + shortName: t.category.living, icon: 'assets/category_icons/6.svg', detailIcon: 'assets/detail_headers/6_haus.svg', color: Color(0xFF00D0C7), ), CategoryAsset( id: 7, - name: context.l10n.category_leisureLong, - shortName: context.l10n.category_leisure, + name: t.category.leisureLong, + shortName: t.category.leisure, icon: 'assets/category_icons/7.svg', detailIcon: 'assets/detail_headers/7_freizeit.svg', color: Color(0xFF007CE8), ), CategoryAsset( id: 8, - name: context.l10n.category_foodLong, - shortName: context.l10n.category_food, + name: t.category.foodLong, + shortName: t.category.food, icon: 'assets/category_icons/8.svg', detailIcon: 'assets/detail_headers/8_essen.svg', color: Color(0xFF197489), ), CategoryAsset( id: 9, - name: context.l10n.category_other, - shortName: context.l10n.category_other, + name: t.category.other, + shortName: t.category.other, icon: 'assets/category_icons/9.svg', detailIcon: null, color: Color(0xFFc51162), ), CategoryAsset( id: 10, - name: context.l10n.category_lunchTables, - shortName: context.l10n.category_lunchTables, + name: t.category.lunchTables, + shortName: t.category.lunchTables, icon: 'assets/category_icons/10.svg', detailIcon: 'assets/detail_headers/10_mittagstische.svg', color: Color(0xFF197489), ), CategoryAsset( id: 11, - name: context.l10n.category_clothingLong, - shortName: context.l10n.category_clothing, + name: t.category.clothingLong, + shortName: t.category.clothing, icon: 'assets/category_icons/11.svg', detailIcon: 'assets/detail_headers/11_kleidung.svg', color: Color(0xFF6EBE00), ), CategoryAsset( id: 12, - name: context.l10n.category_cultureLongNuernberg, - shortName: context.l10n.category_culture, + name: t.category.cultureLongNuernberg, + shortName: t.category.culture, icon: 'assets/category_icons/12.svg', detailIcon: 'assets/detail_headers/12_kultur.svg', color: Color(0xFF7500EB), ), CategoryAsset( id: 13, - name: context.l10n.category_education, - shortName: context.l10n.category_education, + name: t.category.education, + shortName: t.category.education, icon: 'assets/category_icons/13.svg', detailIcon: null, color: Color(0xFFd100cc), ), CategoryAsset( id: 14, - name: context.l10n.category_moviesLong, - shortName: context.l10n.category_movies, + name: t.category.moviesLong, + shortName: t.category.movies, icon: 'assets/category_icons/14.svg', detailIcon: null, color: Color(0xFFc51162), ), CategoryAsset( id: 15, - name: context.l10n.category_pharmaciesLong, - shortName: context.l10n.category_pharmacies, + name: t.category.pharmaciesLong, + shortName: t.category.pharmacies, icon: 'assets/category_icons/15.svg', detailIcon: null, color: Color(0xFF007be0), ), CategoryAsset( id: 16, - name: context.l10n.category_digitalParticipation, - shortName: context.l10n.category_digitalParticipation, + name: t.category.digitalParticipation, + shortName: t.category.digitalParticipation, icon: 'assets/category_icons/16.svg', detailIcon: 'assets/detail_headers/16_teilhabe.svg', color: Color(0xFFFA0000), ), CategoryAsset( id: 17, - name: context.l10n.category_sportsLong, - shortName: context.l10n.category_sports, + name: t.category.sportsLong, + shortName: t.category.sports, icon: 'assets/category_icons/17.svg', detailIcon: 'assets/detail_headers/17_sport.svg', color: Color(0xFF00ccc6), ), CategoryAsset( id: 18, - name: context.l10n.category_mobility, - shortName: context.l10n.category_mobility, + name: t.category.mobility, + shortName: t.category.mobility, icon: 'assets/category_icons/18.svg', detailIcon: 'assets/detail_headers/18_mobilitaet.svg', color: Color(0xffE89600), diff --git a/frontend/lib/home/home_page.dart b/frontend/lib/home/home_page.dart index 3038e9909..aec036628 100644 --- a/frontend/lib/home/home_page.dart +++ b/frontend/lib/home/home_page.dart @@ -11,7 +11,7 @@ import 'package:ehrenamtskarte/search/search_page.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; const mapTabIndex = 0; @@ -39,23 +39,23 @@ class HomePageState extends State { selectAcceptingStore: (id) => setState(() => selectedAcceptingStoreId = id), ), Icons.map_outlined, - (BuildContext context) => context.l10n.map_title, + (BuildContext context) => t.map.title, GlobalKey(debugLabel: 'Map tab key'), ), AppFlow( const SearchPage(), Icons.search_outlined, - (BuildContext context) => context.l10n.search_title, + (BuildContext context) => t.search.title, GlobalKey(debugLabel: 'Search tab key'), ), if (buildConfig.featureFlags.verification) AppFlow( IdentificationPage(), Icons.remove_red_eye_outlined, - (BuildContext context) => context.l10n.identification_title, + (BuildContext context) => t.identification.title, GlobalKey(debugLabel: 'Auth tab key'), ), - AppFlow(const AboutPage(), Icons.info_outline, (BuildContext context) => context.l10n.about_title, + AppFlow(const AboutPage(), Icons.info_outline, (BuildContext context) => t.about.title, GlobalKey(debugLabel: 'About tab key')), ]; } diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index f88ff9fea..373959f8c 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -25,7 +25,7 @@ import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class ActivationCodeScannerPage extends StatelessWidget { final VoidCallback moveToLastCard; @@ -55,19 +55,19 @@ class ActivationCodeScannerPage extends StatelessWidget { await _activateCode(context, activationCode); } on ActivationDidNotOverwriteExisting catch (_) { - await showError(context.l10n.identification_cardAlreadyActivated, null); + await showError(t.identification.cardAlreadyActivated, null); } on QrCodeFieldMissingException catch (e) { - await showError(context.l10n.identification_codeInvalidMissing(e.missingFieldName), null); + await showError(t.identification.codeInvalidMissing(missing: e.missingFieldName), null); } on QrCodeWrongTypeException catch (_) { - await showError(context.l10n.identification_codeSavingFailed, null); + await showError(t.identification.codeSavingFailed, null); } on CardExpiredException catch (e) { final expirationDate = DateFormat('dd.MM.yyyy').format(e.expiry); - await showError(context.l10n.identification_codeExpired(expirationDate), null); + await showError(t.identification.codeExpired(expirationDate: expirationDate), null); } on ServerCardActivationException catch (_) { - await ConnectionFailedDialog.show(context, context.l10n.identification_codeActivationFailedConnection); + await ConnectionFailedDialog.show(context, t.identification.codeActivationFailedConnection); } on Exception catch (e, stacktrace) { debugPrintStack(stackTrace: stacktrace, label: e.toString()); - await showError(context.l10n.identification_codeUnknownError, null); + await showError(t.identification.codeUnknownError, null); } } @@ -116,7 +116,7 @@ class ActivationCodeScannerPage extends StatelessWidget { case ActivationState.failed: await QrParsingErrorDialog.showErrorDialog( context, - context.l10n.identification_codeInvalid, + t.identification.codeInvalid, ); break; case ActivationState.didNotOverwriteExisting: diff --git a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart index 2232d3df7..3eabcce7e 100644 --- a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart +++ b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class ActivationOverwriteExistingDialog extends StatelessWidget { const ActivationOverwriteExistingDialog({super.key}); @@ -8,23 +8,23 @@ class ActivationOverwriteExistingDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(context.l10n.identification_activateCardCurrentDevice, style: TextStyle(fontSize: 18)), + title: Text(t.identification.activateCardCurrentDevice, style: TextStyle(fontSize: 18)), content: SingleChildScrollView( child: ListBody( children: [ - Text(context.l10n.identification_activateCardCurrentDeviceRationale), + Text(t.identification.activateCardCurrentDeviceRationale), ], ), ), actions: [ TextButton( - child: Text(context.l10n.common_cancel), + child: Text(t.common.cancel), onPressed: () { Navigator.of(context).pop(false); }, ), TextButton( - child: Text(context.l10n.identification_activate), + child: Text(t.identification.activate), onPressed: () { Navigator.of(context).pop(true); }, diff --git a/frontend/lib/identification/card_detail_view/card_detail_view.dart b/frontend/lib/identification/card_detail_view/card_detail_view.dart index 224b68cf1..c7e67079e 100644 --- a/frontend/lib/identification/card_detail_view/card_detail_view.dart +++ b/frontend/lib/identification/card_detail_view/card_detail_view.dart @@ -8,7 +8,7 @@ import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; import 'verification_code_view.dart'; class CardDetailView extends StatefulWidget { @@ -185,33 +185,33 @@ class QrCodeAndStatus extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ...switch (status) { - CardStatus.expired => [_PaddedText(context.l10n.identification_cardExpired)], + CardStatus.expired => [_PaddedText(t.identification.cardExpired)], CardStatus.notVerifiedLately => [ - _PaddedText(context.l10n.identification_checkFailed), + _PaddedText(t.identification.checkFailed), Flexible( child: TextButton.icon( icon: const Icon(Icons.refresh), onPressed: onSelfVerifyPressed, - label: Text(context.l10n.common_tryAgain), + label: Text(t.common.tryAgain), ), ), ], CardStatus.timeOutOfSync => [ - _PaddedText(context.l10n.identification_timeIncorrect), + _PaddedText(t.identification.timeIncorrect), Flexible( child: TextButton.icon( icon: const Icon(Icons.refresh), onPressed: onSelfVerifyPressed, - label: Text(context.l10n.common_tryAgain), + label: Text(t.common.tryAgain), )) ], - CardStatus.invalid => [_PaddedText(context.l10n.identification_cardInvalid)], + CardStatus.invalid => [_PaddedText(t.identification.cardInvalid)], CardStatus.valid => [ - _PaddedText(context.l10n.identification_authenticationPossible), + _PaddedText(t.identification.authenticationPossible), Flexible(child: VerificationCodeView(userCode: userCode)) ], CardStatus.notYetValid => [ - _PaddedText(context.l10n.identification_cardNotYetValid), + _PaddedText(t.identification.cardNotYetValid), ] }, Container( @@ -219,7 +219,7 @@ class QrCodeAndStatus extends StatelessWidget { child: TextButton( onPressed: onMoreActionsPressed, child: Text( - context.l10n.common_moreActions, + t.common.moreActions, style: TextStyle(color: Theme.of(context).colorScheme.secondary), ), ), diff --git a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart index 16a898211..eaa0d6171 100644 --- a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart +++ b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart @@ -3,7 +3,7 @@ import 'package:ehrenamtskarte/identification/user_code_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class MoreActionsDialog extends StatelessWidget { final VoidCallback startActivation; @@ -29,7 +29,7 @@ class MoreActionsDialog extends StatelessWidget { return AlertDialog( contentPadding: const EdgeInsets.only(top: 12), - title: Text(context.l10n.common_moreActions), + title: Text(t.common.moreActions), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -77,7 +77,7 @@ class MoreActionsDialog extends StatelessWidget { ], ), ), - actions: [TextButton(onPressed: () => Navigator.pop(context), child: Text(context.l10n.common_cancel))], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel))], ); } } diff --git a/frontend/lib/identification/connection_failed_dialog.dart b/frontend/lib/identification/connection_failed_dialog.dart index a49c442f2..5f0495c41 100644 --- a/frontend/lib/identification/connection_failed_dialog.dart +++ b/frontend/lib/identification/connection_failed_dialog.dart @@ -1,7 +1,7 @@ import 'package:ehrenamtskarte/identification/info_dialog.dart'; import 'package:flutter/material.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class ConnectionFailedDialog extends StatelessWidget { final String reason; @@ -11,7 +11,7 @@ class ConnectionFailedDialog extends StatelessWidget { @override Widget build(BuildContext context) { return InfoDialog( - title: context.l10n.common_connectionFailed, + title: t.common.connectionFailed, icon: Icons.signal_cellular_connected_no_internet_4_bar, iconColor: Theme.of(context).colorScheme.onBackground, child: Text(reason), diff --git a/frontend/lib/identification/id_card/card_content.dart b/frontend/lib/identification/id_card/card_content.dart index bd295ace3..be8e79148 100644 --- a/frontend/lib/identification/id_card/card_content.dart +++ b/frontend/lib/identification/id_card/card_content.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/util/color_utils.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; Color standardCardColor = getColorFromHex(buildConfig.cardBranding.colorStandard); Color premiumCardColor = getColorFromHex(buildConfig.cardBranding.colorPremium); @@ -52,7 +52,7 @@ class CardContent extends StatelessWidget { final expirationDay = cardInfo.hasExpirationDay() ? cardInfo.expirationDay : null; return expirationDay != null ? DateFormat('dd.MM.yyyy').format(DateTime.fromMillisecondsSinceEpoch(0).add(Duration(days: expirationDay))) - : context.l10n.identification_unlimited; + : t.identification.unlimited; } String? get _formattedBirthday { @@ -77,8 +77,8 @@ class CardContent extends StatelessWidget { String _getCardValidityDate(BuildContext context, String? startDate, String expirationDate) { return startDate != null - ? context.l10n.identification_validFromUntil(expirationDate, startDate) - : context.l10n.identification_validUntil(expirationDate); + ? t.identification.validFromUntil(startDate: startDate, expirationDate: expirationDate) + : t.identification.validUntil(expirationDate: expirationDate); } @override diff --git a/frontend/lib/identification/info_dialog.dart b/frontend/lib/identification/info_dialog.dart index c63f8fef8..aceccc19c 100644 --- a/frontend/lib/identification/info_dialog.dart +++ b/frontend/lib/identification/info_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class InfoDialog extends StatelessWidget { final Widget child; @@ -25,7 +25,7 @@ class InfoDialog extends StatelessWidget { title: Text(title, style: theme.textTheme.headlineSmall), ), content: child, - actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.l10n.common_ok))], + actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(t.common.ok))], ); } } diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart index 3fd5f2cde..1d4911a3f 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_camera_permission_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class QrCodeCameraPermissionDialog extends StatelessWidget { const QrCodeCameraPermissionDialog(); @@ -9,23 +9,23 @@ class QrCodeCameraPermissionDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(context.l10n.identification_cameraAccessRequired, style: TextStyle(fontSize: 18)), + title: Text(t.identification.cameraAccessRequired, style: TextStyle(fontSize: 18)), content: SingleChildScrollView( child: ListBody( children: [ - Text(context.l10n.identification_cameraAccessRequiredSettings), + Text(t.identification.cameraAccessRequiredSettings), ], ), ), actions: [ TextButton( - child: Text(context.l10n.common_cancel), + child: Text(t.common.cancel), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( - child: Text(context.l10n.common_openSettings), + child: Text(t.common.openSettings), onPressed: () { openAppSettings(); }, diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart index 661bc320a..bf0f5684a 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_overlay_shape.d import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; typedef OnCodeScannedCallback = Future Function(Uint8List code); @@ -77,7 +77,7 @@ class _QRViewState extends State { children: [ Container( margin: const EdgeInsets.all(8), - child: Text(context.l10n.identification_scanQRCode), + child: Text(t.identification.scanQRCode), ), QrCodeScannerControls(controller: controller) ], diff --git a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart index 6d2c00fb3..0dd49d2e3 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_code_scanner_controls.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class QrCodeScannerControls extends StatelessWidget { final MobileScannerController controller; @@ -21,7 +21,7 @@ class QrCodeScannerControls extends StatelessWidget { child: ValueListenableBuilder( valueListenable: controller.torchState, builder: (ctx, state, child) => Text( - state == TorchState.on ? context.l10n.identification_flashOff : context.l10n.identification_flashOn, + state == TorchState.on ? t.identification.flashOff : t.identification.flashOn, style: const TextStyle(fontSize: 16), ), ), @@ -34,9 +34,7 @@ class QrCodeScannerControls extends StatelessWidget { child: ValueListenableBuilder( valueListenable: controller.cameraFacingState, builder: (ctx, state, child) => Text( - state == CameraFacing.back - ? context.l10n.identification_selfieCamera - : context.l10n.identification_standardCamera, + state == CameraFacing.back ? t.identification.selfieCamera : t.identification.standardCamera, style: const TextStyle(fontSize: 16), ), ), diff --git a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart index 89bbdba0f..ce1b3ab78 100644 --- a/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart +++ b/frontend/lib/identification/qr_code_scanner/qr_parsing_error_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class QrParsingErrorDialog extends StatelessWidget { final String message; @@ -10,7 +10,7 @@ class QrParsingErrorDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(context.l10n.identification_scanningFailed), + title: Text(t.identification.scanningFailed), content: SingleChildScrollView( child: ListBody( children: [ @@ -20,7 +20,7 @@ class QrParsingErrorDialog extends StatelessWidget { ), actions: [ TextButton( - child: Text(context.l10n.common_ok), + child: Text(t.common.ok), onPressed: () { Navigator.of(context).pop(); }, diff --git a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart index 6e71db0b4..ae0b0cb29 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/negative_verification_result_dialog.dart @@ -1,7 +1,7 @@ import 'package:ehrenamtskarte/identification/info_dialog.dart'; import 'package:flutter/material.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class NegativeVerificationResultDialog extends StatelessWidget { final String reason; @@ -11,7 +11,7 @@ class NegativeVerificationResultDialog extends StatelessWidget { @override Widget build(BuildContext context) { return InfoDialog( - title: context.l10n.identification_notVerified, + title: t.identification.notVerified, icon: Icons.error, iconColor: Colors.red, child: Text(reason), diff --git a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart index 77f48e2c3..7991168a3 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart @@ -7,7 +7,7 @@ import 'package:ehrenamtskarte/proto/card.pb.dart'; import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class PositiveVerificationResultDialog extends StatefulWidget { final CardInfo cardInfo; @@ -57,9 +57,8 @@ class PositiveVerificationResultDialogState extends State _onDone(context), ) ], diff --git a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart index ac2adc1a1..62d8ca0b4 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart @@ -19,7 +19,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class VerificationQrScannerPage extends StatelessWidget { final DynamicUserCode? userCode; @@ -76,7 +76,7 @@ class VerificationQrScannerPage extends StatelessWidget { if (cardInfo == null) { await _onError( context, - context.l10n.identification_codeVerificationFailed, + t.identification.codeVerificationFailed, ); } else { await _onSuccess(context, cardInfo, qrcode.hasStaticVerificationCode()); @@ -84,32 +84,32 @@ class VerificationQrScannerPage extends StatelessWidget { } on ServerVerificationException catch (e) { await _onConnectionError( context, - context.l10n.identification_codeVerificationFailedConnection, + t.identification.codeVerificationFailedConnection, e, ); } on QrCodeFieldMissingException catch (e) { await _onError( context, - context.l10n.identification_codeInvalidMissing(e.missingFieldName), + t.identification.codeInvalidMissing(missing: e.missingFieldName), e, ); } on CardExpiredException catch (e) { final expirationDate = DateFormat('dd.MM.yyyy').format(e.expiry); await _onError( context, - context.l10n.identification_codeExpired(expirationDate), + t.identification.codeExpired(expirationDate: expirationDate), e, ); } on QrCodeParseException catch (e) { await _onError( context, - context.l10n.identification_codeInvalid, + t.identification.codeInvalid, e, ); } on Exception catch (e) { await _onError( context, - context.l10n.identification_codeUnknownError, + t.identification.codeUnknownError, e, ); } finally { diff --git a/frontend/lib/intro_slides/intro_screen.dart b/frontend/lib/intro_slides/intro_screen.dart index 4aa5579d1..284ba989a 100644 --- a/frontend/lib/intro_slides/intro_screen.dart +++ b/frontend/lib/intro_slides/intro_screen.dart @@ -4,7 +4,7 @@ import 'package:ehrenamtskarte/intro_slides/location_request_button.dart'; import 'package:flutter/material.dart'; import 'package:intro_slider/intro_slider.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; typedef OnFinishedCallback = void Function(); @@ -23,9 +23,9 @@ class IntroScreen extends StatelessWidget { final theme = Theme.of(context); return IntroSlider( onDonePress: () => onDonePress(context), - renderDoneBtn: Text(context.l10n.common_done), - renderNextBtn: Text(context.l10n.common_next), - renderPrevBtn: Text(context.l10n.common_previous), + renderDoneBtn: Text(t.common.done), + renderNextBtn: Text(t.common.next), + renderPrevBtn: Text(t.common.previous), doneButtonStyle: Theme.of(context).textButtonTheme.style, indicatorConfig: IndicatorConfig( colorActiveIndicator: theme.colorScheme.primary, diff --git a/frontend/lib/intro_slides/location_request_button.dart b/frontend/lib/intro_slides/location_request_button.dart index cb566f250..cdb9849de 100644 --- a/frontend/lib/intro_slides/location_request_button.dart +++ b/frontend/lib/intro_slides/location_request_button.dart @@ -3,7 +3,7 @@ import 'package:ehrenamtskarte/location/determine_position.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class LocationRequestButton extends StatefulWidget { const LocationRequestButton({super.key}); @@ -53,7 +53,7 @@ class _LocationRequestButtonState extends State { if (status == null) { return ElevatedButton( onPressed: null, - child: Text(context.l10n.location_checkSettings), + child: Text(t.location.checkSettings), ); } switch (status) { @@ -61,18 +61,18 @@ class _LocationRequestButtonState extends State { case LocationStatus.notSupported: return ElevatedButton( onPressed: () => _onLocationButtonClicked(settings), - child: Text(context.l10n.location_grantLocation), + child: Text(t.location.grantLocation), ); case LocationStatus.whileInUse: case LocationStatus.always: return ElevatedButton( onPressed: null, - child: Text(context.l10n.location_locationGranted), + child: Text(t.location.locationGranted), ); case LocationStatus.deniedForever: return ElevatedButton( onPressed: null, - child: Text(context.l10n.location_locationAccessDeactivated), + child: Text(t.location.locationAccessDeactivated), ); } } diff --git a/frontend/lib/l10n/app_de.arb b/frontend/lib/l10n/app_de.arb deleted file mode 100644 index 45ffeb627..000000000 --- a/frontend/lib/l10n/app_de.arb +++ /dev/null @@ -1,129 +0,0 @@ -{ - "about_dependencies": "Software-Bibliotheken", - "about_developmentOptions": "Entwickleroptionen", - "about_disclaimer": "Haftung, Haftungsausschluss und Impressum", - "about_license": "Lizenz", - "about_licenses": "Lizenzen", - "about_moreInformation": "Mehr Informationen", - "about_numberLicenses": "{count, plural, =0{Keine Lizenzen} =1{1 Lizenz} other{{count} Lizenzen}}", - "about_privacyDeclaration": "Datenschutzerklärung", - "about_publisher": "Herausgeber", - "about_sourceCode": "Quellcode der App", - "about_title": "Über", - - "category_clothing": "Kleidung", - "category_clothingLong": "Kleidung/Gebrauchtes", - "category_culture": "Kultur", - "category_cultureLong": "Bildung/Kultur/Unterhaltung", - "category_cultureLongNuernberg": "Kultur/Museen/Freizeit", - "category_digitalParticipation": "Digitale Teilhabe", - "category_education": "Bildung", - "category_fashion": "Mode", - "category_fashionLong": "Mode/Beauty", - "category_food": "Gastronomie", - "category_foodLong": "Essen/Trinken/Gastronomie", - "category_health": "Gesundheit", - "category_healthLong": "Gesundheit/Sport/Wellness", - "category_leisure": "Freizeit", - "category_leisureLong": "Freizeit/Reise/Unterkünfte", - "category_living": "Einrichtung", - "category_livingLong": "Wohnen/Haus/Garten", - "category_lunchTables": "Mittagstische", - "category_media": "Multimedia", - "category_mobility": "Mobilität", - "category_mobilityLong": "Auto/Zweirad", - "category_movies": "Schauspiel", - "category_moviesLong": "Kinos/Theater/Konzerte", - "category_other": "Anderes", - "category_pharmacies": "Apotheken", - "category_pharmaciesLong": "Apotheken/Gesundheit", - "category_services": "Dienstleistung", - "category_servicesLong": "Dienstleistungen/Finanzen", - "category_sports": "Sport", - "category_sportsLong": "Sport/Bewegung/Tanz", - - "common_cancel": "Abbrechen", - "common_checkConnection": "Bitte Internetverbindung prüfen.", - "common_connectionFailed": "Keine Verbindung möglich", - "common_done": "Fertig", - "common_moreActions": "Weitere Aktionen", - "common_next": "Weiter", - "common_ok": "OK", - "common_openSettings": "Einstellungen öffnen", - "common_previous": "Zurück", - "common_settings": "Einstellungen", - "common_tryAgain": "Erneut versuchen", - - "identification_activate": "Aktivieren", - "identification_activateCardCurrentDevice": "Karte auf diesem Gerät aktivieren?", - "identification_activateCardCurrentDeviceRationale": "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", - "identification_authenticationPossible": "Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:", - "identification_cameraAccessRequired": "Zugriff auf Kamera erforderlich", - "identification_cameraAccessRequiredSettings": "Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera.\nIn den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.", - "identification_cardAlreadyActivated": "Der eingescannte QRCode wurde bereits aktiviert.", - "identification_cardExpired": "Ihre Karte ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", - "identification_cardInvalid": "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", - "identification_cardNotYetValid": "Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen.", - "identification_checkFailed": "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Internetverbindung besteht und prüfen Sie erneut.", - "identification_checkRequired": "Prüfung nötig", - "identification_checkingCode": "Der QR-Code wird durch eine Server-Anfrage geprüft.", - "identification_codeExpired": "Der eingescannte Code ist bereits am {expirationDate} abgelaufen.", - "identification_codeInvalid": "Der Inhalt des eingescannten Codes kann nicht verstanden werden. Vermutlich handelt es sich um einen QR-Code, der nicht für diese App generiert wurde.", - "identification_codeInvalidMissing": "Der Inhalt des eingescannten Codes ist unvollständig. (Fehlercode: {missing}Missing)", - "identification_codeSavingFailed": "Der eingescannte Code kann nicht in der App gespeichert werden.", - "identification_codeUnknownError": "Beim Scannen des QR-Codes ist ein unbekannter Fehler aufgetreten.", - "identification_codeVerificationFailed": "Der eingescannte Code konnte vom Server nicht verifiziert werden.", - "identification_codeActivationFailedConnection": "Der eingescannte Code konnte nicht aktiviert werden. Bitte stellen Sie sicher, dass eine Internetverbindung besteht und prüfen Sie erneut.", - "identification_codeVerificationFailedConnection": "Der eingescannte Code konnte nicht verifiziert werden. Bitte stellen Sie sicher, dass eine Internetverbindung besteht und prüfen Sie erneut.", - "identification_compareWithID": "Gleichen Sie die angezeigten Daten mit einem amtlichen Lichtbildausweis ab.", - "identification_comparedWithID": "Ich habe die Daten mit einem amtlichen Lichtbildausweis abgeglichen.", - "identification_flashOff": "Blitz aus", - "identification_flashOn": "Blitz an", - "identification_internetRequired": "Eine Internetverbindung wird benötigt.", - "identification_notVerified": "Nicht verifiziert", - "identification_scanCode": "Scannen Sie den QR-Code, der auf dem \"Ausweisen\"-Tab Ihres Gegenübers angezeigt wird.", - "identification_scanQRCode": "Halten Sie die Kamera auf den QR Code.", - "identification_scanningFailed": "Fehler beim Lesen des Codes", - "identification_selfieCamera": "Frontkamera", - "identification_standardCamera": "Standard-Kamera", - "identification_stopShowing": "Nicht mehr anzeigen", - "identification_timeIncorrect": "Die Uhrzeit Ihres Geräts scheint nicht zu stimmen. Bitte synchronisieren Sie die Uhrzeit in den Systemeinstellungen.", - "identification_title": "Ausweisen", - "identification_unlimited": "unbegrenzt", - "identification_validFromUntil": "Gültig: {startDate} bis {expirationDate}", - "identification_validUntil": "Gültig bis: {expirationDate}", - - "location_activateLocationAccess": "Standortermittlung aktivieren", - "location_activateLocationAccessRationale": "Erlauben Sie der App Ihren Standort zu benutzen, um Akzeptanzstellen in Ihrer Umgebung anzuzeigen.", - "location_activateLocationAccessSettings": "Aktivieren Sie die Standortermittlung in den Einstellungen.", - "location_askPermissionsAgain": "Soll nocheinmal nach der Berechtigung gefragt werden?", - "location_grantLocation": "Ich möchte meinen Standort freigeben.", - "location_grantPermission": "Berechtigung erteilen", - "location_locationAccessDeactivated": "Die Standortfreigabe ist deaktiviert.", - "location_locationGranted": "Standort ist freigegeben.", - "location_locationPermission": "Standortberechtigung", - "location_checkSettings": "Prüfe Einstellungen...", - - "map_mapData": "Kartendaten", - "map_osmContributors": "OpenStreetMap Mitwirkende", - "map_showMapCopyright": "Zeige Infos über das Urheberrecht der Kartendaten", - "map_title": "Karte", - - "search_filterByCategories": "Nach Kategorien filtern", - "search_findCloseBy": "In meiner Nähe suchen", - "search_noAcceptingStoresFound": "Auf diese Suche trifft keine Akzeptanzstelle zu.", - "search_searchHint": "Tippen, um zu suchen …", - "search_searchResults": "Suchresultate", - "search_title": "Suche", - - "store_acceptingStore": "Akzeptanzstelle", - "store_acceptingStoreNotFound": "Akzeptanzstelle nicht gefunden.", - "store_address": "Adresse", - "store_email": "E-Mail", - "store_loadingDataFailed": "Fehler beim Laden der Daten.", - "store_noDescriptionAvailable": "Keine Beschreibung verfügbar", - "store_phone": "Telefon", - "store_showOnMap": "Auf Karte zeigen", - "store_unknownCategory": "Unbekannte Kategorie", - "store_website": "Website" -} diff --git a/frontend/lib/l10n/app_en.arb b/frontend/lib/l10n/app_en.arb deleted file mode 100644 index b37938f8c..000000000 --- a/frontend/lib/l10n/app_en.arb +++ /dev/null @@ -1,129 +0,0 @@ -{ - "about_dependencies": "Libraries", - "about_developmentOptions": "Developer options", - "about_disclaimer": "Liability, disclaimer and imprint", - "about_license": "License", - "about_licenses": "Licenses", - "about_moreInformation": "More information", - "about_numberLicenses": "{count, plural, =0{No licenses} =1{1 License} other{{count} Licenses}}", - "about_privacyDeclaration": "Privacy policy", - "about_publisher": "Publisher", - "about_sourceCode": "Source code", - "about_title": "About", - - "category_clothing": "Clothing", - "category_clothingLong": "Clothing/Second hand", - "category_culture": "Culture", - "category_cultureLong": "Education/Culture/Entertainment", - "category_cultureLongNuernberg": "Culture/Museums/Leisure", - "category_digitalParticipation": "Digital participation", - "category_education": "Education", - "category_fashion": "Fashion", - "category_fashionLong": "Fashion/Beauty", - "category_food": "Gastronomy", - "category_foodLong": "Food/Drink/Gastronomy", - "category_health": "Health", - "category_healthLong": "Health/Sports/Wellness", - "category_leisure": "Leisure", - "category_leisureLong": "Leisure/Travel/Accommodation", - "category_living": "Furnishing", - "category_livingLong": "Living/Home/Garden", - "category_lunchTables": "Lunch offers", - "category_media": "Multimedia", - "category_mobility": "Mobility", - "category_mobilityLong": "Car/Bicycle", - "category_movies": "Plays", - "category_moviesLong": "Cinema/Theater/Concerts", - "category_other": "Other", - "category_pharmacies": "Pharmacies", - "category_pharmaciesLong": "Pharmacies/Health", - "category_services": "Services", - "category_servicesLong": "Services/Finances", - "category_sports": "Sports", - "category_sportsLong": "Sports/Movement/Dance", - - "common_cancel": "Cancel", - "common_checkConnection": "Please check internet connection.", - "common_connectionFailed": "No connection possible", - "common_done": "Done", - "common_moreActions": "More actions", - "common_next": "Next", - "common_ok": "OK", - "common_openSettings": "Open settings", - "common_previous": "Back", - "common_settings": "Settings", - "common_tryAgain": "Try again", - - "identification_activate": "Activate", - "identification_activateCardCurrentDevice": "Activate card on this device?", - "identification_activateCardCurrentDeviceRationale": "Your card is already activated on another device. If you activate your card on this device, it will be automatically deactivated on your other device.", - "identification_authenticationPossible": "You can use this QR code to identify yourself at acceptance points:", - "identification_cameraAccessRequired": "Access to camera required", - "identification_cameraAccessRequiredSettings": "To scan a QR code, the app needs access to the camera.\nYou can allow the app to access the camera in the settings.", - "identification_cardAlreadyActivated": "The scanned QR code has already been activated.", - "identification_cardExpired": "Your card has expired.\nYou can apply for renewal under \"More actions\"", - "identification_cardInvalid": "Your card is invalid.\nIt has either been revoked or activated on another device.", - "identification_cardNotYetValid": "The validity period of your card has not started yet.", - "identification_checkFailed": "Your card could not be verified. Please make sure you have an internet connection and try again.", - "identification_checkRequired": "Verification necessary", - "identification_checkingCode": "The QR code is verified by a server request.", - "identification_codeExpired": "The scanned code has already expired on {expirationDate}.", - "identification_codeInvalid": "The content of the scanned code cannot be understood. It is probably a QR code that was not generated for this app.", - "identification_codeInvalidMissing": "The content of the scanned code is incomplete. (Error code: {missing}Missing)", - "identification_codeSavingFailed": "The scanned code cannot be saved in the app.", - "identification_codeUnknownError": "An unknown error occurred while scanning the QR code.", - "identification_codeVerificationFailed": "The scanned code could not be verified by the server.", - "identification_codeActivationFailedConnection": "The scanned code could not be activated. Please make sure you have an internet connection and try again.", - "identification_codeVerificationFailedConnection": "The scanned code could not be verified. Please make sure you have an internet connection and try again.", - "identification_compareWithID": "Verify the displayed data against an official photo ID.", - "identification_comparedWithID": "I verified the data against an official photo ID.", - "identification_flashOff": "Flash off", - "identification_flashOn": "Flash on", - "identification_internetRequired": "An internet connection is required.", - "identification_notVerified": "Not verified", - "identification_scanCode": "Scan the QR code that appears on the \"Identify\" tab of the other party.", - "identification_scanQRCode": "Point the camera at the QR code.", - "identification_scanningFailed": "Error reading the code", - "identification_selfieCamera": "Front camera", - "identification_standardCamera": "Standard camera", - "identification_stopShowing": "Stop showing", - "identification_timeIncorrect": "The time of your device does not seem to be correct. Please synchronize the time in the system settings.", - "identification_title": "Identify", - "identification_unlimited": "unlimited", - "identification_validFromUntil": "Valid: {startDate} until {expirationDate}", - "identification_validUntil": "Valid until: {expirationDate}", - - "location_activateLocationAccess": "Activate location access", - "location_activateLocationAccessRationale": "Allow the app to use your location to display acceptance points in your area.", - "location_activateLocationAccessSettings": "Enable location access in Settings.", - "location_askPermissionsAgain": "Should permission be asked again?", - "location_grantLocation": "I would like to share my location.", - "location_grantPermission": "Grant permission", - "location_locationAccessDeactivated": "Location is disabled.", - "location_locationGranted": "Location is enabled.", - "location_locationPermission": "Location permission", - "location_checkSettings": "Checking settings...", - - "map_mapData": "Map data", - "map_osmContributors": "OpenStreetMap Contributors", - "map_showMapCopyright": "Show info about map data copyright", - "map_title": "Map", - - "search_filterByCategories": "Filter by categories", - "search_findCloseBy": "Search near me", - "search_noAcceptingStoresFound": "No acceptance points found matching this search.", - "search_searchHint": "Tap to search …", - "search_searchResults": "Search results", - "search_title": "Search", - - "store_acceptingStore": "Acceptance points", - "store_acceptingStoreNotFound": "Acceptance point not found.", - "store_address": "Address", - "store_email": "E-Mail", - "store_loadingDataFailed": "Error loading the data.", - "store_noDescriptionAvailable": "No description available", - "store_phone": "Phone", - "store_showOnMap": "Show on map", - "store_unknownCategory": "Unknown category", - "store_website": "Website" -} diff --git a/frontend/lib/location/determine_position.dart b/frontend/lib/location/determine_position.dart index 83d6b60e2..42c9d726d 100644 --- a/frontend/lib/location/determine_position.dart +++ b/frontend/lib/location/determine_position.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:maplibre_gl/mapbox_gl.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; enum LocationStatus { /// This is the initial state on both Android and iOS, but on Android the @@ -146,7 +146,7 @@ Future checkAndRequestLocationPermission( final result = await showDialog( context: context, - builder: (context) => RationaleDialog(rationale: context.l10n.location_activateLocationAccessRationale)); + builder: (context) => RationaleDialog(rationale: t.location.activateLocationAccessRationale)); if (result == true) { return checkAndRequestLocationPermission( diff --git a/frontend/lib/location/dialogs.dart b/frontend/lib/location/dialogs.dart index bfb10ba68..08749c416 100644 --- a/frontend/lib/location/dialogs.dart +++ b/frontend/lib/location/dialogs.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class LocationServiceDialog extends StatelessWidget { const LocationServiceDialog({super.key}); @@ -8,11 +8,11 @@ class LocationServiceDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(context.l10n.location_activateLocationAccess), - content: Text(context.l10n.location_activateLocationAccessSettings), + title: Text(t.location.activateLocationAccess), + content: Text(t.location.activateLocationAccessSettings), actions: [ - TextButton(child: Text(context.l10n.common_cancel), onPressed: () => Navigator.of(context).pop(false)), - TextButton(child: Text(context.l10n.common_openSettings), onPressed: () => Navigator.of(context).pop(true)) + TextButton(child: Text(t.common.cancel), onPressed: () => Navigator.of(context).pop(false)), + TextButton(child: Text(t.common.openSettings), onPressed: () => Navigator.of(context).pop(true)) ], ); } @@ -26,16 +26,15 @@ class RationaleDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(context.l10n.location_locationPermission), + title: Text(t.location.locationPermission), content: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: [Text(_rationale), Text(context.l10n.location_askPermissionsAgain)], + children: [Text(_rationale), Text(t.location.askPermissionsAgain)], ), actions: [ - TextButton( - child: Text(context.l10n.location_grantPermission), onPressed: () => Navigator.of(context).pop(true)), - TextButton(child: Text(context.l10n.common_cancel), onPressed: () => Navigator.of(context).pop(false)) + TextButton(child: Text(t.location.grantPermission), onPressed: () => Navigator.of(context).pop(true)), + TextButton(child: Text(t.common.cancel), onPressed: () => Navigator.of(context).pop(false)) ], ); } diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 806d76177..dbbaf25c6 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,16 +1,47 @@ +import 'dart:io'; + import 'package:ehrenamtskarte/app.dart'; +import 'package:ehrenamtskarte/build_config/build_config.dart'; import 'package:ehrenamtskarte/configuration/definitions.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; import 'package:ehrenamtskarte/sentry.dart'; import 'package:ehrenamtskarte/settings_provider.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + // Only use device locale if set as available in build config, otherwise fallback to de + final locale = Platform.localeName.split('_')[0]; + if (buildConfig.appLocales.contains(locale)) { + LocaleSettings.useDeviceLocale(); + } else { + LocaleSettings.setLocale(AppLocale.de); + } + + // Use override locales for whitelabels (e.g. nuernberg) + // ignore: unnecessary_null_comparison + if (buildConfig.localeOverwritePath != null) { + void override(AppLocale locale) async { + final localeOverwritePath = '${buildConfig.localeOverwritePath}/overwrite_${locale.languageCode}.json'; + String overrideLocales = await rootBundle.loadString(localeOverwritePath); + // TODO uncomment in #1177 + // LocaleSettings.overrideTranslations(locale: locale, fileType: FileType.json, content: overrideLocales); + } + + AppLocale.values.forEach(override); + } + debugPrint('Environment: $appEnvironment'); + + void run() { + return runApp(TranslationProvider(child: SettingsProvider(child: const App()))); + } + if (isProduction()) { - runAppWithSentry(); + runAppWithSentry(run); } else { - runApp(SettingsProvider(child: const App())); + run(); } } diff --git a/frontend/lib/map/location_button.dart b/frontend/lib/map/location_button.dart index f68dd12c5..51ff6f614 100644 --- a/frontend/lib/map/location_button.dart +++ b/frontend/lib/map/location_button.dart @@ -4,7 +4,7 @@ import 'package:ehrenamtskarte/widgets/small_button_spinner.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class LocationButton extends StatefulWidget { final Future Function(RequestedPosition) bringCameraToUser; @@ -54,9 +54,9 @@ class _LocationButtonState extends State { messengerState.showSnackBar( SnackBar( behavior: SnackBarBehavior.floating, - content: Text(context.l10n.location_locationAccessDeactivated), + content: Text(t.location.locationAccessDeactivated), action: SnackBarAction( - label: context.l10n.common_settings, + label: t.common.settings, onPressed: () async { await openSettingsToGrantPermissions(context); }, diff --git a/frontend/lib/map/map/attribution_dialog.dart b/frontend/lib/map/map/attribution_dialog.dart index 66551c4b3..d4881b139 100644 --- a/frontend/lib/map/map/attribution_dialog.dart +++ b/frontend/lib/map/map/attribution_dialog.dart @@ -2,7 +2,7 @@ import 'package:ehrenamtskarte/map/map/attribution_dialog_item.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class AttributionDialog extends StatelessWidget { const AttributionDialog({super.key}); @@ -11,12 +11,12 @@ class AttributionDialog extends StatelessWidget { Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.primary; return SimpleDialog( - title: Text(context.l10n.map_mapData), + title: Text(t.map.mapData), children: [ AttributionDialogItem( icon: Icons.copyright, color: color, - text: context.l10n.map_osmContributors, + text: t.map.osmContributors, onPressed: () { launchUrlString('https://www.openstreetmap.org/copyright', mode: LaunchMode.externalApplication); }, diff --git a/frontend/lib/map/map/map.dart b/frontend/lib/map/map/map.dart index a25960ac1..addf92c3d 100644 --- a/frontend/lib/map/map/map.dart +++ b/frontend/lib/map/map/map.dart @@ -12,7 +12,7 @@ import 'package:geolocator/geolocator.dart'; import 'package:maplibre_gl/mapbox_gl.dart'; import 'package:tuple/tuple.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; typedef OnFeatureClickCallback = void Function(dynamic feature); typedef OnNoFeatureClickCallback = void Function(); @@ -98,7 +98,7 @@ class _MapContainerState extends State implements MapController { color: mapboxColor, iconSize: 20, icon: const Icon(Icons.info_outline), - tooltip: context.l10n.map_showMapCopyright, + tooltip: t.map.showMapCopyright, onPressed: () { showDialog( context: context, diff --git a/frontend/lib/map/preview/accepting_store_preview_card.dart b/frontend/lib/map/preview/accepting_store_preview_card.dart index 15bec86c3..3e4f9537d 100644 --- a/frontend/lib/map/preview/accepting_store_preview_card.dart +++ b/frontend/lib/map/preview/accepting_store_preview_card.dart @@ -4,7 +4,7 @@ import 'package:ehrenamtskarte/store_widgets/accepting_store_summary.dart'; import 'package:ehrenamtskarte/widgets/error_message.dart'; import 'package:flutter/material.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class AcceptingStorePreviewError extends StatelessWidget { final void Function()? refetch; @@ -18,7 +18,7 @@ class AcceptingStorePreviewError extends StatelessWidget { child: Container( height: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16), - child: ErrorMessage(context.l10n.store_loadingDataFailed)), + child: ErrorMessage(t.store.loadingDataFailed)), ); } } diff --git a/frontend/lib/search/filter_bar.dart b/frontend/lib/search/filter_bar.dart index 35849d602..8f9881e8c 100644 --- a/frontend/lib/search/filter_bar.dart +++ b/frontend/lib/search/filter_bar.dart @@ -4,7 +4,7 @@ import 'package:ehrenamtskarte/category_assets.dart'; import 'package:ehrenamtskarte/search/filter_bar_button.dart'; import 'package:flutter/material.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class FilterBar extends StatelessWidget { final Function(CategoryAsset, bool) onCategoryPress; @@ -25,7 +25,7 @@ class FilterBar extends StatelessWidget { padding: const EdgeInsets.all(8), child: Row( children: [ - Text(context.l10n.search_filterByCategories.toUpperCase(), + Text(t.search.filterByCategories.toUpperCase(), maxLines: 1, style: const TextStyle(color: Colors.grey)), const Expanded(child: Padding(padding: EdgeInsets.only(left: 8), child: Divider(thickness: 0.7))) ], diff --git a/frontend/lib/search/location_button.dart b/frontend/lib/search/location_button.dart index 326876224..d1d8e69ea 100644 --- a/frontend/lib/search/location_button.dart +++ b/frontend/lib/search/location_button.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:provider/provider.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class LocationButton extends StatefulWidget { final void Function(Position position) setCoordinates; @@ -57,7 +57,7 @@ class _LocationButtonState extends State { ), ), label: Text( - context.l10n.search_findCloseBy, + t.search.findCloseBy, style: TextStyle(color: Theme.of(context).hintColor), ), ), diff --git a/frontend/lib/search/results_loader.dart b/frontend/lib/search/results_loader.dart index 04df2df45..bd955a24c 100644 --- a/frontend/lib/search/results_loader.dart +++ b/frontend/lib/search/results_loader.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class ResultsLoader extends StatefulWidget { final CoordinatesInput? coordinates; @@ -148,10 +148,10 @@ class ResultsLoaderState extends State { mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.warning, size: 60, color: Colors.orange), - Text(context.l10n.common_checkConnection), + Text(t.common.checkConnection), OutlinedButton( onPressed: _pagingController.retryLastFailedRequest, - child: Text(context.l10n.common_tryAgain), + child: Text(t.common.tryAgain), ) ], ), @@ -162,7 +162,7 @@ class ResultsLoaderState extends State { mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.search_off, size: 60, color: Theme.of(context).disabledColor), - Text(context.l10n.search_noAcceptingStoresFound), + Text(t.search.noAcceptingStoresFound), ], ), ); diff --git a/frontend/lib/search/search_page.dart b/frontend/lib/search/search_page.dart index f2fffcaec..eeb2adf5b 100644 --- a/frontend/lib/search/search_page.dart +++ b/frontend/lib/search/search_page.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/search/results_loader.dart'; import 'package:ehrenamtskarte/widgets/app_bars.dart'; import 'package:flutter/material.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class SearchPage extends StatefulWidget { const SearchPage({super.key}); @@ -39,7 +39,7 @@ class _SearchPageState extends State { child: Row( children: [ Text( - context.l10n.search_searchResults.toUpperCase(), + t.search.searchResults.toUpperCase(), style: const TextStyle(color: Colors.grey), ), const Expanded(child: Padding(padding: EdgeInsets.only(left: 8), child: Divider())) diff --git a/frontend/lib/sentry.dart b/frontend/lib/sentry.dart index e82315635..859c8faff 100644 --- a/frontend/lib/sentry.dart +++ b/frontend/lib/sentry.dart @@ -1,13 +1,10 @@ -import 'package:ehrenamtskarte/settings_provider.dart'; import 'package:flutter/cupertino.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'app.dart'; - -Future runAppWithSentry() async { +Future runAppWithSentry(void Function() runApp) async { await SentryFlutter.init((options) { options.dsn = 'https://ceb1e25ecc334e26b1469a0bc325b7c9@sentry.tuerantuer.org/4'; - }, appRunner: () => runApp(SettingsProvider(child: const App()))); + }, appRunner: runApp); } Future reportError(dynamic exception, dynamic stackTrace) async { diff --git a/frontend/lib/store_widgets/accepting_store_summary.dart b/frontend/lib/store_widgets/accepting_store_summary.dart index fb07acb4a..c401bfaa4 100644 --- a/frontend/lib/store_widgets/accepting_store_summary.dart +++ b/frontend/lib/store_widgets/accepting_store_summary.dart @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class AcceptingStoreSummary extends StatelessWidget { final AcceptingStoreSummaryModel store; @@ -40,7 +40,7 @@ class AcceptingStoreSummary extends StatelessWidget { Widget build(BuildContext context) { final categories = categoryAssets(context); final itemCategoryAsset = store.categoryId < categories.length ? categories[store.categoryId] : null; - final categoryName = itemCategoryAsset?.name ?? context.l10n.store_unknownCategory; + final categoryName = itemCategoryAsset?.name ?? t.store.unknownCategory; final categoryColor = itemCategoryAsset?.color; final useWideDepiction = MediaQuery.of(context).size.width > 400; @@ -155,14 +155,14 @@ class StoreTextOverview extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - store.name ?? context.l10n.store_acceptingStore, + store.name ?? t.store.acceptingStore, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 4), Text( - store.description ?? context.l10n.store_noDescriptionAvailable, + store.description ?? t.store.noDescriptionAvailable, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium, diff --git a/frontend/lib/store_widgets/detail/detail_app_bar.dart b/frontend/lib/store_widgets/detail/detail_app_bar.dart index c7b3ecbe8..f739a9937 100644 --- a/frontend/lib/store_widgets/detail/detail_app_bar.dart +++ b/frontend/lib/store_widgets/detail/detail_app_bar.dart @@ -6,7 +6,7 @@ import 'package:ehrenamtskarte/widgets/app_bars.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; const double bottomSize = 100; @@ -90,7 +90,7 @@ class DetailAppBar extends StatelessWidget { final category = categoryAssets(context)[categoryId]; final accentColor = getDarkenedColorForCategory(context, categoryId); - final title = matchingStore.store.name ?? context.l10n.store_acceptingStore; + final title = matchingStore.store.name ?? t.store.acceptingStore; final backgroundColor = accentColor ?? Theme.of(context).colorScheme.primary; final textColor = getReadableOnColor(backgroundColor); diff --git a/frontend/lib/store_widgets/detail/detail_content.dart b/frontend/lib/store_widgets/detail/detail_content.dart index 7158fa8d8..ec723fb4b 100644 --- a/frontend/lib/store_widgets/detail/detail_content.dart +++ b/frontend/lib/store_widgets/detail/detail_content.dart @@ -11,7 +11,7 @@ import 'package:maplibre_gl/mapbox_gl.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class DetailContent extends StatelessWidget { final AcceptingStoreById$Query$PhysicalStore acceptingStore; @@ -61,7 +61,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.location_on, addressString, - context.l10n.store_address, + t.store.address, onTap: () => _launchMap(mapQueryString), iconColor: readableOnAccentColor, iconFillColor: accentColor, @@ -70,7 +70,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.language, prepareWebsiteUrlForDisplay(website), - context.l10n.store_website, + t.store.website, onTap: () => launchUrlString(prepareWebsiteUrlForLaunch(website), mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, @@ -80,7 +80,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.phone, telephone, - context.l10n.store_phone, + t.store.phone, onTap: () => launchUrlString('tel:${sanitizePhoneNumber(telephone)}', mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, @@ -90,7 +90,7 @@ class DetailContent extends StatelessWidget { ContactInfoRow( Icons.alternate_email, email, - context.l10n.store_email, + t.store.email, onTap: () => launchUrlString('mailto:${email.trim()}', mode: LaunchMode.externalApplication), iconColor: readableOnAccentColor, iconFillColor: accentColor, @@ -107,7 +107,7 @@ class DetailContent extends StatelessWidget { alignment: MainAxisAlignment.center, children: [ OutlinedButton( - child: Text(context.l10n.store_showOnMap), + child: Text(t.store.showOnMap), onPressed: () => _showOnMap(context), ), ], diff --git a/frontend/lib/store_widgets/detail/detail_page.dart b/frontend/lib/store_widgets/detail/detail_page.dart index 07c502faf..af48af720 100644 --- a/frontend/lib/store_widgets/detail/detail_page.dart +++ b/frontend/lib/store_widgets/detail/detail_page.dart @@ -9,7 +9,7 @@ import 'package:ehrenamtskarte/widgets/top_loading_spinner.dart'; import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; -import 'package:ehrenamtskarte/util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class DetailPage extends StatelessWidget { final int _acceptingStoreId; @@ -29,15 +29,15 @@ class DetailPage extends StatelessWidget { final data = result.data; if (result.hasException && exception != null) { - return DetailErrorMessage(message: context.l10n.store_loadingDataFailed, refetch: refetch); + return DetailErrorMessage(message: t.store.loadingDataFailed, refetch: refetch); } else if (result.isNotLoading && data != null) { final matchingStores = byIdQuery.parse(data).physicalStoresByIdInProject; if (matchingStores.length != 1) { - return DetailErrorMessage(message: context.l10n.store_loadingDataFailed, refetch: refetch); + return DetailErrorMessage(message: t.store.loadingDataFailed, refetch: refetch); } final matchingStore = matchingStores.first; if (matchingStore == null) { - return DetailErrorMessage(message: context.l10n.store_acceptingStoreNotFound); + return DetailErrorMessage(message: t.store.acceptingStoreNotFound); } final categoryId = matchingStore.store.category.id; final accentColor = getDarkenedColorForCategory(context, categoryId); diff --git a/frontend/lib/util/l10n.dart b/frontend/lib/util/l10n.dart deleted file mode 100644 index e381b76e6..000000000 --- a/frontend/lib/util/l10n.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -extension L10n on BuildContext { - AppLocalizations get l10n => AppLocalizations.of(this)!; -} diff --git a/frontend/lib/widgets/app_bars.dart b/frontend/lib/widgets/app_bars.dart index 8d3cf9941..ac2cb10c1 100644 --- a/frontend/lib/widgets/app_bars.dart +++ b/frontend/lib/widgets/app_bars.dart @@ -5,7 +5,7 @@ library navigation_bars; import 'package:ehrenamtskarte/debouncer.dart'; import 'package:flutter/material.dart'; -import '../util/l10n.dart'; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; class CustomAppBar extends StatelessWidget { final String title; @@ -93,7 +93,7 @@ class SearchSliverAppBarState extends State { controller: textEditingController, focusNode: focusNode, decoration: InputDecoration.collapsed( - hintText: context.l10n.search_searchHint, + hintText: t.search.searchHint, hintStyle: TextStyle(color: foregroundColor?.withOpacity(0.8)), ), cursorColor: foregroundColor, diff --git a/frontend/pubs/df_build_config/lib/builder.dart b/frontend/pubs/df_build_config/lib/builder.dart index b24dbe3c4..98292f879 100644 --- a/frontend/pubs/df_build_config/lib/builder.dart +++ b/frontend/pubs/df_build_config/lib/builder.dart @@ -38,6 +38,8 @@ void pairToField(String k, dynamic v, StringBuffer root, StringBuffer output) { } else if (v is String) { final escaped = v.replaceAll('"', '\\"').replaceAll("\n", "\\n"); output.write(' String get $k => "$escaped";\n'); + } else if (v is String?) { + output.write(' String? get $k => null;\n'); } else if (v is bool) { output.write(' bool get $k => $v;\n'); } else if (v is double) { diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 1f2b182d7..80ba28d44 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -217,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csv: + dependency: transitive + description: + name: csv + sha256: "142bdf2b24f4a49e35a0fc4398f21d861c4c0f9015e8054dcacd0bb8e23ee27d" + url: "https://pub.dev" + source: hosted + version: "5.1.0" cupertino_icons: dependency: "direct main" description: @@ -619,6 +627,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + json2yaml: + dependency: transitive + description: + name: json2yaml + sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51 + url: "https://pub.dev" + source: hosted + version: "3.0.1" json_annotation: dependency: "direct dev" description: @@ -1132,6 +1148,30 @@ packages: description: flutter source: sdk version: "0.0.99" + slang: + dependency: "direct main" + description: + name: slang + sha256: "829ae38374a328ac8d97d5835e8b4e9bbed1993f66ca85771c5ccec9c87ac397" + url: "https://pub.dev" + source: hosted + version: "3.25.0" + slang_build_runner: + dependency: "direct dev" + description: + name: slang_build_runner + sha256: f5003a3aa8a6a72de59c8ad29c072da9ab5d1b81c599798c0f651c4e5c7e25e5 + url: "https://pub.dev" + source: hosted + version: "3.25.0" + slang_flutter: + dependency: "direct main" + description: + name: slang_flutter + sha256: cb5e1611744cca620cc03f93a54eca6918e25ae7d600cd940ef2d556e2be4c64 + url: "https://pub.dev" + source: hosted + version: "3.25.0" sliver_tools: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 1a0c8c26d..986de2d42 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -54,6 +54,8 @@ dependencies: tinycolor2: ^3.0.1 sentry_flutter: ^7.9.0 carousel_slider: ^4.2.1 + slang: ^3.25.0 + slang_flutter: ^3.25.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -67,6 +69,7 @@ dependencies: dev_dependencies: build_runner: ^2.3.3 + slang_build_runner: ^3.25.0 df_protobuf: path: ./pubs/df_protobuf df_build_config: @@ -109,6 +112,7 @@ flutter: - assets/nuernberg/body-logo.png - assets/nuernberg/background.png - assets/nuernberg/intro_slides/ + - assets/l10n/nuernberg/ # An image asset can refer to one or more resolution-specific 'variants', see # https://flutter.dev/assets-and-images/#resolution-aware. From 41010e6ad85b38f7d8b2fbb9a1493ea152d9da76 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Thu, 26 Oct 2023 15:00:54 +0200 Subject: [PATCH 46/60] 1177: Translate build config and intro slides --- frontend/assets/l10n/main/app_de.json | 35 ++++++++++++-- frontend/assets/l10n/main/app_en.json | 35 ++++++++++++-- .../assets/l10n/nuernberg/overwrite_de.json | 33 ++++++++++++- .../assets/l10n/nuernberg/overwrite_en.json | 33 ++++++++++++- frontend/build-configs/bayern/index.ts | 36 +++----------- frontend/build-configs/bayern/localization.ts | 42 ---------------- frontend/build-configs/nuernberg/index.ts | 38 +++------------ .../build-configs/nuernberg/localization.ts | 42 ---------------- frontend/build-configs/types.ts | 48 +------------------ .../activation_code_scanner_page.dart | 4 +- .../activation_overwrite_existing_dialog.dart | 4 +- .../card_detail_view/more_actions_dialog.dart | 19 ++++---- frontend/lib/identification/no_card_view.dart | 15 +++--- .../positive_verification_result_dialog.dart | 5 +- .../remove_card_confirmation_dialog.dart | 7 ++- .../dialogs/verification_info_dialog.dart | 4 +- .../verification_qr_scanner_page.dart | 3 +- frontend/lib/intro_slides/intro_screen.dart | 24 +++++----- frontend/lib/main.dart | 4 +- 19 files changed, 178 insertions(+), 253 deletions(-) delete mode 100644 frontend/build-configs/bayern/localization.ts delete mode 100644 frontend/build-configs/nuernberg/localization.ts diff --git a/frontend/assets/l10n/main/app_de.json b/frontend/assets/l10n/main/app_de.json index e95d4d3cf..f620b89cf 100644 --- a/frontend/assets/l10n/main/app_de.json +++ b/frontend/assets/l10n/main/app_de.json @@ -65,8 +65,12 @@ }, "identification": { "activate": "Aktivieren", - "activateCardCurrentDevice": "Karte auf diesem Gerät aktivieren?", - "activateCardCurrentDeviceRationale": "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", + "activateCurrentDeviceDescription": "Ihre Karte ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihre Karte auf diesem Gerät aktivieren, wird sie auf Ihrem anderen Gerät automatisch deaktiviert.", + "activateCurrentDeviceTitle": "Karte auf diesem Gerät aktivieren?", + "activateDescription": "Sie haben die Ehrenamtskarte bereits beantragt und den Aktivierungscode Ihrer digitalen Ehrenamtskarte erhalten? Scannen Sie den Code hier ein.", + "activateTitle": "Karte aktivieren", + "applyDescription": "Sie sind ehrenamtlich engagiert, haben aber noch keine Ehrenamtskarte? Hier können Sie Ihre Ehrenamtskarte beantragen.", + "applyTitle": "Beantragen", "authenticationPossible": "Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:", "cameraAccessRequired": "Zugriff auf Kamera erforderlich", "cameraAccessRequiredSettings": "Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera.\nIn den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.", @@ -90,7 +94,18 @@ "flashOff": "Blitz aus", "flashOn": "Blitz an", "internetRequired": "Eine Internetverbindung wird benötigt.", + "moreActionsActivateDescription": "Ihre hinterlegte Ehrenamtskarte bleibt erhalten. Sie können diese manuell entfernen.", + "moreActionsActivateLimitDescription": "Um eine weitere Ehrenamtskarte hinzuzufügen, müssen Sie zuerst eine vorhandene Ehrenamtskarte löschen.", + "moreActionsActivateTitle": "Weitere Ehrenamtskarte hinzufügen", + "moreActionsApplyDescription": "Ihre hinterlegte Karte bleibt erhalten.", + "moreActionsApplyTitle": "Ehrenamtskarte beantragen oder verlängern", + "moreActionsRemoveDescription": "Nach der Auswahl wird diese Ehrenamtskarte vom Gerät gelöscht.", + "moreActionsRemoveTitle": "Diese Ehrenamtskarte löschen", + "moreActionsVerifyDescription": "Prüfen Sie die Gültigkeit einer digitalen Ehrenamtskarte.", + "moreActionsVerifyTitle": "Eine digitale Ehrenamtskarte prüfen", "notVerified": "Nicht verifiziert", + "removeDescription": "Wenn diese Karte gelöscht wird, muss diese für eine erneute Verwendung neu hinzugefügt werden.", + "removeTitle": "Diese Karte löschen?", "scanCode": "Scannen Sie den QR-Code, der auf dem \"Ausweisen\"-Tab Ihres Gegenübers angezeigt wird.", "scanQRCode": "Halten Sie die Kamera auf den QR Code.", "scanningFailed": "Fehler beim Lesen des Codes", @@ -101,7 +116,21 @@ "title": "Ausweisen", "unlimited": "unbegrenzt", "validFromUntil": "Gültig: {{startDate}} bis {{expirationDate}}", - "validUntil": "Gültig bis: {{expirationDate}}" + "validUntil": "Gültig bis: {{expirationDate}}", + "verificationSuccessful": "Karte ist gültig", + "verifyDescription": "Sie möchten die Gültigkeit einer digitalen Ehrenamtskarte prüfen? Scannen Sie den Code hier ein.", + "verifyInfoTitle": "So prüfen Sie die Gültigkeit einer Ehrenamtskarte", + "verifyTitle": "Gültigkeit prüfen" + }, + "intro": { + "applyDescription": "Im Formular geben Sie Informationen über sich und Ihre ehrenamtliche Tätigkeit an.\nAnschließend wird der Antrag weitergeleitet und von der zuständigen Stelle bearbeitet.", + "applyTitle": "Wie kann ich die Ehrenamtskarte beantragen?", + "locationDescription": "Wir können Ihren Standort auf der Karte anzeigen und Akzeptanzstellen in Ihrer Umgebung anzeigen.\nWenn Sie diese Hilfen nutzen möchten, benötigen wir Ihre Zustimmung.\nIhr Standort wird nicht gespeichert.", + "locationTitle": "Finden Sie Akzeptanzstellen in Ihrer Umgebung!", + "usageDescription": "Auf der Karte von Bayern können Sie alle Akzeptanzstellen finden.\nTippen Sie auf einen Standort, um mehr Informationen sehen zu können.", + "usageTitle": "Wo kann ich meine Ehrenamtskarte nutzen?", + "welcomeDescription": "Vielen Dank, dass Sie sich die App zur Bayerischen Ehrenamtskarte heruntergeladen haben!", + "welcomeTitle": "Willkommen!" }, "location": { "activateLocationAccess": "Standortermittlung aktivieren", diff --git a/frontend/assets/l10n/main/app_en.json b/frontend/assets/l10n/main/app_en.json index 33cbaf4fe..60e138bae 100644 --- a/frontend/assets/l10n/main/app_en.json +++ b/frontend/assets/l10n/main/app_en.json @@ -65,8 +65,12 @@ }, "identification": { "activate": "Activate", - "activateCardCurrentDevice": "Activate card on this device?", - "activateCardCurrentDeviceRationale": "Your card is already activated on another device. If you activate your card on this device, it will be automatically deactivated on your other device.", + "activateCurrentDeviceDescription": "Your card is already activated on another device. If you activate your card on this device, it will be automatically deactivated on your other device.", + "activateCurrentDeviceTitle": "Activate card on this device?", + "activateDescription": "You have already applied for the Ehrenamtskarte and received the activation code for your digital Ehrenamtskarte? Scan the code here.", + "activateTitle": "Activate card", + "applyDescription": "Are you involved in voluntary work but do not yet have a Ehrenamtskarte? You can apply for your Ehrenamtskarte here.", + "applyTitle": "Apply", "authenticationPossible": "You can use this QR code to identify yourself at acceptance points:", "cameraAccessRequired": "Access to camera required", "cameraAccessRequiredSettings": "To scan a QR code, the app needs access to the camera.\nYou can allow the app to access the camera in the settings.", @@ -90,7 +94,18 @@ "flashOff": "Flash off", "flashOn": "Flash on", "internetRequired": "An internet connection is required.", + "moreActionsActivateDescription": "Your saved Ehrenamtskarte will be retained. You can remove it manually.", + "moreActionsActivateLimitDescription": "To add another Ehrenamtskarte, you must first delete an existing Ehrenamtskarte.", + "moreActionsActivateTitle": "Add another Ehrenamtskarte", + "moreActionsApplyDescription": "Your saved card will be retained.", + "moreActionsApplyTitle": "Apply for or extend a Ehrenamtskarte", + "moreActionsRemoveDescription": "After selection, this Ehrenamtskarte is deleted from the device.", + "moreActionsRemoveTitle": "Delete this Ehrenamtskarte?", + "moreActionsVerifyDescription": "Check the validity of a Ehrenamtskarte", + "moreActionsVerifyTitle": "Check a Ehrenamtskarte", "notVerified": "Not verified", + "removeDescription": "If this card is deleted, it must be added again before using it again.", + "removeTitle": "Delete this card?", "scanCode": "Scan the QR code that appears on the \"Identify\" tab of the other party.", "scanQRCode": "Point the camera at the QR code.", "scanningFailed": "Error reading the code", @@ -101,7 +116,21 @@ "title": "Identify", "unlimited": "unlimited", "validFromUntil": "Valid: {{startDate}} until {{expirationDate}}", - "validUntil": "Valid until: {{expirationDate}}" + "validUntil": "Valid until: {{expirationDate}}", + "verificationSuccessful": "Card is valid", + "verifyDescription": "You would like to check the validity of a digital Ehrenamtskarte? Scan the code here.", + "verifyInfoTitle": "How to check the validity of a Ehrenamtskarte", + "verifyTitle": "Check validity" + }, + "intro": { + "applyDescription": "Provide information about yourself and your volunteer activity in the form.\nThe application is then forwarded and processed by the responsible office.", + "applyTitle": "How can I apply for the Ehrenamtskarte?", + "locationDescription": "We can show your location on the map and display acceptance points in your area.\nIf you want to use these aids, we need your consent.\nYour location is not stored.", + "locationTitle": "Find acceptance points in your area!", + "usageDescription": "On the map of Bavaria you can find all acceptance points.\nTap on a location to be able to see more information.", + "usageTitle": "Where can I use my Ehrenamtskarte?", + "welcomeDescription": "Thank you for downloading the app for the Bayerische Ehrenamtskarte!", + "welcomeTitle": "Welcome!" }, "location": { "activateLocationAccess": "Activate location access", diff --git a/frontend/assets/l10n/nuernberg/overwrite_de.json b/frontend/assets/l10n/nuernberg/overwrite_de.json index 6a50940ba..31b0a25be 100644 --- a/frontend/assets/l10n/nuernberg/overwrite_de.json +++ b/frontend/assets/l10n/nuernberg/overwrite_de.json @@ -1,5 +1,34 @@ { - "about": { - "title": "Nürnberg" + "identification": { + "activateCurrentDeviceDescription": "Ihr Pass ist bereits auf einem anderen Gerät aktiviert. Wenn Sie Ihren Pass auf diesem Gerät aktivieren, wird er auf Ihrem anderen Gerät automatisch deaktiviert.", + "activateCurrentDeviceTitle": "Pass auf diesem Gerät aktivieren?", + "activateDescription": "Sie haben den Nürnberg-Pass bereits beantragt und einen Aktivierungscode erhalten? Scannen Sie den Code hier ein.", + "activateTitle": "Pass aktivieren", + "applyDescription": "Sie haben noch keinen Nürnberg-Pass? Hier können Sie Ihren Nürnberg-Pass beantragen.", + "cardExpired": "Ihr Pass ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", + "cardInvalid": "Ihr Pass ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", + "cardNotYetValid": "Der Gültigkeitszeitraum Ihres Passes hat noch nicht begonnen.", + "checkFailed": "Ihr Pass konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Internetverbindung besteht und prüfen Sie erneut.", + "moreActionsActivateDescription": "Ihr hinterlegter Nürnberg-Pass bleibt erhalten. Sie können diesen manuell entfernen.", + "moreActionsActivateLimitDescription": "Um einen weiteren Nürnberg-Pass hinzuzufügen, müssen Sie zuerst einen vorhandenen Nürnberg-Pass löschen.", + "moreActionsActivateTitle": "Weiteren Nürnberg-Pass hinzufügen", + "moreActionsApplyDescription": "Ihr hinterlegter Pass bleibt erhalten.", + "moreActionsApplyTitle": "Nürnberg-Pass beantragen oder verlängern", + "moreActionsRemoveDescription": "Nach der Auswahl wird dieser Nürnberg-Pass vom Gerät gelöscht.", + "moreActionsRemoveTitle": "Diesen Nürnberg-Pass löschen", + "moreActionsVerifyDescription": "Prüfen Sie die Gültigkeit eines Nürnberg-Passes.", + "moreActionsVerifyTitle": "Einen Nürnberg-Pass prüfen", + "removeDescription": "Wenn dieser Pass gelöscht wird, muss dieser für eine erneute Verwendung neu hinzugefügt werden.", + "removeTitle": "Diesen Pass löschen?", + "verificationSuccessful": "Pass ist gültig", + "verifyDescription": "Sie möchten die Gültigkeit eines Nürnberg-Passes prüfen? Scannen Sie den Code hier ein.", + "verifyInfoTitle": "So prüfen Sie die Gültigkeit eines Nürnberg-Passes" + }, + "intro": { + "applyDescription": "Im Formular geben Sie Ihre persönlichen Informationen an.\nAnschließend wird der Antrag weitergeleitet und von der zuständigen Stelle bearbeitet.", + "applyTitle": "Wie kann ich den Nürnberg-Pass beantragen?", + "usageDescription": "Auf der Karte von Nürnberg können Sie alle Akzeptanzstellen finden.\nTippen Sie auf einen Standort, um mehr Informationen sehen zu können.", + "usageTitle": "Wo kann ich den Nürnberg-Pass nutzen?", + "welcomeDescription": "Vielen Dank, dass Sie sich die App zum Nürnberg-Pass heruntergeladen haben!" } } diff --git a/frontend/assets/l10n/nuernberg/overwrite_en.json b/frontend/assets/l10n/nuernberg/overwrite_en.json index 6a50940ba..70589e29b 100644 --- a/frontend/assets/l10n/nuernberg/overwrite_en.json +++ b/frontend/assets/l10n/nuernberg/overwrite_en.json @@ -1,5 +1,34 @@ { - "about": { - "title": "Nürnberg" + "identification": { + "activateCurrentDeviceDescription": "Your pass is already activated on another device. If you activate your pass on this device, it will be automatically deactivated on your other device.", + "activateCurrentDeviceTitle": "Activate pass on this device?", + "activateDescription": "You have already applied for the Nürnberg-Pass and received an activation code? Scan the code here.", + "activateTitle": "Activate pass", + "applyDescription": "You don't have a Nürnberg-Pass yet? Here you can apply for your Nürnberg-Pass.", + "cardExpired": "Your pass has expired.\nYou can apply for renewal under \"More actions\"", + "cardInvalid": "Your pass is invalid.\nIt has either been revoked or activated on another device.", + "cardNotYetValid": "The validity period of your pass has not started yet.", + "checkFailed": "Your pass could not be verified. Please make sure you have an internet connection and try again.", + "moreActionsActivateDescription": "Your saved Nürnberg-Pass will be retained. You can remove it manually.", + "moreActionsActivateLimitDescription": "To add another Nürnberg-Pass, you must first delete an existing Nürnberg-Pass.", + "moreActionsActivateTitle": "Add another Nürnberg-Pass", + "moreActionsApplyDescription": "Your saved pass will be retained.", + "moreActionsApplyTitle": "Apply for or extend a Nürnberg-Pass", + "moreActionsRemoveDescription": "After selection, this Nürnberg-Pass is deleted from the device.", + "moreActionsRemoveTitle": "Delete this Nürnberg-Pass?", + "moreActionsVerifyDescription": "Check the validity of a Nürnberg-Pass", + "moreActionsVerifyTitle": "Check a Nürnberg-Pass", + "removeDescription": "If this pass is deleted, it must be added again before using it again.", + "removeTitle": "Delete this pass?", + "verificationSuccessful": "Pass is valid", + "verifyDescription": "You would like to check the validity of a Nürnberg-Pass? Scan the code here.", + "verifyInfoTitle": "How to check the validity of a Nürnberg-Pass" + }, + "intro": { + "applyDescription": "Provide your personal information in the form.\nThe application is then forwarded and processed by the responsible office.", + "applyTitle": "How can I apply for the Nürnberg-Pass?", + "usageDescription": "On the map of Nürnberg you can find all acceptance points.\nTap on a location to be able to see more information.", + "usageTitle": "Where can I use my Nürnberg-Pass?", + "welcomeDescription": "Thank you for downloading the app for the Nürnberg-Pass!" } } diff --git a/frontend/build-configs/bayern/index.ts b/frontend/build-configs/bayern/index.ts index dcf2c4d52..f8bd1f41c 100644 --- a/frontend/build-configs/bayern/index.ts +++ b/frontend/build-configs/bayern/index.ts @@ -1,7 +1,6 @@ import BuildConfigType, {CommonBuildConfigType} from "../types" import publisherText from "./publisherText" import disclaimerText from "./disclaimerText" -import localization from "./localization" export const bayernCommon: CommonBuildConfigType = { appName: "Ehrenamt", @@ -56,34 +55,12 @@ export const bayernCommon: CommonBuildConfigType = { boxDecorationRadius: 1, }, iconInAboutTab: "assets/bayern/icon.png", - introSlide1: { - title: "Willkommen!", - description: "Vielen Dank, dass Sie sich die App zur " + - "Bayerischen Ehrenamtskarte heruntergeladen haben!", - imagePath: "assets/bayern/icon.png" - }, - introSlide2: { - title: "Wie kann ich die Ehrenamtskarte beantragen?", - description: "Im Formular geben Sie Informationen über sich und Ihre " + - "ehrenamtliche Tätigkeit an.\nAnschließend wird " + - "der Antrag weitergeleitet und von der zuständigen Stelle bearbeitet.", - imagePath: "assets/bayern/intro_slides/apply_for_eak.png" - }, - introSlide3: { - title: "Wo kann ich meine Ehrenamtskarte nutzen?", - description: "Auf der Karte von Bayern können Sie alle Akzeptanzstellen" + - " finden.\nTippen Sie auf einen Standort, um mehr Informationen " + - "sehen zu können.", - imagePath: "assets/bayern/intro_slides/map_zoom.jpeg" - }, - introSlide4: { - title: "Finden Sie Akzeptanzstellen in Ihrer Umgebung!", - description: "Wir können Ihren Standort auf der Karte anzeigen" + - " und Akzeptanzstellen in Ihrer Umgebung anzeigen.\n" + - "Wenn Sie diese Hilfen nutzen möchten, benötigen wir Ihre " + - "Zustimmung.\nIhr Standort wird nicht gespeichert.", - imagePath: "assets/bayern/intro_slides/search_with_location.png" - }, + introSlidesImages: [ + "assets/bayern/icon.png", + "assets/bayern/intro_slides/apply_for_eak.png", + "assets/bayern/intro_slides/map_zoom.jpeg", + "assets/bayern/intro_slides/search_with_location.png", + ], featureFlags: { verification: true }, @@ -93,7 +70,6 @@ export const bayernCommon: CommonBuildConfigType = { "Bayerisches Staatsministerium\nfür Familie, Arbeit und Soziales\nWinzererstraße 9\n80797 München", publisherText, disclaimerText, - localization, maxCardAmount: 1 } diff --git a/frontend/build-configs/bayern/localization.ts b/frontend/build-configs/bayern/localization.ts deleted file mode 100644 index 888a70ed1..000000000 --- a/frontend/build-configs/bayern/localization.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { LocalizationType } from "../types" - -const localization: LocalizationType = { - identification: { - noCardView: { - applyTitle: "Beantragen", - applyDescription: - "Sie sind ehrenamtlich engagiert, haben aber noch keine Ehrenamtskarte? Hier können Sie Ihre Ehrenamtskarte beantragen.", - activateTitle: "Karte aktivieren", - activateDescription: - "Sie haben die Ehrenamtskarte bereits beantragt und den Aktivierungscode Ihrer digitalen Ehrenamtskarte erhalten? Scannen Sie den Code hier ein.", - verifyTitle: "Gültigkeit prüfen", - verifyDescription: - "Sie möchten die Gültigkeit einer digitalen Ehrenamtskarte prüfen? Scannen Sie den Code hier ein.", - }, - activationCodeScanner: { - title: "Karte aktivieren", - }, - verificationCodeScanner: { - title: "Karte verifizieren", - infoDialogTitle: "So prüfen Sie die Gültigkeit einer Ehrenamtskarte", - positiveVerificationDialogTitle: "Karte ist gültig", - }, - moreActions: { - applyForAnotherCardTitle: "Ehrenamtskarte beantragen oder verlängern", - applyForAnotherCardDescription: "Ihre hinterlegte Karte bleibt erhalten.", - activateAnotherCardTitle: "Weitere Ehrenamtskarte hinzufügen", - activateAnotherCardDescription: "Ihre hinterlegte Ehrenamtskarte bleibt erhalten. Sie können diese manuell entfernen.", - activationLimitDescription: "Um eine weitere Ehrenamtskarte hinzuzufügen, müssen Sie zuerst eine vorhandene Ehrenamtskarte löschen.", - verifyTitle: "Eine digitale Ehrenamtskarte prüfen", - verifyDescription: "Prüfen Sie die Gültigkeit einer digitalen Ehrenamtskarte.", - removeCardTitle: "Diese Ehrenamtskarte löschen", - removeCardDescription: "Nach der Auswahl wird diese Ehrenamtskarte vom Gerät gelöscht.", - }, - removeCardDialog: { - title: "Diese Karte löschen?", - description: "Wenn diese Karte gelöscht wird, muss diese für eine erneute Verwendung neu hinzugefügt werden.", - } - }, -} - -export default localization diff --git a/frontend/build-configs/nuernberg/index.ts b/frontend/build-configs/nuernberg/index.ts index 5a865a0ac..a9ae629ff 100644 --- a/frontend/build-configs/nuernberg/index.ts +++ b/frontend/build-configs/nuernberg/index.ts @@ -1,7 +1,6 @@ import BuildConfigType, {CommonBuildConfigType} from "../types" import publisherText from "./publisherText" import disclaimerText from "./disclaimerText" -import localization from "./localization" export const nuernbergCommon: CommonBuildConfigType = { appName: "Nürnberg-Pass", @@ -56,36 +55,12 @@ export const nuernbergCommon: CommonBuildConfigType = { boxDecorationRadius: 0, }, iconInAboutTab: "assets/nuernberg/body-logo.png", - introSlide1: { - title: "Willkommen!", - description: "Vielen Dank, dass Sie sich die App zum Nürnberg-Pass heruntergeladen haben!", - imagePath: "assets/nuernberg/body-logo.png", - }, - introSlide2: { - title: "Wie kann ich den Nürnberg-Pass beantragen?", - description: - "Im Formular geben Sie Ihre " + - "persönlichen Informationen an. Anschließend wird " + - "der Antrag weitergeleitet und von der zuständigen Stelle bearbeitet.", - imagePath: "assets/nuernberg/intro_slides/apply_for_sozialpass.png", - }, - introSlide3: { - title: "Wo kann ich den Nürnberg-Pass nutzen?", - description: - "Auf der Karte von Nürnberg können Sie alle Akzeptanzstellen" + - " finden. Tippen Sie auf einen Standort, um mehr Informationen " + - "sehen zu können.", - imagePath: "assets/nuernberg/intro_slides/map_zoom.png", - }, - introSlide4: { - title: "Finden Sie Akzeptanzstellen in Ihrer Umgebung!", - description: - "Wir können Ihren Standort auf der Karte anzeigen" + - " und Akzeptanzstellen in Ihrer Umgebung anzeigen. " + - "Wenn Sie diese Hilfen nutzen möchten, benötigen wir Ihre " + - "Zustimmung. Ihr Standort wird nicht gespeichert.", - imagePath: "assets/nuernberg/intro_slides/search_with_location.png", - }, + introSlidesImages: [ + "assets/nuernberg/body-logo.png", + "assets/nuernberg/intro_slides/apply_for_sozialpass.png", + "assets/nuernberg/intro_slides/map_zoom.png", + "assets/nuernberg/intro_slides/search_with_location.png", + ], featureFlags: { verification: true }, @@ -95,7 +70,6 @@ export const nuernbergCommon: CommonBuildConfigType = { dataPrivacyPolicyUrl: "https://nuernberg.sozialpass.app/data-privacy-policy", publisherText, disclaimerText, - localization, maxCardAmount: 5, } diff --git a/frontend/build-configs/nuernberg/localization.ts b/frontend/build-configs/nuernberg/localization.ts deleted file mode 100644 index 3d5573b64..000000000 --- a/frontend/build-configs/nuernberg/localization.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { LocalizationType } from "../types" - -const localization: LocalizationType = { - identification: { - noCardView: { - applyTitle: "Beantragen", - applyDescription: - "Sie haben noch keinen Nürnberg-Pass? Hier können Sie Ihren Nürnberg-Pass beantragen.", - activateTitle: "Pass aktivieren", - activateDescription: - "Sie haben den Nürnberg-Pass bereits beantragt und einen Aktivierungscode erhalten? Scannen Sie den Code hier ein.", - verifyTitle: "Gültigkeit prüfen", - verifyDescription: - "Sie möchten die Gültigkeit eines Nürnberg-Passes prüfen? Scannen Sie den Code hier ein.", - }, - activationCodeScanner: { - title: "Pass aktivieren", - }, - verificationCodeScanner: { - title: "Pass verifizieren", - infoDialogTitle: "So prüfen Sie die Gültigkeit eines Nürnberg-Passes", - positiveVerificationDialogTitle: "Pass ist gültig", - }, - moreActions: { - applyForAnotherCardTitle: "Nürnberg-Pass beantragen oder verlängern", - applyForAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten.", - activateAnotherCardTitle: "Weiteren Nürnberg-Pass hinzufügen", - activateAnotherCardDescription: "Ihr hinterlegter Pass bleibt erhalten. Sie können diesen manuell entfernen.", - activationLimitDescription: "Um einen weiteren Pass hinzuzufügen, müssen Sie zuerst einen der vorhandenen Pässe löschen.", - verifyTitle: "Einen Nürnberg-Pass prüfen", - verifyDescription: "Prüfen Sie die Gültigkeit eines Nürnberg-Passes.", - removeCardTitle: "Diesen Nürnberg-Pass löschen", - removeCardDescription: "Nach der Auswahl wird der hinterlegte Pass vom Gerät gelöscht.", - }, - removeCardDialog: { - title: "Diesen Pass löschen?", - description: "Wenn dieser Pass gelöscht wird, muss dieser für eine erneute Verwendung neu hinzugefügt werden.", - } - }, -} - -export default localization diff --git a/frontend/build-configs/types.ts b/frontend/build-configs/types.ts index 13d07865f..7403f6db7 100644 --- a/frontend/build-configs/types.ts +++ b/frontend/build-configs/types.ts @@ -13,48 +13,6 @@ export type ThemeType = { primaryDark: string } -type SlideType = { - title: string - description: string - imagePath: string -} - -export type LocalizationType = { - identification: { - noCardView: { - applyTitle: string - applyDescription: string - activateTitle: string - activateDescription: string - verifyTitle: string - verifyDescription: string - } - activationCodeScanner: { - title: string - } - verificationCodeScanner: { - title: string - infoDialogTitle: string - positiveVerificationDialogTitle: string - } - moreActions: { - applyForAnotherCardTitle: string - applyForAnotherCardDescription: string - activateAnotherCardTitle: string - activateAnotherCardDescription: string - activationLimitDescription: string - verifyTitle: string - verifyDescription: string - removeCardTitle: string - removeCardDescription: string - } - removeCardDialog: { - title: string - description: string - } - } -} - export type CommonBuildConfigType = { appName: string appIcon: string @@ -113,10 +71,7 @@ export type CommonBuildConfigType = { boxDecorationRadius: number } iconInAboutTab: string - introSlide1: SlideType, - introSlide2: SlideType, - introSlide3: SlideType, - introSlide4: SlideType + introSlidesImages: [string, string, string, string], theme: ThemeType categories: number[] featureFlags: FeatureFlagsType @@ -125,7 +80,6 @@ export type CommonBuildConfigType = { publisherAddress: string publisherText: string disclaimerText: string - localization: LocalizationType maxCardAmount: number } diff --git a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart index 373959f8c..b6543cf63 100644 --- a/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart +++ b/frontend/lib/identification/activation_workflow/activation_code_scanner_page.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/configuration/configuration.dart'; import 'package:ehrenamtskarte/graphql/graphql_api.graphql.dart'; import 'package:ehrenamtskarte/identification/activation_workflow/activate_code.dart'; @@ -33,10 +32,9 @@ class ActivationCodeScannerPage extends StatelessWidget { @override Widget build(BuildContext context) { - final localization = buildConfig.localization.identification.activationCodeScanner; return Column( children: [ - CustomAppBar(title: localization.title), + CustomAppBar(title: t.identification.activateTitle), Expanded( child: QrCodeScannerPage( onCodeScanned: (code) async => _onCodeScanned(context, code), diff --git a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart index 3eabcce7e..d40be1428 100644 --- a/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart +++ b/frontend/lib/identification/activation_workflow/activation_overwrite_existing_dialog.dart @@ -8,11 +8,11 @@ class ActivationOverwriteExistingDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(t.identification.activateCardCurrentDevice, style: TextStyle(fontSize: 18)), + title: Text(t.identification.activateCurrentDeviceTitle, style: TextStyle(fontSize: 18)), content: SingleChildScrollView( child: ListBody( children: [ - Text(t.identification.activateCardCurrentDeviceRationale), + Text(t.identification.activateCurrentDeviceDescription), ], ), ), diff --git a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart index eaa0d6171..092067574 100644 --- a/frontend/lib/identification/card_detail_view/more_actions_dialog.dart +++ b/frontend/lib/identification/card_detail_view/more_actions_dialog.dart @@ -21,7 +21,6 @@ class MoreActionsDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final localization = buildConfig.localization.identification.moreActions; final userCodeModel = Provider.of(context, listen: false); final String cardsInUse = userCodeModel.userCodes.length.toString(); final String maxCardAmount = buildConfig.maxCardAmount.toString(); @@ -35,8 +34,8 @@ class MoreActionsDialog extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: Text(localization.applyForAnotherCardTitle), - subtitle: Text(localization.applyForAnotherCardDescription), + title: Text(t.identification.moreActionsApplyTitle), + subtitle: Text(t.identification.moreActionsApplyDescription), leading: const Icon(Icons.assignment, size: 36), onTap: () { Navigator.pop(context); @@ -44,8 +43,8 @@ class MoreActionsDialog extends StatelessWidget { }, ), ListTile( - title: Text(localization.verifyTitle), - subtitle: Text(localization.verifyDescription), + title: Text(t.identification.moreActionsVerifyTitle), + subtitle: Text(t.identification.moreActionsVerifyDescription), leading: const Icon(Icons.verified, size: 36), onTap: () { Navigator.pop(context); @@ -54,11 +53,11 @@ class MoreActionsDialog extends StatelessWidget { ), ListTile( enabled: !cardLimitIsReached, - title: Text('${localization.activateAnotherCardTitle} ($cardsInUse/$maxCardAmount)', + title: Text('${t.identification.moreActionsActivateTitle} ($cardsInUse/$maxCardAmount)', style: TextStyle(color: Theme.of(context).colorScheme.onBackground)), subtitle: Text(cardLimitIsReached - ? localization.activationLimitDescription - : localization.activateAnotherCardDescription), + ? t.identification.moreActionsActivateLimitDescription + : t.identification.moreActionsActivateDescription), leading: Icon(Icons.add_card, size: 36), onTap: () { Navigator.pop(context); @@ -66,8 +65,8 @@ class MoreActionsDialog extends StatelessWidget { }, ), ListTile( - title: Text(localization.removeCardTitle), - subtitle: Text(localization.removeCardDescription), + title: Text(t.identification.moreActionsRemoveTitle), + subtitle: Text(t.identification.moreActionsRemoveDescription), leading: const Icon(Icons.delete, size: 36), onTap: () { Navigator.pop(context); diff --git a/frontend/lib/identification/no_card_view.dart b/frontend/lib/identification/no_card_view.dart index 056f26b7c..b5fcc46e9 100644 --- a/frontend/lib/identification/no_card_view.dart +++ b/frontend/lib/identification/no_card_view.dart @@ -1,4 +1,4 @@ -import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; import 'package:flutter/material.dart'; class NoCardView extends StatelessWidget { @@ -15,7 +15,6 @@ class NoCardView extends StatelessWidget { @override Widget build(BuildContext context) { - final localization = buildConfig.localization.identification.noCardView; return LayoutBuilder( builder: (BuildContext context, BoxConstraints viewportConstraints) => SingleChildScrollView( child: ConstrainedBox( @@ -27,20 +26,20 @@ class NoCardView extends StatelessWidget { children: [ _TapableCardWithArea( onTap: startApplication, - title: localization.applyTitle, - description: localization.applyDescription, + title: t.identification.applyTitle, + description: t.identification.applyDescription, icon: Icons.assignment, ), _TapableCardWithArea( onTap: startActivation, - title: localization.activateTitle, - description: localization.activateDescription, + title: t.identification.activateTitle, + description: t.identification.activateDescription, icon: Icons.add_card, ), _TapableCardWithArea( onTap: startVerification, - title: localization.verifyTitle, - description: localization.verifyDescription, + title: t.identification.verifyTitle, + description: t.identification.verifyDescription, icon: Icons.verified, ), ].wrapWithSpacers(height: 24), diff --git a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart index 7991168a3..d7daae30c 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/positive_verification_result_dialog.dart @@ -1,4 +1,3 @@ -import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/configuration/configuration.dart'; import 'package:ehrenamtskarte/graphql/graphql_api.dart'; import 'package:ehrenamtskarte/identification/id_card/id_card.dart'; @@ -41,7 +40,6 @@ class PositiveVerificationResultDialogState extends State { @override Widget build(BuildContext context) { - final localization = buildConfig.localization.identification.removeCardDialog; final projectId = Configuration.of(context).projectId; final regionsQuery = GetRegionsByIdQuery( variables: GetRegionsByIdArguments( @@ -52,14 +51,14 @@ class RemoveCardConfirmationDialogState extends State[ Padding( padding: EdgeInsets.only(bottom: 20), - child: Text(localization.description, style: TextStyle(fontSize: 14))), + child: Text(t.identification.removeDescription, style: TextStyle(fontSize: 14))), IdCard( cardInfo: widget.userCode.info, region: region != null ? Region(region.prefix, region.name) : null, diff --git a/frontend/lib/identification/verification_workflow/dialogs/verification_info_dialog.dart b/frontend/lib/identification/verification_workflow/dialogs/verification_info_dialog.dart index bacb1cd48..19e83c7ab 100644 --- a/frontend/lib/identification/verification_workflow/dialogs/verification_info_dialog.dart +++ b/frontend/lib/identification/verification_workflow/dialogs/verification_info_dialog.dart @@ -1,4 +1,3 @@ -import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -11,9 +10,8 @@ class VerificationInfoDialog extends StatelessWidget { @override Widget build(BuildContext context) { final settings = Provider.of(context); - final localization = buildConfig.localization.identification.verificationCodeScanner; return AlertDialog( - title: Text(localization.infoDialogTitle), + title: Text(t.identification.verifyInfoTitle), content: SingleChildScrollView( child: ListBody( children: [ diff --git a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart index 62d8ca0b4..3332d7532 100644 --- a/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart +++ b/frontend/lib/identification/verification_workflow/verification_qr_scanner_page.dart @@ -1,6 +1,5 @@ import 'dart:typed_data'; -import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/configuration/configuration.dart'; import 'package:ehrenamtskarte/configuration/settings_model.dart'; import 'package:ehrenamtskarte/identification/connection_failed_dialog.dart'; @@ -33,7 +32,7 @@ class VerificationQrScannerPage extends StatelessWidget { return Column( children: [ CustomAppBar( - title: buildConfig.localization.identification.verificationCodeScanner.title, + title: t.identification.verifyTitle, actions: [ IconButton( icon: const Icon(Icons.help), diff --git a/frontend/lib/intro_slides/intro_screen.dart b/frontend/lib/intro_slides/intro_screen.dart index 284ba989a..cb23cae0c 100644 --- a/frontend/lib/intro_slides/intro_screen.dart +++ b/frontend/lib/intro_slides/intro_screen.dart @@ -34,9 +34,9 @@ class IntroScreen extends StatelessWidget { isShowSkipBtn: false, listContentConfig: [ ContentConfig( - title: buildConfig.introSlide1.title, - description: buildConfig.introSlide1.description, - pathImage: buildConfig.introSlide1.imagePath, + title: t.intro.welcomeTitle, + description: t.intro.welcomeDescription, + pathImage: buildConfig.introSlidesImages[0], backgroundColor: theme.brightness == Brightness.light ? const Color(0xffECECEC) : theme.colorScheme.background, maxLineTitle: 3, @@ -44,9 +44,9 @@ class IntroScreen extends StatelessWidget { styleDescription: theme.textTheme.bodyLarge?.apply(fontSizeFactor: 1.2), ), ContentConfig( - title: buildConfig.introSlide2.title, - description: buildConfig.introSlide2.description, - pathImage: buildConfig.introSlide2.imagePath, + title: t.intro.applyTitle, + description: t.intro.applyDescription, + pathImage: buildConfig.introSlidesImages[1], backgroundColor: theme.brightness == Brightness.light ? const Color(0xffECECEC) : theme.colorScheme.background, maxLineTitle: 3, @@ -54,9 +54,9 @@ class IntroScreen extends StatelessWidget { styleDescription: theme.textTheme.bodyLarge?.apply(fontSizeFactor: 1.2), ), ContentConfig( - title: buildConfig.introSlide3.title, - description: buildConfig.introSlide3.description, - pathImage: buildConfig.introSlide3.imagePath, + title: t.intro.usageTitle, + description: t.intro.usageDescription, + pathImage: buildConfig.introSlidesImages[2], backgroundColor: theme.brightness == Brightness.light ? const Color(0xffECECEC) : theme.colorScheme.background, maxLineTitle: 3, @@ -64,17 +64,17 @@ class IntroScreen extends StatelessWidget { styleDescription: theme.textTheme.bodyLarge?.apply(fontSizeFactor: 1.2), ), ContentConfig( - title: buildConfig.introSlide4.title, + title: t.intro.locationTitle, backgroundColor: theme.brightness == Brightness.light ? const Color(0xffECECEC) : theme.colorScheme.background, maxLineTitle: 3, styleTitle: theme.textTheme.headlineSmall, - pathImage: buildConfig.introSlide4.imagePath, + pathImage: buildConfig.introSlidesImages[3], widgetDescription: Center( child: Column( children: [ Text( - buildConfig.introSlide4.description, + t.intro.locationDescription, style: theme.textTheme.bodyLarge?.apply(fontSizeFactor: 1.2), textAlign: TextAlign.center, maxLines: 100, diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index dbbaf25c6..3ce466071 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -8,6 +8,7 @@ import 'package:ehrenamtskarte/sentry.dart'; import 'package:ehrenamtskarte/settings_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:slang/builder/model/enums.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -26,8 +27,7 @@ Future main() async { void override(AppLocale locale) async { final localeOverwritePath = '${buildConfig.localeOverwritePath}/overwrite_${locale.languageCode}.json'; String overrideLocales = await rootBundle.loadString(localeOverwritePath); - // TODO uncomment in #1177 - // LocaleSettings.overrideTranslations(locale: locale, fileType: FileType.json, content: overrideLocales); + LocaleSettings.overrideTranslations(locale: locale, fileType: FileType.json, content: overrideLocales); } AppLocale.values.forEach(override); From 8921cbf19f62880d8a27dfcd5cdda8db0a04b772 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Thu, 26 Oct 2023 15:29:09 +0200 Subject: [PATCH 47/60] 1177: Remove escaped chars --- frontend/assets/l10n/main/app_de.json | 12 ++++++------ frontend/assets/l10n/main/app_en.json | 12 ++++++------ frontend/assets/l10n/nuernberg/overwrite_de.json | 8 ++++---- frontend/assets/l10n/nuernberg/overwrite_en.json | 10 +++++----- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/frontend/assets/l10n/main/app_de.json b/frontend/assets/l10n/main/app_de.json index f620b89cf..44ad09c9e 100644 --- a/frontend/assets/l10n/main/app_de.json +++ b/frontend/assets/l10n/main/app_de.json @@ -73,10 +73,10 @@ "applyTitle": "Beantragen", "authenticationPossible": "Mit diesem QR-Code können Sie sich bei Akzeptanzstellen ausweisen:", "cameraAccessRequired": "Zugriff auf Kamera erforderlich", - "cameraAccessRequiredSettings": "Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera.\nIn den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.", + "cameraAccessRequiredSettings": "Um einen QR-Code einzuscannen, benötigt die App Zugriff auf die Kamera. In den Einstellungen können Sie der App den Zugriff auf die Kamera erlauben.", "cardAlreadyActivated": "Der eingescannte QRCode wurde bereits aktiviert.", - "cardExpired": "Ihre Karte ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", - "cardInvalid": "Ihre Karte ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", + "cardExpired": "Ihre Karte ist abgelaufen. Unter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", + "cardInvalid": "Ihre Karte ist ungültig. Sie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", "cardNotYetValid": "Der Gültigkeitszeitraum Ihrer Karte hat noch nicht begonnen.", "checkFailed": "Ihre Karte konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Internetverbindung besteht und prüfen Sie erneut.", "checkRequired": "Prüfung nötig", @@ -123,11 +123,11 @@ "verifyTitle": "Gültigkeit prüfen" }, "intro": { - "applyDescription": "Im Formular geben Sie Informationen über sich und Ihre ehrenamtliche Tätigkeit an.\nAnschließend wird der Antrag weitergeleitet und von der zuständigen Stelle bearbeitet.", + "applyDescription": "Im Formular geben Sie Informationen über sich und Ihre ehrenamtliche Tätigkeit an. Anschließend wird der Antrag weitergeleitet und von der zuständigen Stelle bearbeitet.", "applyTitle": "Wie kann ich die Ehrenamtskarte beantragen?", - "locationDescription": "Wir können Ihren Standort auf der Karte anzeigen und Akzeptanzstellen in Ihrer Umgebung anzeigen.\nWenn Sie diese Hilfen nutzen möchten, benötigen wir Ihre Zustimmung.\nIhr Standort wird nicht gespeichert.", + "locationDescription": "Wir können Ihren Standort auf der Karte anzeigen und Akzeptanzstellen in Ihrer Umgebung anzeigen. Wenn Sie diese Hilfen nutzen möchten, benötigen wir Ihre Zustimmung. Ihr Standort wird nicht gespeichert.", "locationTitle": "Finden Sie Akzeptanzstellen in Ihrer Umgebung!", - "usageDescription": "Auf der Karte von Bayern können Sie alle Akzeptanzstellen finden.\nTippen Sie auf einen Standort, um mehr Informationen sehen zu können.", + "usageDescription": "Auf der Karte von Bayern können Sie alle Akzeptanzstellen finden. Tippen Sie auf einen Standort, um mehr Informationen sehen zu können.", "usageTitle": "Wo kann ich meine Ehrenamtskarte nutzen?", "welcomeDescription": "Vielen Dank, dass Sie sich die App zur Bayerischen Ehrenamtskarte heruntergeladen haben!", "welcomeTitle": "Willkommen!" diff --git a/frontend/assets/l10n/main/app_en.json b/frontend/assets/l10n/main/app_en.json index 60e138bae..285aa1890 100644 --- a/frontend/assets/l10n/main/app_en.json +++ b/frontend/assets/l10n/main/app_en.json @@ -73,10 +73,10 @@ "applyTitle": "Apply", "authenticationPossible": "You can use this QR code to identify yourself at acceptance points:", "cameraAccessRequired": "Access to camera required", - "cameraAccessRequiredSettings": "To scan a QR code, the app needs access to the camera.\nYou can allow the app to access the camera in the settings.", + "cameraAccessRequiredSettings": "To scan a QR code, the app needs access to the camera. You can allow the app to access the camera in the settings.", "cardAlreadyActivated": "The scanned QR code has already been activated.", - "cardExpired": "Your card has expired.\nYou can apply for renewal under \"More actions\"", - "cardInvalid": "Your card is invalid.\nIt has either been revoked or activated on another device.", + "cardExpired": "Your card has expired. You can apply for renewal under \"More actions\"", + "cardInvalid": "Your card is invalid. It has either been revoked or activated on another device.", "cardNotYetValid": "The validity period of your card has not started yet.", "checkFailed": "Your card could not be verified. Please make sure you have an internet connection and try again.", "checkRequired": "Verification necessary", @@ -123,11 +123,11 @@ "verifyTitle": "Check validity" }, "intro": { - "applyDescription": "Provide information about yourself and your volunteer activity in the form.\nThe application is then forwarded and processed by the responsible office.", + "applyDescription": "Provide information about yourself and your volunteer activity in the form. The application is then forwarded and processed by the responsible office.", "applyTitle": "How can I apply for the Ehrenamtskarte?", - "locationDescription": "We can show your location on the map and display acceptance points in your area.\nIf you want to use these aids, we need your consent.\nYour location is not stored.", + "locationDescription": "We can show your location on the map and display acceptance points in your area. If you want to use these aids, we need your consent. Your location is not stored.", "locationTitle": "Find acceptance points in your area!", - "usageDescription": "On the map of Bavaria you can find all acceptance points.\nTap on a location to be able to see more information.", + "usageDescription": "On the map of Bavaria you can find all acceptance points. Tap on a location to be able to see more information.", "usageTitle": "Where can I use my Ehrenamtskarte?", "welcomeDescription": "Thank you for downloading the app for the Bayerische Ehrenamtskarte!", "welcomeTitle": "Welcome!" diff --git a/frontend/assets/l10n/nuernberg/overwrite_de.json b/frontend/assets/l10n/nuernberg/overwrite_de.json index 31b0a25be..76c38c031 100644 --- a/frontend/assets/l10n/nuernberg/overwrite_de.json +++ b/frontend/assets/l10n/nuernberg/overwrite_de.json @@ -5,8 +5,8 @@ "activateDescription": "Sie haben den Nürnberg-Pass bereits beantragt und einen Aktivierungscode erhalten? Scannen Sie den Code hier ein.", "activateTitle": "Pass aktivieren", "applyDescription": "Sie haben noch keinen Nürnberg-Pass? Hier können Sie Ihren Nürnberg-Pass beantragen.", - "cardExpired": "Ihr Pass ist abgelaufen.\nUnter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", - "cardInvalid": "Ihr Pass ist ungültig.\nSie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", + "cardExpired": "Ihr Pass ist abgelaufen. Unter \"Weitere Aktionen\" können Sie einen Antrag auf Verlängerung stellen.", + "cardInvalid": "Ihr Pass ist ungültig. Sie wurde entweder widerrufen oder auf einem anderen Gerät aktiviert.", "cardNotYetValid": "Der Gültigkeitszeitraum Ihres Passes hat noch nicht begonnen.", "checkFailed": "Ihr Pass konnte nicht auf ihre Gültigkeit geprüft werden. Bitte stellen Sie sicher, dass eine Internetverbindung besteht und prüfen Sie erneut.", "moreActionsActivateDescription": "Ihr hinterlegter Nürnberg-Pass bleibt erhalten. Sie können diesen manuell entfernen.", @@ -25,9 +25,9 @@ "verifyInfoTitle": "So prüfen Sie die Gültigkeit eines Nürnberg-Passes" }, "intro": { - "applyDescription": "Im Formular geben Sie Ihre persönlichen Informationen an.\nAnschließend wird der Antrag weitergeleitet und von der zuständigen Stelle bearbeitet.", + "applyDescription": "Im Formular geben Sie Ihre persönlichen Informationen an. Anschließend wird der Antrag weitergeleitet und von der zuständigen Stelle bearbeitet.", "applyTitle": "Wie kann ich den Nürnberg-Pass beantragen?", - "usageDescription": "Auf der Karte von Nürnberg können Sie alle Akzeptanzstellen finden.\nTippen Sie auf einen Standort, um mehr Informationen sehen zu können.", + "usageDescription": "Auf der Karte von Nürnberg können Sie alle Akzeptanzstellen finden. Tippen Sie auf einen Standort, um mehr Informationen sehen zu können.", "usageTitle": "Wo kann ich den Nürnberg-Pass nutzen?", "welcomeDescription": "Vielen Dank, dass Sie sich die App zum Nürnberg-Pass heruntergeladen haben!" } diff --git a/frontend/assets/l10n/nuernberg/overwrite_en.json b/frontend/assets/l10n/nuernberg/overwrite_en.json index 70589e29b..6a1cb650d 100644 --- a/frontend/assets/l10n/nuernberg/overwrite_en.json +++ b/frontend/assets/l10n/nuernberg/overwrite_en.json @@ -4,9 +4,9 @@ "activateCurrentDeviceTitle": "Activate pass on this device?", "activateDescription": "You have already applied for the Nürnberg-Pass and received an activation code? Scan the code here.", "activateTitle": "Activate pass", - "applyDescription": "You don't have a Nürnberg-Pass yet? Here you can apply for your Nürnberg-Pass.", - "cardExpired": "Your pass has expired.\nYou can apply for renewal under \"More actions\"", - "cardInvalid": "Your pass is invalid.\nIt has either been revoked or activated on another device.", + "applyDescription": "You do not yet have a Nürnberg-Pass? Here you can apply for your Nürnberg-Pass.", + "cardExpired": "Your pass has expired. You can apply for renewal under \"More actions\"", + "cardInvalid": "Your pass is invalid. It has either been revoked or activated on another device.", "cardNotYetValid": "The validity period of your pass has not started yet.", "checkFailed": "Your pass could not be verified. Please make sure you have an internet connection and try again.", "moreActionsActivateDescription": "Your saved Nürnberg-Pass will be retained. You can remove it manually.", @@ -25,9 +25,9 @@ "verifyInfoTitle": "How to check the validity of a Nürnberg-Pass" }, "intro": { - "applyDescription": "Provide your personal information in the form.\nThe application is then forwarded and processed by the responsible office.", + "applyDescription": "Provide your personal information in the form. The application is then forwarded and processed by the responsible office.", "applyTitle": "How can I apply for the Nürnberg-Pass?", - "usageDescription": "On the map of Nürnberg you can find all acceptance points.\nTap on a location to be able to see more information.", + "usageDescription": "On the map of Nürnberg you can find all acceptance points. Tap on a location to be able to see more information.", "usageTitle": "Where can I use my Nürnberg-Pass?", "welcomeDescription": "Thank you for downloading the app for the Nürnberg-Pass!" } From e0dc53155fe62e754e4209acdea38dd875d54842 Mon Sep 17 00:00:00 2001 From: Sarah Date: Fri, 27 Oct 2023 11:25:21 +0200 Subject: [PATCH 48/60] move matomo.env into docker folder --- docker-compose.yml | 4 ++-- matomo.env => docker/matomo.env | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename matomo.env => docker/matomo.env (100%) diff --git a/docker-compose.yml b/docker-compose.yml index be2962e38..53ca33d2e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,7 +47,7 @@ services: - MARIADB_AUTO_UPGRADE=1 - MARIADB_DISABLE_UPGRADE_BACKUP=1 env_file: - - ./matomo.env + - ./docker/matomo.env networks: - network matomo: @@ -57,7 +57,7 @@ services: volumes: - matomo:/var/www/html:z env_file: - - ./matomo.env + - ./docker/matomo.env ports: - 5003:80 networks: diff --git a/matomo.env b/docker/matomo.env similarity index 100% rename from matomo.env rename to docker/matomo.env From 895f57fd7246aca9d711057e1d0ffd77ed947454 Mon Sep 17 00:00:00 2001 From: ztefanie Date: Tue, 31 Oct 2023 11:28:18 +0100 Subject: [PATCH 49/60] 1191: Fix mail content type header charset --- .../src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt index 135b47c26..8e789b1ee 100644 --- a/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt @@ -55,7 +55,7 @@ object Mailer { .from(fromName, smtpConfig.username) .withSubject(subject) .withPlainText(message) - .withHeader("Content-Type", "text/plain; charset=utf8") + .withHeader("Content-Type", "text/plain; charset=UTF-8") .buildEmail() ).join() } catch (exception: MailException) { @@ -160,6 +160,7 @@ object Mailer { personalData: PersonalData, accessKey: String ) { + println("MAIL SENT") val message = """ Guten Tag ${personalData.forenames.shortText} ${personalData.surname.shortText}, From b7ca3457b6c69bfe702db86e3411b0a4e488e19c Mon Sep 17 00:00:00 2001 From: ztefanie Date: Tue, 31 Oct 2023 11:46:18 +0100 Subject: [PATCH 50/60] 1190: Increase comprehensibility of application form --- .../application/forms/WorkAtOrganizationForm.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/administration/src/mui-modules/application/forms/WorkAtOrganizationForm.tsx b/administration/src/mui-modules/application/forms/WorkAtOrganizationForm.tsx index ed1129403..017e6c6c1 100644 --- a/administration/src/mui-modules/application/forms/WorkAtOrganizationForm.tsx +++ b/administration/src/mui-modules/application/forms/WorkAtOrganizationForm.tsx @@ -67,11 +67,7 @@ const WorkAtOrganizationForm: Form ( <> - -

Angaben zur Tätigkeit

+

Angaben zu ihrer ehrenamtlichen Tätigkeit

+ Date: Mon, 6 Nov 2023 08:45:13 +0100 Subject: [PATCH 51/60] Change lowercase ihrer to uppercase Co-authored-by: Michael Markl --- .../mui-modules/application/forms/WorkAtOrganizationForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administration/src/mui-modules/application/forms/WorkAtOrganizationForm.tsx b/administration/src/mui-modules/application/forms/WorkAtOrganizationForm.tsx index 017e6c6c1..287ca6a35 100644 --- a/administration/src/mui-modules/application/forms/WorkAtOrganizationForm.tsx +++ b/administration/src/mui-modules/application/forms/WorkAtOrganizationForm.tsx @@ -67,7 +67,7 @@ const WorkAtOrganizationForm: Form ( <> -

Angaben zu ihrer ehrenamtlichen Tätigkeit

+

Angaben zu Ihrer ehrenamtlichen Tätigkeit

Date: Mon, 6 Nov 2023 09:40:38 +0100 Subject: [PATCH 52/60] 1191: Remove println --- .../src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt index 8e789b1ee..825116025 100644 --- a/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/mail/Mailer.kt @@ -160,7 +160,6 @@ object Mailer { personalData: PersonalData, accessKey: String ) { - println("MAIL SENT") val message = """ Guten Tag ${personalData.forenames.shortText} ${personalData.surname.shortText}, From 6d3eafcf5e64fb68cf9e05aed9993225ef692c65 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 6 Nov 2023 13:34:35 +0100 Subject: [PATCH 53/60] 1177: Improve translations --- frontend/assets/l10n/main/app_en.json | 20 +++++++++---------- .../assets/l10n/nuernberg/overwrite_en.json | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/assets/l10n/main/app_en.json b/frontend/assets/l10n/main/app_en.json index 285aa1890..12d154821 100644 --- a/frontend/assets/l10n/main/app_en.json +++ b/frontend/assets/l10n/main/app_en.json @@ -40,7 +40,7 @@ "media": "Multimedia", "mobility": "Mobility", "mobilityLong": "Car/Bicycle", - "movies": "Plays", + "movies": "Shows", "moviesLong": "Cinema/Theater/Concerts", "other": "Other", "pharmacies": "Pharmacies", @@ -69,7 +69,7 @@ "activateCurrentDeviceTitle": "Activate card on this device?", "activateDescription": "You have already applied for the Ehrenamtskarte and received the activation code for your digital Ehrenamtskarte? Scan the code here.", "activateTitle": "Activate card", - "applyDescription": "Are you involved in voluntary work but do not yet have a Ehrenamtskarte? You can apply for your Ehrenamtskarte here.", + "applyDescription": "Are you involved in voluntary work but do not yet have an Ehrenamtskarte? You can apply for your Ehrenamtskarte here.", "applyTitle": "Apply", "authenticationPossible": "You can use this QR code to identify yourself at acceptance points:", "cameraAccessRequired": "Access to camera required", @@ -77,7 +77,7 @@ "cardAlreadyActivated": "The scanned QR code has already been activated.", "cardExpired": "Your card has expired. You can apply for renewal under \"More actions\"", "cardInvalid": "Your card is invalid. It has either been revoked or activated on another device.", - "cardNotYetValid": "The validity period of your card has not started yet.", + "cardNotYetValid": "The validity period of your card has not yet started.", "checkFailed": "Your card could not be verified. Please make sure you have an internet connection and try again.", "checkRequired": "Verification necessary", "checkingCode": "The QR code is verified by a server request.", @@ -94,15 +94,15 @@ "flashOff": "Flash off", "flashOn": "Flash on", "internetRequired": "An internet connection is required.", - "moreActionsActivateDescription": "Your saved Ehrenamtskarte will be retained. You can remove it manually.", + "moreActionsActivateDescription": "Your saved cards will be retained. You can remove them manually.", "moreActionsActivateLimitDescription": "To add another Ehrenamtskarte, you must first delete an existing Ehrenamtskarte.", "moreActionsActivateTitle": "Add another Ehrenamtskarte", - "moreActionsApplyDescription": "Your saved card will be retained.", - "moreActionsApplyTitle": "Apply for or extend a Ehrenamtskarte", - "moreActionsRemoveDescription": "After selection, this Ehrenamtskarte is deleted from the device.", + "moreActionsApplyDescription": "Your saved cards will be retained.", + "moreActionsApplyTitle": "Apply for or extend an Ehrenamtskarte", + "moreActionsRemoveDescription": "After selection, this Ehrenamtskarte will be deleted from the device.", "moreActionsRemoveTitle": "Delete this Ehrenamtskarte?", - "moreActionsVerifyDescription": "Check the validity of a Ehrenamtskarte", - "moreActionsVerifyTitle": "Check a Ehrenamtskarte", + "moreActionsVerifyDescription": "Check the validity of an Ehrenamtskarte", + "moreActionsVerifyTitle": "Check an Ehrenamtskarte", "notVerified": "Not verified", "removeDescription": "If this card is deleted, it must be added again before using it again.", "removeTitle": "Delete this card?", @@ -119,7 +119,7 @@ "validUntil": "Valid until: {{expirationDate}}", "verificationSuccessful": "Card is valid", "verifyDescription": "You would like to check the validity of a digital Ehrenamtskarte? Scan the code here.", - "verifyInfoTitle": "How to check the validity of a Ehrenamtskarte", + "verifyInfoTitle": "How to check the validity of an Ehrenamtskarte", "verifyTitle": "Check validity" }, "intro": { diff --git a/frontend/assets/l10n/nuernberg/overwrite_en.json b/frontend/assets/l10n/nuernberg/overwrite_en.json index 6a1cb650d..159272b6a 100644 --- a/frontend/assets/l10n/nuernberg/overwrite_en.json +++ b/frontend/assets/l10n/nuernberg/overwrite_en.json @@ -7,14 +7,14 @@ "applyDescription": "You do not yet have a Nürnberg-Pass? Here you can apply for your Nürnberg-Pass.", "cardExpired": "Your pass has expired. You can apply for renewal under \"More actions\"", "cardInvalid": "Your pass is invalid. It has either been revoked or activated on another device.", - "cardNotYetValid": "The validity period of your pass has not started yet.", + "cardNotYetValid": "The validity period of your pass has not yet started.", "checkFailed": "Your pass could not be verified. Please make sure you have an internet connection and try again.", - "moreActionsActivateDescription": "Your saved Nürnberg-Pass will be retained. You can remove it manually.", + "moreActionsActivateDescription": "Your saved passes will be retained. You can remove them manually.", "moreActionsActivateLimitDescription": "To add another Nürnberg-Pass, you must first delete an existing Nürnberg-Pass.", "moreActionsActivateTitle": "Add another Nürnberg-Pass", - "moreActionsApplyDescription": "Your saved pass will be retained.", + "moreActionsApplyDescription": "Your saved passes will be retained.", "moreActionsApplyTitle": "Apply for or extend a Nürnberg-Pass", - "moreActionsRemoveDescription": "After selection, this Nürnberg-Pass is deleted from the device.", + "moreActionsRemoveDescription": "After selection, this Nürnberg-Pass will be deleted from the device.", "moreActionsRemoveTitle": "Delete this Nürnberg-Pass?", "moreActionsVerifyDescription": "Check the validity of a Nürnberg-Pass", "moreActionsVerifyTitle": "Check a Nürnberg-Pass", From 57d9640540e8da45657abfd1ccee29cfd85a7cc9 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 6 Nov 2023 13:50:39 +0100 Subject: [PATCH 54/60] 1177: Fix l10n override paths --- frontend/assets/l10n/{main => }/app_de.json | 0 frontend/assets/l10n/{main => }/app_en.json | 0 .../overwrite_de.json => nuernberg/l10n/override_de.json} | 0 .../overwrite_en.json => nuernberg/l10n/override_en.json} | 0 frontend/build-configs/bayern/index.ts | 2 +- frontend/build-configs/nuernberg/index.ts | 2 +- frontend/build-configs/types.ts | 2 +- frontend/build.yaml | 4 ++-- frontend/lib/main.dart | 6 +++--- frontend/pubspec.yaml | 2 +- 10 files changed, 9 insertions(+), 9 deletions(-) rename frontend/assets/l10n/{main => }/app_de.json (100%) rename frontend/assets/l10n/{main => }/app_en.json (100%) rename frontend/assets/{l10n/nuernberg/overwrite_de.json => nuernberg/l10n/override_de.json} (100%) rename frontend/assets/{l10n/nuernberg/overwrite_en.json => nuernberg/l10n/override_en.json} (100%) diff --git a/frontend/assets/l10n/main/app_de.json b/frontend/assets/l10n/app_de.json similarity index 100% rename from frontend/assets/l10n/main/app_de.json rename to frontend/assets/l10n/app_de.json diff --git a/frontend/assets/l10n/main/app_en.json b/frontend/assets/l10n/app_en.json similarity index 100% rename from frontend/assets/l10n/main/app_en.json rename to frontend/assets/l10n/app_en.json diff --git a/frontend/assets/l10n/nuernberg/overwrite_de.json b/frontend/assets/nuernberg/l10n/override_de.json similarity index 100% rename from frontend/assets/l10n/nuernberg/overwrite_de.json rename to frontend/assets/nuernberg/l10n/override_de.json diff --git a/frontend/assets/l10n/nuernberg/overwrite_en.json b/frontend/assets/nuernberg/l10n/override_en.json similarity index 100% rename from frontend/assets/l10n/nuernberg/overwrite_en.json rename to frontend/assets/nuernberg/l10n/override_en.json diff --git a/frontend/build-configs/bayern/index.ts b/frontend/build-configs/bayern/index.ts index f8bd1f41c..1185ba8e4 100644 --- a/frontend/build-configs/bayern/index.ts +++ b/frontend/build-configs/bayern/index.ts @@ -31,7 +31,7 @@ export const bayernCommon: CommonBuildConfigType = { local: "http://localhost:8000", }, appLocales: ['de'], - localeOverwritePath: null, + localeOverridePath: null, cardBranding: { headerTextColor: "#008dc9", headerColor: "#F5F5FFF5", diff --git a/frontend/build-configs/nuernberg/index.ts b/frontend/build-configs/nuernberg/index.ts index a9ae629ff..526fc1f47 100644 --- a/frontend/build-configs/nuernberg/index.ts +++ b/frontend/build-configs/nuernberg/index.ts @@ -31,7 +31,7 @@ export const nuernbergCommon: CommonBuildConfigType = { local: "http://localhost:8000", }, appLocales: ['de', 'en'], - localeOverwritePath: 'assets/l10n/nuernberg', + localeOverridePath: 'assets/nuernberg/l10n', cardBranding: { headerTextColor: "#000000", headerTextFontSize: 9, diff --git a/frontend/build-configs/types.ts b/frontend/build-configs/types.ts index 7403f6db7..8b553c4b2 100644 --- a/frontend/build-configs/types.ts +++ b/frontend/build-configs/types.ts @@ -37,7 +37,7 @@ export type CommonBuildConfigType = { local: string } appLocales: string[] - localeOverwritePath: string | null + localeOverridePath: string | null cardBranding: { headerTextColor: string headerColor: string diff --git a/frontend/build.yaml b/frontend/build.yaml index 4d9f53c9d..417a551a2 100644 --- a/frontend/build.yaml +++ b/frontend/build.yaml @@ -8,7 +8,7 @@ targets: - schema.graphql - card.proto - lib/build_config/build_config.yaml - - assets/l10n/main/** + - assets/l10n/* builders: df_build_config: generate_for: @@ -38,7 +38,7 @@ targets: slang_build_runner: options: base_locale: de - input_directory: assets/l10n/main + input_directory: assets/l10n input_file_pattern: .json output_file_name: translations.g.dart output_directory: lib/l10n diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 3ce466071..e5bec10f2 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -23,10 +23,10 @@ Future main() async { // Use override locales for whitelabels (e.g. nuernberg) // ignore: unnecessary_null_comparison - if (buildConfig.localeOverwritePath != null) { + if (buildConfig.localeOverridePath != null) { void override(AppLocale locale) async { - final localeOverwritePath = '${buildConfig.localeOverwritePath}/overwrite_${locale.languageCode}.json'; - String overrideLocales = await rootBundle.loadString(localeOverwritePath); + final localeOverridePath = '${buildConfig.localeOverridePath}/override_${locale.languageCode}.json'; + String overrideLocales = await rootBundle.loadString(localeOverridePath); LocaleSettings.overrideTranslations(locale: locale, fileType: FileType.json, content: overrideLocales); } diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 986de2d42..ac9a6e619 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -112,7 +112,7 @@ flutter: - assets/nuernberg/body-logo.png - assets/nuernberg/background.png - assets/nuernberg/intro_slides/ - - assets/l10n/nuernberg/ + - assets/nuernberg/l10n/ # An image asset can refer to one or more resolution-specific 'variants', see # https://flutter.dev/assets-and-images/#resolution-aware. From aa3d4448978c26751eb27fa75660bc85a88da324 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 6 Nov 2023 14:27:27 +0100 Subject: [PATCH 55/60] 1177: Use English as locale fallback if supported --- frontend/lib/main.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index e5bec10f2..311e136dc 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -17,6 +17,8 @@ Future main() async { final locale = Platform.localeName.split('_')[0]; if (buildConfig.appLocales.contains(locale)) { LocaleSettings.useDeviceLocale(); + } else if (buildConfig.appLocales.contains('en')) { + LocaleSettings.setLocale(AppLocale.en); } else { LocaleSettings.setLocale(AppLocale.de); } From c90e91c1c24f7ea7e2b5e796bcaf64b38cd9d703 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 6 Nov 2023 14:32:46 +0100 Subject: [PATCH 56/60] 1177: Fix pubspec.yaml --- frontend/pubspec.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index ac9a6e619..145fcc894 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -93,9 +93,6 @@ dev_dependencies: # The following section is specific to Flutter. flutter: - # Used for localization (l10n) - 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. From d029402d33d554b6252ddaa99429804fbe9dc728 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 7 Nov 2023 16:39:31 +0100 Subject: [PATCH 57/60] 1175: add language switch, adjust tabItem for sozialpass --- frontend/assets/l10n/app_de.json | 4 + frontend/assets/l10n/app_en.json | 4 + .../assets/nuernberg/l10n/override_de.json | 3 + .../assets/nuernberg/l10n/override_en.json | 3 + frontend/build-configs/bayern/index.ts | 3 +- frontend/build-configs/nuernberg/index.ts | 3 +- frontend/build-configs/types.ts | 1 + frontend/lib/about/about_page.dart | 109 +++++++++++------- frontend/lib/about/language_change.dart | 27 +++++ frontend/lib/about/section.dart | 23 ++++ frontend/lib/home/home_page.dart | 4 +- 11 files changed, 138 insertions(+), 46 deletions(-) create mode 100644 frontend/lib/about/language_change.dart create mode 100644 frontend/lib/about/section.dart diff --git a/frontend/assets/l10n/app_de.json b/frontend/assets/l10n/app_de.json index 44ad09c9e..a82517af3 100644 --- a/frontend/assets/l10n/app_de.json +++ b/frontend/assets/l10n/app_de.json @@ -15,6 +15,10 @@ }, "privacyDeclaration": "Datenschutzerklärung", "publisher": "Herausgeber", + "settings": { + "headline": "Einstellungen", + "languageChange": "Sprache wechseln" + }, "sourceCode": "Quellcode der App", "title": "Über" }, diff --git a/frontend/assets/l10n/app_en.json b/frontend/assets/l10n/app_en.json index 12d154821..a3b68975c 100644 --- a/frontend/assets/l10n/app_en.json +++ b/frontend/assets/l10n/app_en.json @@ -15,6 +15,10 @@ }, "privacyDeclaration": "Privacy policy", "publisher": "Publisher", + "settings": { + "headline": "Settings", + "languageChange": "Change language" + }, "sourceCode": "Source code", "title": "About" }, diff --git a/frontend/assets/nuernberg/l10n/override_de.json b/frontend/assets/nuernberg/l10n/override_de.json index 76c38c031..a63359282 100644 --- a/frontend/assets/nuernberg/l10n/override_de.json +++ b/frontend/assets/nuernberg/l10n/override_de.json @@ -30,5 +30,8 @@ "usageDescription": "Auf der Karte von Nürnberg können Sie alle Akzeptanzstellen finden. Tippen Sie auf einen Standort, um mehr Informationen sehen zu können.", "usageTitle": "Wo kann ich den Nürnberg-Pass nutzen?", "welcomeDescription": "Vielen Dank, dass Sie sich die App zum Nürnberg-Pass heruntergeladen haben!" + }, + "about": { + "title": "Mehr" } } diff --git a/frontend/assets/nuernberg/l10n/override_en.json b/frontend/assets/nuernberg/l10n/override_en.json index 159272b6a..ca52269e8 100644 --- a/frontend/assets/nuernberg/l10n/override_en.json +++ b/frontend/assets/nuernberg/l10n/override_en.json @@ -30,5 +30,8 @@ "usageDescription": "On the map of Nürnberg you can find all acceptance points. Tap on a location to be able to see more information.", "usageTitle": "Where can I use my Nürnberg-Pass?", "welcomeDescription": "Thank you for downloading the app for the Nürnberg-Pass!" + }, + "about": { + "title": "More" } } diff --git a/frontend/build-configs/bayern/index.ts b/frontend/build-configs/bayern/index.ts index d5c55ddf9..71c6dfbd7 100644 --- a/frontend/build-configs/bayern/index.ts +++ b/frontend/build-configs/bayern/index.ts @@ -62,7 +62,8 @@ export const bayernCommon: CommonBuildConfigType = { "assets/bayern/intro_slides/search_with_location.png", ], featureFlags: { - verification: true + verification: true, + settings: false }, applicationUrl: "https://bayern.ehrenamtskarte.app/beantragen", dataPrivacyPolicyUrl: "https://bayern.ehrenamtskarte.app/data-privacy-policy", diff --git a/frontend/build-configs/nuernberg/index.ts b/frontend/build-configs/nuernberg/index.ts index c24ca1b16..2bd24eefd 100644 --- a/frontend/build-configs/nuernberg/index.ts +++ b/frontend/build-configs/nuernberg/index.ts @@ -62,7 +62,8 @@ export const nuernbergCommon: CommonBuildConfigType = { "assets/nuernberg/intro_slides/search_with_location.png", ], featureFlags: { - verification: true + verification: true, + settings: true }, applicationUrl: "https://beantragen.nuernberg.sozialpass.app", publisherAddress: diff --git a/frontend/build-configs/types.ts b/frontend/build-configs/types.ts index e7085380b..1825aa5dc 100644 --- a/frontend/build-configs/types.ts +++ b/frontend/build-configs/types.ts @@ -6,6 +6,7 @@ type BuildConfigType = { export type FeatureFlagsType = { verification: boolean + settings: boolean } export type ThemeType = { diff --git a/frontend/lib/about/about_page.dart b/frontend/lib/about/about_page.dart index 10e942cdb..c1c9beaa1 100644 --- a/frontend/lib/about/about_page.dart +++ b/frontend/lib/about/about_page.dart @@ -1,7 +1,9 @@ import 'package:ehrenamtskarte/about/backend_switch_dialog.dart'; import 'package:ehrenamtskarte/about/content_tile.dart'; import 'package:ehrenamtskarte/about/dev_settings_view.dart'; +import 'package:ehrenamtskarte/about/language_change.dart'; import 'package:ehrenamtskarte/about/license_page.dart'; +import 'package:ehrenamtskarte/about/section.dart'; import 'package:ehrenamtskarte/about/texts.dart'; import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; import 'package:ehrenamtskarte/configuration/configuration.dart'; @@ -95,53 +97,76 @@ class AboutPageState extends State { ); }, ), + if (buildConfig.featureFlags.settings) + Column(children: [ + const Divider( + height: 1, + thickness: 1, + ), + Section( + headline: t.about.settings.headline, + children: [ + ContentTile( + icon: Icons.language, title: t.about.settings.languageChange, children: [LanguageChange()]), + ], + ), + ]), const Divider( height: 1, thickness: 1, ), - const SizedBox(height: 20), - ContentTile(icon: Icons.copyright, title: t.about.licenses(n: 1), children: getCopyrightText(context)), - ListTile( - leading: const Icon(Icons.privacy_tip_outlined), - title: Text(t.about.privacyDeclaration), - onTap: () => launchUrlString(buildConfig.dataPrivacyPolicyUrl, mode: LaunchMode.externalApplication), - ), - ContentTile( - icon: Icons.info_outline, - title: t.about.disclaimer, - children: getDisclaimerText(context), - ), - ListTile( - leading: const Icon(Icons.book_outlined), - title: Text(t.about.dependencies), - onTap: () { - Navigator.push( - context, - AppRoute( - builder: (context) => const CustomLicensePage(), - ), - ); - }, - ), - ListTile( - leading: const Icon(Icons.code_outlined), - title: Text(t.about.sourceCode), - onTap: () { - launchUrlString( - 'https://github.com/digitalfabrik/entitlementcard', - mode: LaunchMode.externalApplication, - ); - }, - ), - if (config.showDevSettings) + Section(headline: t.about.moreInformation, children: [ + ContentTile(icon: Icons.copyright, title: t.about.licenses(n: 1), children: getCopyrightText(context)), ListTile( - leading: const Icon(Icons.build), - title: Text(t.about.developmentOptions), - onTap: () => showDialog( - context: context, - builder: (context) => - SimpleDialog(title: Text(t.about.developmentOptions), children: [DevSettingsView()]), - ), + leading: const Icon(Icons.privacy_tip_outlined), + title: Text(t.about.privacyDeclaration), + onTap: () => launchUrlString(buildConfig.dataPrivacyPolicyUrl, mode: LaunchMode.externalApplication), + ), + ContentTile( + icon: Icons.info_outline, + title: t.about.disclaimer, + children: getDisclaimerText(context), + ), + ListTile( + leading: const Icon(Icons.book_outlined), + title: Text(t.about.dependencies), + onTap: () { + Navigator.push( + context, + AppRoute( + builder: (context) => const CustomLicensePage(), + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.code_outlined), + title: Text(t.about.sourceCode), + onTap: () { + launchUrlString( + 'https://github.com/digitalfabrik/entitlementcard', + mode: LaunchMode.externalApplication, + ); + }, + ), + ]), + if (config.showDevSettings) + Column( + children: [ + const Divider( + height: 1, + thickness: 1, + ), + ListTile( + leading: const Icon(Icons.build), + title: Text(t.about.developmentOptions), + onTap: () => showDialog( + context: context, + builder: (context) => + SimpleDialog(title: Text(t.about.developmentOptions), children: [DevSettingsView()]), + ), + ) + ], ) ]; } else { diff --git a/frontend/lib/about/language_change.dart b/frontend/lib/about/language_change.dart new file mode 100644 index 000000000..f065e08dc --- /dev/null +++ b/frontend/lib/about/language_change.dart @@ -0,0 +1,27 @@ +import 'package:ehrenamtskarte/build_config/build_config.dart' show buildConfig; +import 'package:ehrenamtskarte/l10n/translations.g.dart'; +import 'package:flutter/material.dart'; + +Map languages = {'en': 'Englisch', 'de': 'Deutsch'}; + +class LanguageChange extends StatelessWidget { + const LanguageChange({super.key}); + + @override + Widget build(BuildContext context) { + return Column(children: [ + ...buildConfig.appLocales.map((item) => DecoratedBox( + decoration: BoxDecoration( + color: LocaleSettings.currentLocale.languageCode == item + ? Theme.of(context).colorScheme.surfaceVariant + : null), + child: ListTile( + title: Text( + languages[item]!, + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.bold), + ), + onTap: () => LocaleSettings.setLocaleRaw(item)))) + ]); + } +} diff --git a/frontend/lib/about/section.dart b/frontend/lib/about/section.dart new file mode 100644 index 000000000..00f044d67 --- /dev/null +++ b/frontend/lib/about/section.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class Section extends StatelessWidget { + final String headline; + final List children; + + const Section({super.key, required this.headline, required this.children}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 20, left: 20, right: 20), + child: Text(headline, style: TextStyle(color: Theme.of(context).colorScheme.primary)), + ), + Column(children: children), + const SizedBox(height: 10), + ], + ); + } +} diff --git a/frontend/lib/home/home_page.dart b/frontend/lib/home/home_page.dart index aec036628..588b93def 100644 --- a/frontend/lib/home/home_page.dart +++ b/frontend/lib/home/home_page.dart @@ -55,8 +55,8 @@ class HomePageState extends State { (BuildContext context) => t.identification.title, GlobalKey(debugLabel: 'Auth tab key'), ), - AppFlow(const AboutPage(), Icons.info_outline, (BuildContext context) => t.about.title, - GlobalKey(debugLabel: 'About tab key')), + AppFlow(const AboutPage(), buildConfig.featureFlags.settings ? Icons.menu : Icons.info_outline, + (BuildContext context) => t.about.title, GlobalKey(debugLabel: 'About tab key')), ]; } From 5aa21d3143bd44df92b93ee27bc2bb50ac7a94ac Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 7 Nov 2023 17:48:08 +0100 Subject: [PATCH 58/60] 1175: add notification for language change --- frontend/assets/l10n/app_de.json | 3 ++- frontend/assets/l10n/app_en.json | 3 ++- frontend/lib/about/about_page.dart | 3 ++- frontend/lib/about/language_change.dart | 15 ++++++++++++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/frontend/assets/l10n/app_de.json b/frontend/assets/l10n/app_de.json index a82517af3..80da2d001 100644 --- a/frontend/assets/l10n/app_de.json +++ b/frontend/assets/l10n/app_de.json @@ -17,7 +17,8 @@ "publisher": "Herausgeber", "settings": { "headline": "Einstellungen", - "languageChange": "Sprache wechseln" + "languageChange": "Sprache wechseln", + "languageNotification": "Ihre Sprache wurde erfolgreich geändert!" }, "sourceCode": "Quellcode der App", "title": "Über" diff --git a/frontend/assets/l10n/app_en.json b/frontend/assets/l10n/app_en.json index a3b68975c..ebaeba9b6 100644 --- a/frontend/assets/l10n/app_en.json +++ b/frontend/assets/l10n/app_en.json @@ -17,7 +17,8 @@ "publisher": "Publisher", "settings": { "headline": "Settings", - "languageChange": "Change language" + "languageChange": "Change language", + "languageNotification": "Your language was changed successfully!" }, "sourceCode": "Source code", "title": "About" diff --git a/frontend/lib/about/about_page.dart b/frontend/lib/about/about_page.dart index c1c9beaa1..90b9e71a4 100644 --- a/frontend/lib/about/about_page.dart +++ b/frontend/lib/about/about_page.dart @@ -29,7 +29,8 @@ class AboutPageState extends State { @override Widget build(BuildContext context) { final config = Configuration.of(context); - + print('title:${t.about.settings.languageChange}'); + print('locale:${LocaleSettings.currentLocale.languageCode}'); return FutureBuilder( future: PackageInfo.fromPlatform(), builder: (context, snapshot) { diff --git a/frontend/lib/about/language_change.dart b/frontend/lib/about/language_change.dart index f065e08dc..103dff979 100644 --- a/frontend/lib/about/language_change.dart +++ b/frontend/lib/about/language_change.dart @@ -21,7 +21,20 @@ class LanguageChange extends StatelessWidget { textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.bold), ), - onTap: () => LocaleSettings.setLocaleRaw(item)))) + onTap: () => switchLanguage(context, item)))) ]); } + + switchLanguage(BuildContext context, String language) { + final messengerState = ScaffoldMessenger.of(context); + LocaleSettings.setLocaleRaw(language); + messengerState.showSnackBar( + SnackBar( + backgroundColor: Theme.of(context).colorScheme.primary, + content: Text(t.about.settings.languageNotification, + style: TextStyle(color: Theme.of(context).colorScheme.background)), + ), + ); + Navigator.pop(context); + } } From a0b82b25ecd4f8506e9a2758a4736217f728a4df Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 8 Nov 2023 14:41:03 +0100 Subject: [PATCH 59/60] 1175: replace identity icon, restructure translations for about --- frontend/assets/l10n/app_de.json | 23 +++++++++++++---------- frontend/assets/l10n/app_en.json | 23 +++++++++++++---------- frontend/build-configs/bayern/index.ts | 1 - frontend/build-configs/nuernberg/index.ts | 1 - frontend/build-configs/types.ts | 1 - frontend/lib/about/about_page.dart | 17 ++++++++--------- frontend/lib/about/language_change.dart | 2 +- frontend/lib/about/license_page.dart | 2 +- frontend/lib/about/section.dart | 8 ++++++-- frontend/lib/home/home_page.dart | 4 ++-- frontend/lib/themes.dart | 4 ++-- 11 files changed, 46 insertions(+), 40 deletions(-) diff --git a/frontend/assets/l10n/app_de.json b/frontend/assets/l10n/app_de.json index 80da2d001..83b0bb3a1 100644 --- a/frontend/assets/l10n/app_de.json +++ b/frontend/assets/l10n/app_de.json @@ -1,26 +1,29 @@ { "about": { - "dependencies": "Software-Bibliotheken", "developmentOptions": "Entwickleroptionen", - "disclaimer": "Haftung, Haftungsausschluss und Impressum", - "licenses": { - "one": "Lizenz", - "other": "Lizenzen" + "info": { + "dependencies": "Software-Bibliotheken", + "disclaimer": "Haftung, Haftungsausschluss und Impressum", + "headline": "Info", + "licenses": { + "one": "Lizenz", + "other": "Lizenzen" + }, + "privacyDeclaration": "Datenschutzerklärung", + "sourceCode": "Quellcode der App" }, "moreInformation": "Mehr Informationen", "numberLicenses": { - "zero": "Keine Lizenzen", "one": "1 Lizenz", - "other": "$n Lizenzen" + "other": "$n Lizenzen", + "zero": "Keine Lizenzen" }, - "privacyDeclaration": "Datenschutzerklärung", "publisher": "Herausgeber", "settings": { "headline": "Einstellungen", "languageChange": "Sprache wechseln", - "languageNotification": "Ihre Sprache wurde erfolgreich geändert!" + "languageChangeSuccessful": "Ihre Sprache wurde erfolgreich geändert!" }, - "sourceCode": "Quellcode der App", "title": "Über" }, "category": { diff --git a/frontend/assets/l10n/app_en.json b/frontend/assets/l10n/app_en.json index ebaeba9b6..e2c3933fc 100644 --- a/frontend/assets/l10n/app_en.json +++ b/frontend/assets/l10n/app_en.json @@ -1,26 +1,29 @@ { "about": { - "dependencies": "Libraries", "developmentOptions": "Developer options", - "disclaimer": "Liability, disclaimer and imprint", - "licenses": { - "one": "License", - "other": "Licenses" + "info": { + "dependencies": "Libraries", + "disclaimer": "Liability, disclaimer and imprint", + "headline": "Info", + "licenses": { + "one": "License", + "other": "Licenses" + }, + "privacyDeclaration": "Privacy policy", + "sourceCode": "Source code" }, "moreInformation": "More information", "numberLicenses": { - "zero": "No licenses", "one": "1 license", - "other": "$n licenses" + "other": "$n licenses", + "zero": "No licenses" }, - "privacyDeclaration": "Privacy policy", "publisher": "Publisher", "settings": { "headline": "Settings", "languageChange": "Change language", - "languageNotification": "Your language was changed successfully!" + "languageChangeSuccessful": "Your language was changed successfully!" }, - "sourceCode": "Source code", "title": "About" }, "category": { diff --git a/frontend/build-configs/bayern/index.ts b/frontend/build-configs/bayern/index.ts index 71c6dfbd7..79f0f6e30 100644 --- a/frontend/build-configs/bayern/index.ts +++ b/frontend/build-configs/bayern/index.ts @@ -63,7 +63,6 @@ export const bayernCommon: CommonBuildConfigType = { ], featureFlags: { verification: true, - settings: false }, applicationUrl: "https://bayern.ehrenamtskarte.app/beantragen", dataPrivacyPolicyUrl: "https://bayern.ehrenamtskarte.app/data-privacy-policy", diff --git a/frontend/build-configs/nuernberg/index.ts b/frontend/build-configs/nuernberg/index.ts index 2bd24eefd..6dd7f404e 100644 --- a/frontend/build-configs/nuernberg/index.ts +++ b/frontend/build-configs/nuernberg/index.ts @@ -63,7 +63,6 @@ export const nuernbergCommon: CommonBuildConfigType = { ], featureFlags: { verification: true, - settings: true }, applicationUrl: "https://beantragen.nuernberg.sozialpass.app", publisherAddress: diff --git a/frontend/build-configs/types.ts b/frontend/build-configs/types.ts index 1825aa5dc..e7085380b 100644 --- a/frontend/build-configs/types.ts +++ b/frontend/build-configs/types.ts @@ -6,7 +6,6 @@ type BuildConfigType = { export type FeatureFlagsType = { verification: boolean - settings: boolean } export type ThemeType = { diff --git a/frontend/lib/about/about_page.dart b/frontend/lib/about/about_page.dart index 90b9e71a4..ce944186c 100644 --- a/frontend/lib/about/about_page.dart +++ b/frontend/lib/about/about_page.dart @@ -29,8 +29,6 @@ class AboutPageState extends State { @override Widget build(BuildContext context) { final config = Configuration.of(context); - print('title:${t.about.settings.languageChange}'); - print('locale:${LocaleSettings.currentLocale.languageCode}'); return FutureBuilder( future: PackageInfo.fromPlatform(), builder: (context, snapshot) { @@ -98,7 +96,7 @@ class AboutPageState extends State { ); }, ), - if (buildConfig.featureFlags.settings) + if (buildConfig.appLocales.length > 1) Column(children: [ const Divider( height: 1, @@ -116,21 +114,22 @@ class AboutPageState extends State { height: 1, thickness: 1, ), - Section(headline: t.about.moreInformation, children: [ - ContentTile(icon: Icons.copyright, title: t.about.licenses(n: 1), children: getCopyrightText(context)), + Section(headline: t.about.info.headline, children: [ + ContentTile( + icon: Icons.copyright, title: t.about.info.licenses(n: 1), children: getCopyrightText(context)), ListTile( leading: const Icon(Icons.privacy_tip_outlined), - title: Text(t.about.privacyDeclaration), + title: Text(t.about.info.privacyDeclaration), onTap: () => launchUrlString(buildConfig.dataPrivacyPolicyUrl, mode: LaunchMode.externalApplication), ), ContentTile( icon: Icons.info_outline, - title: t.about.disclaimer, + title: t.about.info.disclaimer, children: getDisclaimerText(context), ), ListTile( leading: const Icon(Icons.book_outlined), - title: Text(t.about.dependencies), + title: Text(t.about.info.dependencies), onTap: () { Navigator.push( context, @@ -142,7 +141,7 @@ class AboutPageState extends State { ), ListTile( leading: const Icon(Icons.code_outlined), - title: Text(t.about.sourceCode), + title: Text(t.about.info.sourceCode), onTap: () { launchUrlString( 'https://github.com/digitalfabrik/entitlementcard', diff --git a/frontend/lib/about/language_change.dart b/frontend/lib/about/language_change.dart index 103dff979..d8573140c 100644 --- a/frontend/lib/about/language_change.dart +++ b/frontend/lib/about/language_change.dart @@ -31,7 +31,7 @@ class LanguageChange extends StatelessWidget { messengerState.showSnackBar( SnackBar( backgroundColor: Theme.of(context).colorScheme.primary, - content: Text(t.about.settings.languageNotification, + content: Text(t.about.settings.languageChangeSuccessful, style: TextStyle(color: Theme.of(context).colorScheme.background)), ), ); diff --git a/frontend/lib/about/license_page.dart b/frontend/lib/about/license_page.dart index 6c9c85182..acfd21518 100644 --- a/frontend/lib/about/license_page.dart +++ b/frontend/lib/about/license_page.dart @@ -53,7 +53,7 @@ class CustomLicensePage extends StatelessWidget { return CustomScrollView( slivers: [ - CustomSliverAppBar(title: t.about.licenses(n: licenses.length)), + CustomSliverAppBar(title: t.about.info.licenses(n: licenses.length)), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { diff --git a/frontend/lib/about/section.dart b/frontend/lib/about/section.dart index 00f044d67..652727715 100644 --- a/frontend/lib/about/section.dart +++ b/frontend/lib/about/section.dart @@ -12,8 +12,12 @@ class Section extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.only(top: 20, left: 20, right: 20), - child: Text(headline, style: TextStyle(color: Theme.of(context).colorScheme.primary)), + padding: EdgeInsets.only(top: 16, left: 16, right: 16), + child: Text(headline, + style: Theme.of(context) + .textTheme + .bodySmall + ?.merge(TextStyle(color: Theme.of(context).colorScheme.secondary))), ), Column(children: children), const SizedBox(height: 10), diff --git a/frontend/lib/home/home_page.dart b/frontend/lib/home/home_page.dart index 588b93def..c306cf0be 100644 --- a/frontend/lib/home/home_page.dart +++ b/frontend/lib/home/home_page.dart @@ -51,11 +51,11 @@ class HomePageState extends State { if (buildConfig.featureFlags.verification) AppFlow( IdentificationPage(), - Icons.remove_red_eye_outlined, + Icons.credit_card, (BuildContext context) => t.identification.title, GlobalKey(debugLabel: 'Auth tab key'), ), - AppFlow(const AboutPage(), buildConfig.featureFlags.settings ? Icons.menu : Icons.info_outline, + AppFlow(const AboutPage(), buildConfig.appLocales.length > 1 ? Icons.menu : Icons.info_outline, (BuildContext context) => t.about.title, GlobalKey(debugLabel: 'About tab key')), ]; } diff --git a/frontend/lib/themes.dart b/frontend/lib/themes.dart index 71cebd3eb..7c925b8d3 100644 --- a/frontend/lib/themes.dart +++ b/frontend/lib/themes.dart @@ -16,7 +16,7 @@ ThemeData get lightTheme { ), textTheme: defaultTypography.copyWith( headlineMedium: defaultTypography.headlineMedium?.apply(color: Colors.black87), - headlineSmall: defaultTypography.headlineMedium?.apply(color: Colors.black87), + headlineSmall: defaultTypography.headlineSmall?.apply(color: Colors.black87), titleLarge: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold), bodyLarge: const TextStyle(fontSize: 15.0, fontWeight: FontWeight.normal), bodyMedium: const TextStyle(fontSize: 15.0, color: Color(0xFF505050)), @@ -59,7 +59,7 @@ ThemeData get darkTheme { ), textTheme: defaultTypography.copyWith( headlineMedium: defaultTypography.headlineMedium?.apply(color: Colors.white), - headlineSmall: defaultTypography.headlineMedium?.apply(color: Colors.white), + headlineSmall: defaultTypography.headlineSmall?.apply(color: Colors.white), titleLarge: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold), bodyLarge: const TextStyle(fontSize: 15.0, fontWeight: FontWeight.normal), bodyMedium: const TextStyle(fontSize: 15.0, color: Color(0xFFC6C4C4)), From 31689755292b7454cc3f8fc70920f6bf908c1a38 Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 8 Nov 2023 16:22:40 +0100 Subject: [PATCH 60/60] 1175: revert restructure translations for about --- frontend/assets/l10n/app_de.json | 26 +++++++++++-------------- frontend/assets/l10n/app_en.json | 26 +++++++++++-------------- frontend/lib/about/about_page.dart | 18 ++++++++--------- frontend/lib/about/language_change.dart | 4 ++-- frontend/lib/about/license_page.dart | 2 +- 5 files changed, 33 insertions(+), 43 deletions(-) diff --git a/frontend/assets/l10n/app_de.json b/frontend/assets/l10n/app_de.json index 83b0bb3a1..07f4dc13d 100644 --- a/frontend/assets/l10n/app_de.json +++ b/frontend/assets/l10n/app_de.json @@ -1,16 +1,14 @@ { "about": { + "dependencies": "Software-Bibliotheken", "developmentOptions": "Entwickleroptionen", - "info": { - "dependencies": "Software-Bibliotheken", - "disclaimer": "Haftung, Haftungsausschluss und Impressum", - "headline": "Info", - "licenses": { - "one": "Lizenz", - "other": "Lizenzen" - }, - "privacyDeclaration": "Datenschutzerklärung", - "sourceCode": "Quellcode der App" + "disclaimer": "Haftung, Haftungsausschluss und Impressum", + "infoTitle": "Info", + "languageChange": "Sprache wechseln", + "languageChangeSuccessful": "Ihre Sprache wurde erfolgreich geändert!", + "licenses": { + "one": "Lizenz", + "other": "Lizenzen" }, "moreInformation": "Mehr Informationen", "numberLicenses": { @@ -18,12 +16,10 @@ "other": "$n Lizenzen", "zero": "Keine Lizenzen" }, + "privacyDeclaration": "Datenschutzerklärung", "publisher": "Herausgeber", - "settings": { - "headline": "Einstellungen", - "languageChange": "Sprache wechseln", - "languageChangeSuccessful": "Ihre Sprache wurde erfolgreich geändert!" - }, + "settingsTitle": "Einstellungen", + "sourceCode": "Quellcode der App", "title": "Über" }, "category": { diff --git a/frontend/assets/l10n/app_en.json b/frontend/assets/l10n/app_en.json index e2c3933fc..c2d7aaa4d 100644 --- a/frontend/assets/l10n/app_en.json +++ b/frontend/assets/l10n/app_en.json @@ -1,16 +1,14 @@ { "about": { + "dependencies": "Libraries", "developmentOptions": "Developer options", - "info": { - "dependencies": "Libraries", - "disclaimer": "Liability, disclaimer and imprint", - "headline": "Info", - "licenses": { - "one": "License", - "other": "Licenses" - }, - "privacyDeclaration": "Privacy policy", - "sourceCode": "Source code" + "disclaimer": "Liability, disclaimer and imprint", + "infoTitle": "Info", + "languageChange": "Change language", + "languageChangeSuccessful": "Your language was changed successfully!", + "licenses": { + "one": "License", + "other": "Licenses" }, "moreInformation": "More information", "numberLicenses": { @@ -18,12 +16,10 @@ "other": "$n licenses", "zero": "No licenses" }, + "privacyDeclaration": "Privacy policy", "publisher": "Publisher", - "settings": { - "headline": "Settings", - "languageChange": "Change language", - "languageChangeSuccessful": "Your language was changed successfully!" - }, + "settingsTitle": "Settings", + "sourceCode": "Source code", "title": "About" }, "category": { diff --git a/frontend/lib/about/about_page.dart b/frontend/lib/about/about_page.dart index ce944186c..4e41af45f 100644 --- a/frontend/lib/about/about_page.dart +++ b/frontend/lib/about/about_page.dart @@ -103,10 +103,9 @@ class AboutPageState extends State { thickness: 1, ), Section( - headline: t.about.settings.headline, + headline: t.about.settingsTitle, children: [ - ContentTile( - icon: Icons.language, title: t.about.settings.languageChange, children: [LanguageChange()]), + ContentTile(icon: Icons.language, title: t.about.languageChange, children: [LanguageChange()]), ], ), ]), @@ -114,22 +113,21 @@ class AboutPageState extends State { height: 1, thickness: 1, ), - Section(headline: t.about.info.headline, children: [ - ContentTile( - icon: Icons.copyright, title: t.about.info.licenses(n: 1), children: getCopyrightText(context)), + Section(headline: t.about.infoTitle, children: [ + ContentTile(icon: Icons.copyright, title: t.about.licenses(n: 1), children: getCopyrightText(context)), ListTile( leading: const Icon(Icons.privacy_tip_outlined), - title: Text(t.about.info.privacyDeclaration), + title: Text(t.about.privacyDeclaration), onTap: () => launchUrlString(buildConfig.dataPrivacyPolicyUrl, mode: LaunchMode.externalApplication), ), ContentTile( icon: Icons.info_outline, - title: t.about.info.disclaimer, + title: t.about.disclaimer, children: getDisclaimerText(context), ), ListTile( leading: const Icon(Icons.book_outlined), - title: Text(t.about.info.dependencies), + title: Text(t.about.dependencies), onTap: () { Navigator.push( context, @@ -141,7 +139,7 @@ class AboutPageState extends State { ), ListTile( leading: const Icon(Icons.code_outlined), - title: Text(t.about.info.sourceCode), + title: Text(t.about.sourceCode), onTap: () { launchUrlString( 'https://github.com/digitalfabrik/entitlementcard', diff --git a/frontend/lib/about/language_change.dart b/frontend/lib/about/language_change.dart index d8573140c..eb5eb2b13 100644 --- a/frontend/lib/about/language_change.dart +++ b/frontend/lib/about/language_change.dart @@ -31,8 +31,8 @@ class LanguageChange extends StatelessWidget { messengerState.showSnackBar( SnackBar( backgroundColor: Theme.of(context).colorScheme.primary, - content: Text(t.about.settings.languageChangeSuccessful, - style: TextStyle(color: Theme.of(context).colorScheme.background)), + content: + Text(t.about.languageChangeSuccessful, style: TextStyle(color: Theme.of(context).colorScheme.background)), ), ); Navigator.pop(context); diff --git a/frontend/lib/about/license_page.dart b/frontend/lib/about/license_page.dart index acfd21518..6c9c85182 100644 --- a/frontend/lib/about/license_page.dart +++ b/frontend/lib/about/license_page.dart @@ -53,7 +53,7 @@ class CustomLicensePage extends StatelessWidget { return CustomScrollView( slivers: [ - CustomSliverAppBar(title: t.about.info.licenses(n: licenses.length)), + CustomSliverAppBar(title: t.about.licenses(n: licenses.length)), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) {