diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..db370e6 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_de.arb +output-localization-file: app_localizations.dart \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb new file mode 100644 index 0000000..a2a206f --- /dev/null +++ b/lib/l10n/app_de.arb @@ -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" +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..99806a5 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -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" +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 6090760..8c41c44 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 { + 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 createState() => _MyHomePageState(); @@ -162,9 +193,13 @@ class _MyHomePageState extends State { @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 { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ 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 { ), ), ), - child: const Text( - 'Berechnen', - style: TextStyle( + child: Text( + localizations.calculate, + style: const TextStyle( fontWeight: FontWeight.bold, ), ), @@ -256,24 +291,34 @@ class _MyHomePageState extends State { 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, + ), + ), + ], + ), + ), ); } } \ No newline at end of file diff --git a/lib/pages/chart_page.dart b/lib/pages/chart_page.dart index ec869e7..3b21246 100644 --- a/lib/pages/chart_page.dart +++ b/lib/pages/chart_page.dart @@ -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: [ @@ -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, ), diff --git a/lib/pages/milestone_page.dart b/lib/pages/milestone_page.dart index 09ae7b9..0c55bbf 100644 --- a/lib/pages/milestone_page.dart +++ b/lib/pages/milestone_page.dart @@ -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), ], diff --git a/lib/utils.dart b/lib/utils.dart index 456bb40..0e097cd 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -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'; - } -} diff --git a/lib/widgets/chart_widget.dart b/lib/widgets/chart_widget.dart index 9d37a75..4f2bce3 100644 --- a/lib/widgets/chart_widget.dart +++ b/lib/widgets/chart_widget.dart @@ -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: [ // 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, ), ], diff --git a/lib/widgets/interval_widget.dart b/lib/widgets/interval_widget.dart index 24cb0d1..76b5eb5 100644 --- a/lib/widgets/interval_widget.dart +++ b/lib/widgets/interval_widget.dart @@ -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 { } 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 { 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 { @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 { child: ElevatedButton( onPressed: _toggleDropdown, style: ButtonStyle( + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(horizontal: 15.0), + ), backgroundColor: MaterialStateProperty.all(CupertinoColors.extraLightBackgroundGray), foregroundColor: MaterialStateProperty.all(CupertinoColors.black), overlayColor: MaterialStateProperty.all(CupertinoColors.extraLightBackgroundGray), @@ -150,9 +160,13 @@ class IntervalWidgetState extends State { 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,), ], ), ), diff --git a/lib/widgets/language_switcher_widget.dart b/lib/widgets/language_switcher_widget.dart new file mode 100644 index 0000000..cdaa287 --- /dev/null +++ b/lib/widgets/language_switcher_widget.dart @@ -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 { + 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( + 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( + 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( + 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), + ], + ), + ), + ]; + }, + ), + ), + ); + } +} diff --git a/lib/widgets/milestone_timeline_widget.dart b/lib/widgets/milestone_timeline_widget.dart index 8992c5f..2b1beba 100644 --- a/lib/widgets/milestone_timeline_widget.dart +++ b/lib/widgets/milestone_timeline_widget.dart @@ -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( diff --git a/lib/widgets/result_widget.dart b/lib/widgets/result_widget.dart index 2958cba..1ed58fa 100644 --- a/lib/widgets/result_widget.dart +++ b/lib/widgets/result_widget.dart @@ -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> 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: [ - _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,43 +89,43 @@ 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( - label, - textAlign: TextAlign.start, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + Text( + label, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 10, ), ), - const SizedBox(width: 20), - Flexible( - child: Text( - value, - textAlign: TextAlign.end, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + const SizedBox(height: 5), + Text( + value, + style: const TextStyle( + fontWeight: FontWeight.bold, ), ), ], ), ); } -} +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 06d3e6b..a1e07b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 5f5a39c..c6fd886 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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. diff --git a/test/unit_test.dart b/test/unit_test.dart index ac24497..47c8f79 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -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'); - }); }); } diff --git a/test/widget_test.dart b/test/widget_test.dart index f170ab6..226923b 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -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 lowerValues = [100, 200, 300, 400]; - final List 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,93 +176,65 @@ void main() { }); }); group('Result Widget Tests', () { - testWidgets('Displays result values correctly', (WidgetTester tester) async { - const compoundInterest = '1000'; - const investedMoney = '500'; - const time = '5'; - const monthlySavingsRate = '100'; - const interestRate = '5'; - const payoutInterval = PayoutInterval.monthly; - final List investedMoneyList = [100, 200, 300, 400, 500]; - final List compoundInterestList = [100, 200, 300, 400, 1000]; + testWidgets('Shows correct localized texts', (WidgetTester tester) async { + const Locale testLocale = Locale('en'); + final localizations = await AppLocalizations.delegate.load(testLocale); - await tester.pumpWidget( - MaterialApp( - home: Material( - child: ResultWidget( - compoundInterest: compoundInterest, - investedMoney: investedMoney, - time: time, - monthlySavingsRate: monthlySavingsRate, - interestRate: interestRate, - payoutInterval: payoutInterval, - investedMoneyList: investedMoneyList, - compoundInterestList: compoundInterestList, + const compoundInterest = '12000'; + const investedMoney = '10000'; + const time = '10'; + const monthlySavingsRate = '100'; + const interestRate = '5'; + const payoutInterval = PayoutInterval.yearly; + final List investedMoneyList = [1000, 2000, 3000, 4000, 5000]; + final List compoundInterestList = [1100, 2300, 3600, 5000, 6600]; + + await tester.pumpWidget( + MaterialApp( + locale: testLocale, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + home: Scaffold( + body: ResultWidget( + compoundInterest: compoundInterest, + investedMoney: investedMoney, + time: time, + monthlySavingsRate: monthlySavingsRate, + interestRate: interestRate, + payoutInterval: payoutInterval, + investedMoneyList: investedMoneyList, + compoundInterestList: compoundInterestList, + ), ), ), - ), - ); + ); - 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); + + final resultText = localizations.result_text( + double.parse(compoundInterest) - double.parse(investedMoney), + compoundInterest, + interestRate, + investedMoney, + time, + monthlySavingsRate, + ); + expect(find.text(resultText), findsOneWidget); + + expect(find.text(localizations.graphic), findsOneWidget); + expect(find.text(localizations.milestones), findsOneWidget); + }); }); - testWidgets('Navigates to ChartPage on "Grafik" button press', (WidgetTester tester) async { - final List investedMoneyList = [100, 200, 300, 400, 500]; - final List 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, - ), - ), - ), - ); - - 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); - }); -}); - group('Error Widget Tests', () { testWidgets('Displays error message correctly', (WidgetTester tester) async { const String errorMessage = 'This is an error message'; @@ -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> 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); + }); + }); } \ No newline at end of file