Added internationalization, Updated tests

main
henryhdr 2024-06-14 15:54:55 +02:00
parent 26c8cd977e
commit daa79922ea
16 changed files with 584 additions and 307 deletions

3
l10n.yaml 100644
View File

@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_de.arb
output-localization-file: app_localizations.dart

View File

@ -0,0 +1,61 @@
{
"title": "Zinseszinsrechner",
"language_english": "Englisch",
"language_german": "Deutsch",
"choose_language": "Sprache auswählen",
"initial_capital": "Anfangskapital",
"initial_capital_tooltiptext": "Das Anfangskapital ist der Betrag, den Sie zu Beginn Ihrer Anlage haben.",
"currency": "€",
"amount": "{value}€",
"@amount": {
"value": {
"type": "int"
}
},
"monthly_savings_rate": "Monatliche Sparrate",
"monthly_savings_rate_tooltiptext": "Die monatliche Sparrate ist der Betrag, den Sie jeden Monat zu Ihrer Investition hinzufügen.",
"interest_rate": "Jährlicher Zinssatz",
"interest_rate_tooltiptext": "Der jährliche Zinssatz ist der Prozentsatz, zu dem Ihr investiertes Kapital jedes Jahr wächst.",
"investment_period": "Anlagezeitraum",
"investment_period_tooltiptext": "Der Anlagezeitraum ist die Zeitspanne, für die Sie planen, Ihr Geld anzulegen.",
"years": "Jahre",
"yearly": "jährlich",
"monthly": "monatlich",
"calculate": "Berechnen",
"invalid_input": "Ungültige Eingabe",
"milestone_text1": "Smartphone\nPreis:",
"milestone_text2": "eBike\nPreis:",
"milestone_text3": "Weltreise\nPreis:",
"milestone_text4": "Sportwagen\nPreis:",
"milestone_text5": "150qm Einfamilienhaus\nPreis:",
"final_capital": "Endkapital",
"deposits": "Einzahlungen",
"interest_received": "Erhaltene Zinsen",
"result_text": "Wenn du über einen Zeitraum von {investment_period} Jahren monatlich {monthly_savings_rate}€ mit einem Zinssatz von {interest_rate}% investierst, erreichst du am Ende ein Endkapital von {final_capital}€. Dieses setzt sich aus Einzahlungen von {invested_money}€ und Zinsen bzw. Kapitalerträgen in Höhe von {compound_interest}€ zusammen.",
"@result_text": {
"investment_period": {
"type" : "int"
},
"monthly_savings_rate": {
"type" : "int"
},
"interest_rate": {
"type" : "int"
},
"final_capital": {
"type" : "int"
},
"invested_money": {
"type" : "int"
},
"compound_interest": {
"type" : "int"
}
},
"graphic": "Grafik",
"milestones": "Meilensteine",
"payout_interval": "Ausschüttungsintervall",
"payout_interval_tooltiptext": "Das Ausschüttungsintervall bezieht sich darauf, wie oft die Erträge aus Ihrer Investition ausgeschüttet werden.",
"year": "Jahr",
"currency_written_out": "Euro"
}

View File

@ -0,0 +1,36 @@
{
"title": "Compound interest calculator",
"language_english": "English",
"language_german": "German",
"choose_language": "Choose language",
"initial_capital": "Initial capital",
"initial_capital_tooltiptext": "The initial capital is the amount you have at the start of your investment.",
"currency": "$",
"amount": "${value}",
"monthly_savings_rate": "Monthly savings rate",
"monthly_savings_rate_tooltiptext": "The monthly savings rate is the amount you add to your investment each month.",
"interest_rate": "Annual interest rate",
"interest_rate_tooltiptext": "The annual interest rate is the percentage at which your invested capital grows each year.",
"investment_period": "Investment period",
"investment_period_tooltiptext": "The investment period is the length of time for which you plan to invest your money.",
"years": "Years",
"yearly": "yearly",
"monthly": "monthly",
"calculate": "Calculate",
"invalid_input": "Invalid input",
"milestone_text1": "Smartphone\nPrice: ",
"milestone_text2": "eBike\nPrice: ",
"milestone_text3": "World Travel\nPrice: ",
"milestone_text4": "Sports car\nPrice: ",
"milestone_text5": "150sqm single family house\nPrice: ",
"final_capital": "Final capital",
"deposits": "Deposits",
"interest_received": "Interest received",
"result_text": "If you invest ${monthly_savings_rate} per month over a period of {investment_period} years at an interest rate of {interest_rate}%, you will end up with a final capital of ${final_capital}. This consists of deposits of ${invested_money} and interest or capital gains of ${compound_interest}.",
"graphic": "Graphic",
"milestones": "Milestones",
"payout_interval": "Payout interval",
"payout_interval_tooltiptext": "The payout interval refers to how often the income from your investment is distributed.",
"year": "Year",
"currency_written_out": "Dollar"
}

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_application_1/calculator.dart';
import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/utils.dart';
@ -7,31 +9,60 @@ import 'package:flutter_application_1/widgets/input_widget.dart';
import 'package:flutter_application_1/widgets/interval_widget.dart';
import 'package:flutter_application_1/widgets/result_widget.dart';
import 'package:flutter_application_1/widgets/error_widget.dart';
import 'package:flutter_application_1/widgets/language_switcher_widget.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
Locale _locale = const Locale('de'); // Standard-Locale
void _changeLanguage(Locale locale) {
setState(() {
_locale = locale;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Zinseszinsrechner',
title: 'Compound interest calculator',
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en'), // English
Locale('de'), // Deutsch
],
locale: _locale,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: CupertinoColors.white, background: CupertinoColors.white),
useMaterial3: true,
),
home: const MyHomePage(title: 'Zinseszinsrechner',),
home: MyHomePage(
title: 'Compound interest calculator',
onLocaleChanged: _changeLanguage, // Übergibt die Sprachwechsel-Funktion an die Startseite
),
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
const MyHomePage({super.key, required this.title, required this.onLocaleChanged});
final String title;
final Function(Locale) onLocaleChanged;
@override
State<MyHomePage> createState() => _MyHomePageState();
@ -162,9 +193,13 @@ class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Stack(
children: [
SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(20.0),
@ -172,42 +207,42 @@ class _MyHomePageState extends State<MyHomePage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
InputWidget(
label: 'Anfangskapital',
label: localizations.initial_capital,
controller: _initialCapitalController,
focusNode: _initialCapitalFocusNode,
isValid: _isInitialCapitalEntered,
suffixText: '',
tooltipText: 'Das Anfangskapital ist der Betrag, den Sie zu Beginn Ihrer Anlage haben.'
suffixText: localizations.currency,
tooltipText: localizations.initial_capital_tooltiptext
),
InputWidget(
label: 'Monatliche Sparrate',
label: localizations.monthly_savings_rate,
controller: _monthlySavingsRateController,
focusNode: _monthlySavingsRateFocusNode,
isValid: _isMonthlySavingsRateEntered,
suffixText: '',
tooltipText: 'Die monatliche Sparrate ist der Betrag, den Sie jeden Monat zu Ihrer Investition hinzufügen.'
suffixText: localizations.currency,
tooltipText: localizations.monthly_savings_rate_tooltiptext
),
InputWidget(
label: 'Jährlicher Zinssatz',
label: localizations.interest_rate,
controller: _interestRateController,
focusNode: _interestRateFocusNode,
isValid: _isInterestRateEntered,
suffixText: '%',
tooltipText: 'Der jährliche Zinssatz ist der Prozentsatz, zu dem Ihr investiertes Kapital jedes Jahr wächst.'
tooltipText: localizations.interest_rate_tooltiptext
),
InputWidget(
label: 'Anlagezeitraum',
label: localizations.investment_period,
controller: _timeController,
focusNode: _timeFocusNode,
isValid: _isTimeEntered,
suffixText: 'Jahre',
tooltipText: 'Der Anlagezeitraum ist die Zeitspanne, für die Sie planen, Ihr Geld anzulegen.'
suffixText: localizations.years,
tooltipText: localizations.investment_period_tooltiptext
),
IntervalWidget(
selectedInterval: translateInterval(_payoutInterval),
selectedInterval: _payoutInterval == PayoutInterval.yearly ? localizations.yearly : localizations.monthly,
onChanged: (newInterval) {
setState(() {
_payoutInterval = newInterval == 'jährlich' ? PayoutInterval.yearly : PayoutInterval.monthly;
_payoutInterval = newInterval == localizations.yearly ? PayoutInterval.yearly : PayoutInterval.monthly;
});
},
),
@ -246,9 +281,9 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
),
child: const Text(
'Berechnen',
style: TextStyle(
child: Text(
localizations.calculate,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
@ -256,24 +291,34 @@ class _MyHomePageState extends State<MyHomePage> {
const SizedBox(height: 20),
if(_isCalculated == CalculationPerformed.yes)
ResultWidget(
compoundInterest: '$_compoundInterest',
investedMoney: '$_investedMoney',
time: '$_time',
monthlySavingsRate: '$_monthlySavingsRate',
interestRate: '$_interestRate',
compoundInterest: _compoundInterest.toStringAsFixed(0),
investedMoney: _investedMoney.toStringAsFixed(0),
time: _time.toStringAsFixed(0),
monthlySavingsRate: _monthlySavingsRate.toStringAsFixed(0),
interestRate: _interestRate.toStringAsFixed(0),
payoutInterval: _payoutInterval,
investedMoneyList: _investedMoneyList,
compoundInterestList: _compoundInterestList,
),
if(_isCalculated == CalculationPerformed.no)
const ErrWidget(
errorMessage: 'Ungültige Eingabe',
ErrWidget(
errorMessage: localizations.invalid_input,
),
],
),
)
)
)
),
),
Positioned(
top: MediaQuery.of(context).size.height / 2 - 20,
right: 10,
child: LanguageSwitcher(
currentLocale: Localizations.localeOf(context),
onLocaleChanged: widget.onLocaleChanged,
),
),
],
),
),
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_application_1/widgets/chart_widget.dart';
// Widget für die Seite, die das gestapelte Säulendiagramm anzeigt
@ -15,6 +16,8 @@ class ChartPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
@ -34,9 +37,9 @@ class ChartPage extends StatelessWidget {
Navigator.pop(context); // Zurück zur vorherigen Seite
},
),
const Text(
'Grafik',
style: TextStyle(fontWeight: FontWeight.bold),
Text(
localizations.graphic,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 40),
],
@ -78,16 +81,16 @@ class ChartPage extends StatelessWidget {
),
children: [
// Tabellenkopf
const TableRow(
decoration: BoxDecoration(
color: CupertinoColors.darkBackgroundGray,
TableRow(
decoration: const BoxDecoration(
color: CupertinoColors.black,
),
children: [
Padding(
padding: EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
child: Text(
'Jahr',
style: TextStyle(
localizations.year,
style: const TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
@ -95,10 +98,10 @@ class ChartPage extends StatelessWidget {
),
),
Padding(
padding: EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
child: Text(
'Einzahlungen',
style: TextStyle(
localizations.deposits,
style: const TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
@ -106,10 +109,10 @@ class ChartPage extends StatelessWidget {
),
),
Padding(
padding: EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
child: Text(
'Zinsen',
style: TextStyle(
localizations.interest_received,
style: const TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
@ -117,10 +120,10 @@ class ChartPage extends StatelessWidget {
),
),
Padding(
padding: EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
child: Text(
'Endkapital',
style: TextStyle(
localizations.final_capital,
style: const TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
@ -144,7 +147,7 @@ class ChartPage extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${investedMoneyList[i]}',
localizations.amount(investedMoneyList[i]),
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
@ -152,7 +155,7 @@ class ChartPage extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${compoundInterestList[i]}',
localizations.amount(compoundInterestList[i]),
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
@ -160,7 +163,7 @@ class ChartPage extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${compoundInterestList[i] + investedMoneyList[i]}',
localizations.amount(compoundInterestList[i] + investedMoneyList[i]),
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),

View File

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_application_1/widgets/milestone_timeline_widget.dart';
// Widget für die Seite, die die Meilenstein-Timeline anzeigt
@ -32,9 +33,9 @@ class MilestonePage extends StatelessWidget {
Navigator.pop(context); // Zurück zur vorherigen Seite
},
),
const Text(
'Meilensteine',
style: TextStyle(fontWeight: FontWeight.bold),
Text(
AppLocalizations.of(context)!.milestones,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 40),
],

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/enums.dart';
// Rundet den Wert im Textfeld-Controller auf die nächste ganze Zahl
void roundToInteger(TextEditingController controller) {
@ -21,13 +20,3 @@ void restoreDefaultValuesIfEmpty(TextEditingController controller) {
controller.text = '0';
}
}
// Übersetzt das Auszahlungsintervall
String translateInterval(PayoutInterval interval) {
switch (interval) {
case PayoutInterval.yearly:
return 'jährlich';
case PayoutInterval.monthly:
return 'monatlich';
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
// Widget für die Erstellung eines gestapelten Säulendiagramms
@ -14,16 +15,18 @@ class StackedColumnChart extends StatelessWidget {
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
return SfCartesianChart(
legend: const Legend(
isVisible: true,
position: LegendPosition.top,
),
primaryXAxis: const CategoryAxis(
title: AxisTitle(text: 'Jahr'),
primaryXAxis: CategoryAxis(
title: AxisTitle(text: localizations.year),
),
primaryYAxis: const NumericAxis(
title: AxisTitle(text: 'Euro'),
primaryYAxis: NumericAxis(
title: AxisTitle(text: localizations.currency_written_out),
),
series: <CartesianSeries>[
// Serie für die Einzahlungen (untere Werte)
@ -31,7 +34,7 @@ class StackedColumnChart extends StatelessWidget {
dataSource: _getLowerChartData(),
xValueMapper: (SalesData sales, _) => sales.year,
yValueMapper: (SalesData sales, _) => sales.value,
name: 'Einzahlungen',
name: localizations.deposits,
color: CupertinoColors.systemRed.highContrastColor,
),
// Serie für die Zinsen (obere Werte)
@ -39,7 +42,7 @@ class StackedColumnChart extends StatelessWidget {
dataSource: _getUpperChartData(),
xValueMapper: (SalesData sales, _) => sales.year,
yValueMapper: (SalesData sales, _) => sales.value,
name: 'Zinsen',
name: localizations.interest_received,
color: CupertinoColors.systemBlue.highContrastColor,
),
],

View File

@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/utils.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// Widget zur Auswahl des Ausschüttungsintervalls
class IntervalWidget extends StatefulWidget {
@ -41,13 +40,16 @@ class IntervalWidgetState extends State<IntervalWidget> {
}
OverlayEntry _createOverlayEntry() {
final localizations = AppLocalizations.of(context)!;
final isMobile = Theme.of(context).platform == TargetPlatform.iOS || Theme.of(context).platform == TargetPlatform.android;
return OverlayEntry(
builder: (context) => Positioned(
width: 160,
child: CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
offset: const Offset(0.0, 60),
offset: Offset(0.0, isMobile ? 70 : 60),
child: Material(
color: CupertinoColors.extraLightBackgroundGray,
borderRadius: BorderRadius.circular(5),
@ -57,34 +59,37 @@ class IntervalWidgetState extends State<IntervalWidget> {
children: [
// ListTile für jährliches Ausschüttungsintervall
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 15.0),
title: Text(
translateInterval(PayoutInterval.yearly),
localizations.yearly,
style: const TextStyle(
color: CupertinoColors.black,
fontSize: 14,
fontSize: 16,
),
),
trailing: widget.selectedInterval == translateInterval(PayoutInterval.yearly)
? const Icon(CupertinoIcons.checkmark_alt, color: CupertinoColors.black)
trailing: widget.selectedInterval == localizations.yearly
? const Icon(CupertinoIcons.checkmark_alt, color: CupertinoColors.black, size: 20,)
: null,
onTap: () {
widget.onChanged(translateInterval(PayoutInterval.yearly));
widget.onChanged(localizations.yearly);
_toggleDropdown();
},
),
// ListTile für monatliches Ausschüttungsintervall
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 15.0),
title: Text(
translateInterval(PayoutInterval.monthly),
localizations.monthly,
style: const TextStyle(
color: CupertinoColors.black,
fontSize: 16,
),
),
trailing: widget.selectedInterval == translateInterval(PayoutInterval.monthly)
? const Icon(CupertinoIcons.checkmark_alt, color: CupertinoColors.black)
trailing: widget.selectedInterval == localizations.monthly
? const Icon(CupertinoIcons.checkmark_alt, color: CupertinoColors.black, size: 20,)
: null,
onTap: () {
widget.onChanged(translateInterval(PayoutInterval.monthly));
widget.onChanged(localizations.monthly);
_toggleDropdown();
},
),
@ -98,34 +103,36 @@ class IntervalWidgetState extends State<IntervalWidget> {
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
return CompositedTransformTarget(
link: _layerLink,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
Row(
children: [
Text(
'Ausschüttungsintervall',
style: TextStyle(
localizations.payout_interval,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
SizedBox(width: 5),
const SizedBox(width: 5),
// Tooltip mit Erklärung zum Ausschüttungsintervall
Tooltip(
message: 'Das Ausschüttungsintervall bezieht sich darauf, wie oft die Erträge aus Ihrer Investition ausgeschüttet werden.',
message: localizations.payout_interval_tooltiptext,
triggerMode: TooltipTriggerMode.tap,
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: CupertinoColors.black,
borderRadius: BorderRadius.all(Radius.circular(5))
),
textStyle: TextStyle(
textStyle: const TextStyle(
color: CupertinoColors.white,
),
margin: EdgeInsets.all(20),
child: Icon(CupertinoIcons.question_circle_fill, size: 15),
margin: const EdgeInsets.all(20),
child: const Icon(CupertinoIcons.question_circle_fill, size: 15),
),
]
),
@ -135,6 +142,9 @@ class IntervalWidgetState extends State<IntervalWidget> {
child: ElevatedButton(
onPressed: _toggleDropdown,
style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
const EdgeInsets.symmetric(horizontal: 15.0),
),
backgroundColor: MaterialStateProperty.all<Color>(CupertinoColors.extraLightBackgroundGray),
foregroundColor: MaterialStateProperty.all<Color>(CupertinoColors.black),
overlayColor: MaterialStateProperty.all<Color>(CupertinoColors.extraLightBackgroundGray),
@ -150,9 +160,13 @@ class IntervalWidgetState extends State<IntervalWidget> {
Expanded(
child: Text(
widget.selectedInterval, // Text des ausgewählten Intervalls
style: const TextStyle(
color: CupertinoColors.black,
fontSize: 16,
),
),
Icon(isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down),
),
Icon(isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 20,),
],
),
),

View File

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LanguageSwitcher extends StatefulWidget {
final Locale currentLocale;
final Function(Locale) onLocaleChanged;
const LanguageSwitcher({
super.key,
required this.currentLocale,
required this.onLocaleChanged,
});
@override
LanguageSwitcherState createState() => LanguageSwitcherState();
}
class LanguageSwitcherState extends State<LanguageSwitcher> {
late Locale _selectedLocale;
@override
void initState() {
super.initState();
_selectedLocale = widget.currentLocale;
}
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
final isMobile = Theme.of(context).platform == TargetPlatform.iOS || Theme.of(context).platform == TargetPlatform.android;
return Theme(
data: ThemeData.light().copyWith(
popupMenuTheme: PopupMenuThemeData(
color: CupertinoColors.extraLightBackgroundGray,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
textStyle: const TextStyle(color: CupertinoColors.black),
),
tooltipTheme: TooltipThemeData(
decoration: BoxDecoration(
color: CupertinoColors.black,
borderRadius: BorderRadius.circular(4.0),
),
textStyle: const TextStyle(color: CupertinoColors.white),
),
),
child: Container(
decoration: BoxDecoration(
color: CupertinoColors.extraLightBackgroundGray,
borderRadius: BorderRadius.circular(100.0),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 8.0,
offset: Offset(0, 4),
),
],
),
child: PopupMenuButton<Locale>(
offset: Offset(0.0, isMobile ? 55 : 45),
onSelected: (Locale locale) {
setState(() {
_selectedLocale = locale;
});
widget.onLocaleChanged(locale);
},
tooltip: localizations.choose_language,
icon: const Icon(
CupertinoIcons.globe,
color: CupertinoColors.black,
),
itemBuilder: (BuildContext context) {
return [
PopupMenuItem<Locale>(
value: const Locale('en'),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(AppLocalizations.of(context)!.language_english),
if (_selectedLocale.languageCode == 'en') const Icon(CupertinoIcons.checkmark_alt, size: 20),
],
),
),
PopupMenuItem<Locale>(
value: const Locale('de'),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(AppLocalizations.of(context)!.language_german),
if (_selectedLocale.languageCode == 'de') const Icon(CupertinoIcons.checkmark_alt, size: 20),
],
),
),
];
},
),
),
);
}
}

View File

@ -39,7 +39,7 @@ class MilestoneTimeline extends StatelessWidget {
children: [
Text(
milestoneEmoji,
style: const TextStyle(fontSize: 20),
style: const TextStyle(fontSize: 30),
),
const SizedBox(height: 5),
Text(

View File

@ -1,9 +1,9 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/pages/chart_page.dart';
import 'package:flutter_application_1/pages/milestone_page.dart';
import 'package:flutter_application_1/utils.dart';
import 'package:flutter_application_1/widgets/custom_image_button_widget.dart';
// Widget zur Anzeige der Ergebnisse der Berechnungen und zur Navigation zu anderen Seiten
@ -31,22 +31,29 @@ class ResultWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
List<Map<String, dynamic>> milestoneList = [
{'value': 700.0, 'emoji': '📱', 'text': 'Smartphone\nPreis: 700€'},
{'value': 3250.0, 'emoji': '🚲', 'text': 'eBike\nPreis: 3250€'},
{'value': 20000.0, 'emoji': '🌎', 'text': 'Weltreise\nPreis: 20000€'},
{'value': 100000.0, 'emoji': '🏎️', 'text': 'Sportwagen\nPreis: 100000€'},
{'value': 350000.0, 'emoji': '🏡', 'text': '150qm Einfamilienhaus\nPreis: 350000€'},
{'value': 700.0, 'emoji': '📱', 'text': localizations.milestone_text1 + localizations.amount(700)},
{'value': 3250.0, 'emoji': '🚲', 'text': localizations.milestone_text2 + localizations.amount(3250)},
{'value': 20000.0, 'emoji': '🌎', 'text': localizations.milestone_text3 + localizations.amount(20000)},
{'value': 100000.0, 'emoji': '🏎️', 'text': localizations.milestone_text4 + localizations.amount(100000)},
{'value': 350000.0, 'emoji': '🏡', 'text': localizations.milestone_text5 + localizations.amount(350000)},
];
return Column(
children: <Widget>[
_buildResultRow('Endkapital:', '$compoundInterest'),
_buildResultRow('Einzahlungen:', '$investedMoney'),
_buildResultRow('Erhaltene Zinsen:', '${double.parse(compoundInterest) + double.parse(investedMoney)}'),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(child: _buildResultBox(localizations.final_capital, localizations.amount(compoundInterest))),
Expanded(child: _buildResultBox(localizations.deposits, localizations.amount(investedMoney))),
Expanded(child: _buildResultBox(localizations.interest_received, localizations.amount(double.parse(compoundInterest) - double.parse(investedMoney)))),
],
),
const SizedBox(height: 10),
Text(
'Wenn du über einen Zeitraum von $time Jahren ${translateInterval(payoutInterval)} $monthlySavingsRate€ mit einem Zinssatz von $interestRate% investierst, erreichst du am Ende ein Endkapital von $compoundInterest€. Dieses setzt sich aus Einzahlungen von $investedMoney€ und Zinsen bzw. Kapitalerträgen in Höhe von ${double.parse(compoundInterest) - double.parse(investedMoney)}€ zusammen.'
localizations.result_text(double.parse(compoundInterest) - double.parse(investedMoney), compoundInterest, interestRate, investedMoney, time, monthlySavingsRate)
),
const SizedBox(height: 20),
CustomImageButton(
@ -62,9 +69,9 @@ class ResultWidget extends StatelessWidget {
);
},
backgroundImage: 'assets/images/button_bg1.jpg',
child: const Text(
'Grafik',
style: TextStyle(color: CupertinoColors.white, fontSize: 20),
child: Text(
localizations.graphic,
style: const TextStyle(color: CupertinoColors.white, fontSize: 20),
),
),
const SizedBox(height: 20),
@ -82,41 +89,41 @@ class ResultWidget extends StatelessWidget {
);
},
backgroundImage: 'assets/images/button_bg2.jpg',
child: const Text(
'Meilensteine',
style: TextStyle(color: CupertinoColors.white, fontSize: 20),
child: Text(
localizations.milestones,
style: const TextStyle(color: CupertinoColors.white, fontSize: 20),
),
),
],
);
}
// Widget zum Aufbau einer Ergebniszeile
Widget _buildResultRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// Widget zum Aufbau einer Ergebnisbox
Widget _buildResultBox(String label, String value) {
return Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.symmetric(horizontal: 5.0),
decoration: BoxDecoration(
color: CupertinoColors.extraLightBackgroundGray,
borderRadius: BorderRadius.circular(5),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
Text(
label,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
),
const SizedBox(width: 20),
Flexible(
child: Text(
const SizedBox(height: 5),
Text(
value,
textAlign: TextAlign.end,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
],
),
);

View File

@ -70,19 +70,24 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
intl:
dependency: transitive
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.19.0"
version: "0.18.1"
leak_tracker:
dependency: transitive
description:

View File

@ -36,6 +36,9 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
syncfusion_flutter_charts: ^25.1.42+1
flutter_localizations:
sdk: flutter
intl: any
dev_dependencies:
flutter_test:
@ -53,7 +56,7 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
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.

View File

@ -68,10 +68,5 @@ void main() {
restoreDefaultValuesIfEmpty(controller);
expect(controller.text, '5');
});
test('translateInterval should return correct translation', () {
expect(translateInterval(PayoutInterval.yearly), 'jährlich');
expect(translateInterval(PayoutInterval.monthly), 'monatlich');
});
});
}

View File

@ -1,9 +1,10 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/pages/chart_page.dart';
import 'package:flutter_application_1/pages/milestone_page.dart';
import 'package:flutter_application_1/widgets/chart_widget.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/widgets/error_widget.dart';
import 'package:flutter_application_1/widgets/input_widget.dart';
import 'package:flutter_application_1/widgets/interval_widget.dart';
@ -104,103 +105,46 @@ void main() {
expect(controller.text, '12345');
});
});
group('Interal Widget Tests', () {
testWidgets('Initial state', (WidgetTester tester) async {
const selectedInterval = 'jährlich';
group('Interval Widget Tests', () {
testWidgets('Shows correct localized texts and handles dropdown selection', (WidgetTester tester) async {
const Locale testLocale = Locale('en');
final localizations = await AppLocalizations.delegate.load(testLocale);
String selectedInterval = localizations.yearly;
await tester.pumpWidget(
MaterialApp(
locale: testLocale,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: Scaffold(
body: IntervalWidget(
selectedInterval: selectedInterval,
onChanged: (newInterval) {},
),
),
),
);
expect(find.text('Ausschüttungsintervall'), findsOneWidget);
expect(find.text(selectedInterval), findsOneWidget);
expect(find.byIcon(Icons.keyboard_arrow_down), findsOneWidget);
});
testWidgets('Tapping the button expands the dropdown', (WidgetTester tester) async {
const selectedInterval = 'jährlich';
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: IntervalWidget(
selectedInterval: selectedInterval,
onChanged: (newInterval) {
onChanged: (String newValue) {
selectedInterval = newValue;
},
),
),
),
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.text(localizations.payout_interval), findsOneWidget);
expect(find.text(localizations.payout_interval_tooltiptext), findsNothing);
expect(find.byType(ListTile), findsNWidgets(2));
expect(find.byIcon(CupertinoIcons.checkmark_alt), findsOneWidget);
});
testWidgets('Selecting an interval updates the state', (WidgetTester tester) async {
const selectedInterval = 'jährlich';
var currentInterval = selectedInterval;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: IntervalWidget(
selectedInterval: selectedInterval,
onChanged: (newInterval) {
currentInterval = newInterval;
},
),
),
),
);
expect(find.text(localizations.yearly), findsOneWidget);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
await tester.tap(find.text('monatlich'));
await tester.pump();
expect(currentInterval, 'monatlich');
expect(find.byType(ListTile), findsNothing);
expect(find.byIcon(Icons.keyboard_arrow_down), findsOneWidget);
});
});
group('Chart Widget Tests', () {
testWidgets('Displays chart correctly', (WidgetTester tester) async {
final List<double> lowerValues = [100, 200, 300, 400];
final List<double> upperValues = [50, 150, 250, 350];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StackedColumnChart(
lowerValues: lowerValues,
upperValues: upperValues,
),
),
),
);
expect(find.byType(SfCartesianChart), findsOneWidget);
expect(find.text('Einzahlungen'), findsOneWidget);
expect(find.text('Zinsen'), findsOneWidget);
expect(find.byType(CategoryAxis), findsOneWidget);
expect(find.byType(NumericAxis), findsOneWidget);
expect(find.text('Einzahlungen'), findsOneWidget);
expect(find.text('Zinsen'), findsOneWidget);
await tester.pumpAndSettle();
expect(find.text(localizations.yearly), findsNWidgets(2)); // Eins im Button und eins im Dropdown-Menü
expect(find.text(localizations.monthly), findsOneWidget);
await tester.tap(find.text(localizations.monthly).last);
expect(selectedInterval, localizations.monthly);
});
});
group('Milestone Timeline Widget Tests', () {
@ -232,20 +176,30 @@ void main() {
});
});
group('Result Widget Tests', () {
testWidgets('Displays result values correctly', (WidgetTester tester) async {
const compoundInterest = '1000';
const investedMoney = '500';
const time = '5';
testWidgets('Shows correct localized texts', (WidgetTester tester) async {
const Locale testLocale = Locale('en');
final localizations = await AppLocalizations.delegate.load(testLocale);
const compoundInterest = '12000';
const investedMoney = '10000';
const time = '10';
const monthlySavingsRate = '100';
const interestRate = '5';
const payoutInterval = PayoutInterval.monthly;
final List<double> investedMoneyList = [100, 200, 300, 400, 500];
final List<double> compoundInterestList = [100, 200, 300, 400, 1000];
const payoutInterval = PayoutInterval.yearly;
final List<double> investedMoneyList = [1000, 2000, 3000, 4000, 5000];
final List<double> compoundInterestList = [1100, 2300, 3600, 5000, 6600];
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ResultWidget(
locale: testLocale,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: Scaffold(
body: ResultWidget(
compoundInterest: compoundInterest,
investedMoney: investedMoney,
time: time,
@ -259,63 +213,25 @@ void main() {
),
);
expect(find.text('Endkapital:'), findsOneWidget);
expect(find.text('$compoundInterest'), findsOneWidget);
expect(find.text('Einzahlungen:'), findsOneWidget);
expect(find.text('$investedMoney'), findsOneWidget);
expect(find.text('Erhaltene Zinsen:'), findsOneWidget);
expect(find.text('${double.parse(compoundInterest) + double.parse(investedMoney)}'), findsOneWidget);
});
expect(find.text(localizations.final_capital), findsOneWidget);
expect(find.text(localizations.amount(compoundInterest)), findsOneWidget);
expect(find.text(localizations.deposits), findsOneWidget);
expect(find.text(localizations.amount(investedMoney)), findsOneWidget);
expect(find.text(localizations.interest_received), findsOneWidget);
expect(find.text(localizations.amount(double.parse(compoundInterest) - double.parse(investedMoney))), findsOneWidget);
testWidgets('Navigates to ChartPage on "Grafik" button press', (WidgetTester tester) async {
final List<double> investedMoneyList = [100, 200, 300, 400, 500];
final List<double> compoundInterestList = [100, 200, 300, 400, 1000];
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ResultWidget(
compoundInterest: '1000',
investedMoney: '500',
time: '5',
monthlySavingsRate: '100',
interestRate: '5',
payoutInterval: PayoutInterval.monthly,
investedMoneyList: investedMoneyList,
compoundInterestList: compoundInterestList,
),
),
),
final resultText = localizations.result_text(
double.parse(compoundInterest) - double.parse(investedMoney),
compoundInterest,
interestRate,
investedMoney,
time,
monthlySavingsRate,
);
expect(find.text(resultText), findsOneWidget);
await tester.tap(find.text('Grafik'));
await tester.pumpAndSettle();
expect(find.byType(ChartPage), findsOneWidget);
});
testWidgets('Navigates to MilestonePage on "Meilensteine" button press', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: ResultWidget(
compoundInterest: '1000',
investedMoney: '500',
time: '5',
monthlySavingsRate: '100',
interestRate: '5',
payoutInterval: PayoutInterval.monthly,
investedMoneyList: [],
compoundInterestList: [],
),
),
),
);
await tester.tap(find.text('Meilensteine'));
await tester.pumpAndSettle();
expect(find.byType(MilestonePage), findsOneWidget);
expect(find.text(localizations.graphic), findsOneWidget);
expect(find.text(localizations.milestones), findsOneWidget);
});
});
@ -337,4 +253,97 @@ void main() {
expect(find.text(errorMessage), findsOneWidget);
});
});
group('Chart Page Tests', () {
testWidgets('Shows correct localized texts and table data', (WidgetTester tester) async {
const Locale testLocale = Locale('en');
final localizations = await AppLocalizations.delegate.load(testLocale);
final investedMoneyList = [1000.0, 2000.0, 3000.0, 4000.0, 5000.0];
final compoundInterestList = [1100.0, 2200.0, 3300.0, 4400.0, 5500.0];
await tester.pumpWidget(
MaterialApp(
locale: testLocale,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: ChartPage(
investedMoneyList: investedMoneyList,
compoundInterestList: compoundInterestList,
),
),
);
expect(find.text(localizations.graphic), findsOneWidget);
expect(find.byType(SfCartesianChart), findsOneWidget);
expect(find.text(localizations.year), findsOneWidget);
expect(find.text(localizations.deposits), findsNWidgets(2));
expect(find.text(localizations.interest_received), findsNWidgets(2));
expect(find.text(localizations.final_capital), findsOneWidget);
for (int i = 0; i < investedMoneyList.length; i++) {
expect(find.text('${i + 1}'), findsOneWidget);
expect(find.text(localizations.amount(investedMoneyList[i])), findsOneWidget);
expect(find.text(localizations.amount(compoundInterestList[i])), findsOneWidget);
expect(find.text(localizations.amount(compoundInterestList[i] + investedMoneyList[i])), findsOneWidget);
}
await tester.tap(find.byIcon(CupertinoIcons.chevron_left));
await tester.pumpAndSettle();
expect(find.byIcon(CupertinoIcons.chevron_left), findsNothing);
});
});
group('Milestone Page Tests', () {
testWidgets('Shows correct localized texts and milestone data', (WidgetTester tester) async {
const Locale testLocale = Locale('en');
final localizations = await AppLocalizations.delegate.load(testLocale);
const compoundInterest = '50000';
const investedMoney = '30000';
List<Map<String, dynamic>> milestoneList = [
{'value': 700.0, 'emoji': '📱', 'text': localizations.milestone_text1 + localizations.amount(700)},
{'value': 3250.0, 'emoji': '🚲', 'text': localizations.milestone_text2 + localizations.amount(3250)},
{'value': 20000.0, 'emoji': '🌎', 'text': localizations.milestone_text3 + localizations.amount(20000)},
{'value': 100000.0, 'emoji': '🏎️', 'text': localizations.milestone_text4 + localizations.amount(100000)},
{'value': 350000.0, 'emoji': '🏡', 'text': localizations.milestone_text5 + localizations.amount(350000)},
];
await tester.pumpWidget(
MaterialApp(
locale: testLocale,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: MilestonePage(
compoundInterest: compoundInterest,
investedMoney: investedMoney,
milestoneList: milestoneList,
),
),
);
expect(find.text(localizations.milestones), findsOneWidget);
expect(find.byType(MilestoneTimeline), findsOneWidget);
for (var milestone in milestoneList) {
expect(find.text(milestone['emoji']), findsOneWidget);
expect(find.text(milestone['text']), findsOneWidget);
}
await tester.tap(find.byIcon(CupertinoIcons.chevron_left));
await tester.pumpAndSettle();
expect(find.byIcon(CupertinoIcons.chevron_left), findsNothing);
});
});
}