Add i18n via slang

This commit is contained in:
PapaTutuWawa 2025-05-12 00:02:19 +02:00
parent 99ab2f006d
commit e0fba11f25
25 changed files with 447 additions and 415 deletions

3
.gitignore vendored
View File

@ -43,3 +43,6 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
# Build artifacts
lib/i18n/*

137
assets/i18n/en.i18n.json Normal file
View File

@ -0,0 +1,137 @@
{
"common": {
"beneficiary": {
"addBeneficiary": {
"title": "Add beneficiary",
"body": "The beneficiary '$name' does not exist. Do you want to add it?"
},
"nameWithAccount": "$name (Account)"
},
"transaction": {
"directionSend": "Send",
"directionReceive": "Receive",
"beneficiaryTextfieldHintSend": "Payee",
"beneficiaryTextfieldHintReceive": "Payer"
},
"amount": "Amount",
"date": "Date",
"expenseCategory": {
"name": "Expense category",
"none": "None"
},
"templateName": "Template name",
"period": {
"days": "Days",
"weeks": "Weeks",
"months": "Months",
"years": "Years",
"daysNumber": "$number days",
"weeksNumber": "$number weeks",
"monthsNumber": "$number months",
"yearsNumber": "$number years"
}
},
"pages": {
"accounts": {
"title": "Accounts",
"accountSelector": {
"none": "None"
},
"addAccount": {
"accountName": "Acount name"
},
"expenseBreakdown": {
"title": "Expense Breakdown",
"noActiveAccount": "No account active",
"noExpensesAvailable": "No expenses available",
"availableFunds": "Available funds: $amount"
},
"totalBalance": {
"title": "Total Balance",
"loading": "..."
},
"upcomingTransactions": {
"title": "Upcoming Transactions",
"noUpcomingTransactions": "No upcoming transactions",
"items": {
"title": "$name ($amount)",
"dueIn": "Due in $number days"
}
},
"deleteAccount": {
"title": "Delete Account",
"content": "Are you sure you want to delete the account '$name'? This will delete all related data as well!"
}
},
"budgets": {
"addBudget": {
"budgetNameHint": "Budget name",
"income": "Income",
"includeOtherSpendings": "Include other spendings"
},
"addBudgetItem": {
"amountHint": "Amount"
},
"noBudgets": "No budgets",
"details": {
"noBudgetSelected": "No budget selected",
"noBudgetItems": "No budget items",
"budgetItems": "Budget items",
"items": {
"title": "$name ($amount)",
"loading": "...",
"over": "$amount over",
"remaining": "$amount left"
},
"daysLeft": "Days left",
"budgetLeft": "Budget left",
"totalBudget": "Budget total",
"budgetBreakdown": {
"title": "Budget breakdown",
"noSpendingAvailable": "No spending available"
},
"spendingBreakdown": {
"title": "Spending Breakdown"
}
}
},
"transactions": {
"balance": "Account Balance",
"details": {
"noTransactionSelected": "No transaction selected"
},
"addTransaction": {
"useTemplate": "Use template"
}
},
"templates": {
"removeTemplate": {
"title": "Remove Template",
"body": "Are you sure you want to remove the template '$name'?"
},
"addTemplate": {
"isRecurring": "Is recurring"
},
"nonRecurring": {
"title": "Non-recurring"
},
"recurring": {
"title": "Recurring"
}
},
"settings": {
"colorSchemes": {
"title": "Color Scheme",
"dark": "Dark",
"light": "Light",
"system": "System"
}
}
},
"modals": {
"add": "Add",
"delete": "Delete",
"cancel": "Cancel",
"save": "Save"
}
}

9
build.yaml Normal file
View File

@ -0,0 +1,9 @@
targets:
$default:
builders:
slang_build_runner:
options:
input_directory: assets/i18n
output_directory: lib/i18n
fallback_strategy: base_locale
base_locale: en

View File

@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/screen.dart'; import 'package:okane/screen.dart';
import 'package:okane/ui/navigation.dart'; import 'package:okane/ui/navigation.dart';
import 'package:okane/ui/pages/budgets/budget_details.dart'; import 'package:okane/ui/pages/budgets/budget_details.dart';
@ -15,6 +16,7 @@ import 'package:okane/ui/state/settings.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
LocaleSettings.useDeviceLocale();
final settings = SettingsCubit(); final settings = SettingsCubit();
await settings.loadSettings(); await settings.loadSettings();

View File

@ -10,6 +10,7 @@ import 'package:okane/ui/pages/account/total_balance_card.dart';
import 'package:okane/ui/pages/account/upcoming_transactions_card.dart'; import 'package:okane/ui/pages/account/upcoming_transactions_card.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
import 'package:okane/i18n/strings.g.dart';
class AccountListPage extends StatefulWidget { class AccountListPage extends StatefulWidget {
final bool isPage; final bool isPage;
@ -33,7 +34,7 @@ class AccountListPageState extends State<AccountListPage> {
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text( child: Text(
"Accounts", t.pages.accounts.title,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
), ),
@ -104,7 +105,10 @@ class AccountListPageState extends State<AccountListPage> {
), ),
); );
}, },
child: Text(bloc.activeAccount?.name ?? "None"), child: Text(
bloc.activeAccount?.name ??
t.pages.accounts.accountSelector.none,
),
), ),
], ],
), ),
@ -207,7 +211,8 @@ class AccountListPageState extends State<AccountListPage> {
child: TextField( child: TextField(
controller: _accountNameController, controller: _accountNameController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "Account name", hintText:
t.pages.accounts.addAccount.accountName,
), ),
), ),
), ),
@ -229,7 +234,7 @@ class AccountListPageState extends State<AccountListPage> {
_accountNameController.text = ""; _accountNameController.text = "";
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text("Add"), child: Text(t.modals.add),
), ),
], ],
), ),

View File

@ -3,6 +3,7 @@ import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
@ -67,7 +68,7 @@ class AccountBalanceGraphCard extends StatelessWidget {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
children: [ children: [
Text("Account balance"), Text(t.pages.transactions.balance),
SizedBox( SizedBox(
height: 150, height: 150,
child: FutureBuilder( child: FutureBuilder(

View File

@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/collections/transaction.dart'; import 'package:okane/database/collections/transaction.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/piechart.dart'; import 'package:okane/ui/widgets/piechart.dart';
@ -75,7 +76,7 @@ class BreakdownCard extends StatelessWidget {
Widget _buildCard(Widget child, String? subtitle) { Widget _buildCard(Widget child, String? subtitle) {
return ResponsiveCard( return ResponsiveCard(
titleText: "Expense Breakdown", titleText: t.pages.accounts.expenseBreakdown.title,
subtitleText: subtitle, subtitleText: subtitle,
child: child, child: child,
); );
@ -91,7 +92,9 @@ class BreakdownCard extends StatelessWidget {
return BlocBuilder<CoreCubit, CoreState>( return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) { builder: (context, state) {
if (bloc.activeAccount == null) { if (bloc.activeAccount == null) {
return _buildCenterText("No account active"); return _buildCenterText(
t.pages.accounts.expenseBreakdown.noActiveAccount,
);
} }
return FutureBuilder( return FutureBuilder(
@ -125,7 +128,9 @@ class BreakdownCard extends StatelessWidget {
) )
.toList(); .toList();
if (sectionData.isEmpty) { if (sectionData.isEmpty) {
return _buildCenterText("No expenses available"); return _buildCenterText(
t.pages.accounts.expenseBreakdown.noExpensesAvailable,
);
} }
return _buildCard( return _buildCard(
OkanePieChart( OkanePieChart(
@ -141,7 +146,9 @@ class BreakdownCard extends StatelessWidget {
) )
.toList(), .toList(),
), ),
"Available money: ${formatCurrency(data.usable)}", t.pages.accounts.expenseBreakdown.availableFunds(
amount: formatCurrency(data.usable),
),
); );
}, },
); );

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/collections/account.dart'; import 'package:okane/database/collections/account.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
class DeleteAccountPopup extends StatelessWidget { class DeleteAccountPopup extends StatelessWidget {
@ -22,7 +23,7 @@ class DeleteAccountPopup extends StatelessWidget {
return BlocBuilder<CoreCubit, CoreState>( return BlocBuilder<CoreCubit, CoreState>(
builder: builder:
(context, state) => AlertDialog( (context, state) => AlertDialog(
title: Text("Delete Account"), title: Text(t.pages.accounts.deleteAccount.title),
content: content:
state.isDeletingAccount state.isDeletingAccount
? Row( ? Row(
@ -37,7 +38,9 @@ class DeleteAccountPopup extends StatelessWidget {
], ],
) )
: Text( : Text(
"Are you sure you want to delete the account '${account.name!}'? This will delete all transactions as well.", t.pages.accounts.deleteAccount.content(
name: account.name!,
),
), ),
actions: [ actions: [
TextButton( TextButton(
@ -48,7 +51,10 @@ class DeleteAccountPopup extends StatelessWidget {
await GetIt.I.get<CoreCubit>().deleteAccount(account); await GetIt.I.get<CoreCubit>().deleteAccount(account);
afterDelete(); afterDelete();
}, },
child: Text("Delete", style: TextStyle(color: Colors.red)), child: Text(
t.modals.delete,
style: TextStyle(color: Colors.red),
),
), ),
TextButton( TextButton(
onPressed: onPressed:
@ -57,7 +63,7 @@ class DeleteAccountPopup extends StatelessWidget {
: () { : () {
onCancel(); onCancel();
}, },
child: Text("Cancel"), child: Text(t.modals.cancel),
), ),
], ],
), ),

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:okane/database/collections/account.dart'; import 'package:okane/database/collections/account.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/piechart_card.dart'; import 'package:okane/ui/widgets/piechart_card.dart';
@ -24,14 +25,16 @@ class TotalBalanceCard extends StatelessWidget {
return BlocBuilder<CoreCubit, CoreState>( return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) { builder: (context, state) {
return ResponsiveCard( return ResponsiveCard(
titleText: "Total Balance", titleText: t.pages.accounts.totalBalance.title,
child: Padding( child: Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: FutureBuilder( child: FutureBuilder(
future: _getTotalBalance(state.accounts), future: _getTotalBalance(state.accounts),
builder: (context, snapshot) { builder: (context, snapshot) {
return Text( return Text(
snapshot.hasData ? formatCurrency(snapshot.data!) : "...", snapshot.hasData
? formatCurrency(snapshot.data!)
: t.pages.accounts.totalBalance.loading,
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
); );
}, },

View File

@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/collections/recurrent.dart'; import 'package:okane/database/collections/recurrent.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/add_transaction.dart'; import 'package:okane/ui/widgets/add_transaction.dart';
@ -28,25 +29,40 @@ class UpcomingTransactionsCard extends StatelessWidget {
upcoming.isEmpty upcoming.isEmpty
? [ ? [
Text( Text(
"No upcoming transactions", t
.pages
.accounts
.upcomingTransactions
.noUpcomingTransactions,
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
), ),
] ]
: upcoming : upcoming
.map( .map(
(t) => ListTile( (transaction) => ListTile(
title: Text( title: Text(
"${t.template.value!.name} (${t.template.value!.amount}€)", t.pages.accounts.upcomingTransactions.items.title(
name: transaction.template.value!.name,
amount:
"${formatCurrency(transaction.template.value!.amount)}",
),
), ),
subtitle: Text( subtitle: Text(
"Due in ${today.difference(t.lastExecution ?? today).inDays} days", t.pages.accounts.upcomingTransactions.items.dueIn(
number:
today
.difference(
transaction.lastExecution ?? today,
)
.inDays,
),
), ),
leading: Icon( leading: Icon(
t.template.value!.amount < 0 transaction.template.value!.amount < 0
? Icons.remove ? Icons.remove
: Icons.add, : Icons.add,
color: color:
t.template.value!.amount < 0 transaction.template.value!.amount < 0
? Colors.red ? Colors.red
: Colors.green, : Colors.green,
), ),
@ -58,12 +74,14 @@ class UpcomingTransactionsCard extends StatelessWidget {
builder: builder:
(context) => AddTransactionWidget( (context) => AddTransactionWidget(
activeAccountItem: bloc.activeAccount!, activeAccountItem: bloc.activeAccount!,
template: t.template.value!, template: transaction.template.value!,
onAdd: (transaction) async { onAdd: (newTransaction) async {
// Update the recurring template // Update the recurring template
print(transaction.date); transaction.lastExecution =
t.lastExecution = transaction.date; newTransaction.date;
await upsertRecurringTransaction(t); await upsertRecurringTransaction(
transaction,
);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
@ -74,7 +92,7 @@ class UpcomingTransactionsCard extends StatelessWidget {
) )
.toList(); .toList();
return ResponsiveCard( return ResponsiveCard(
titleText: "Upcoming Transactions", titleText: t.pages.accounts.upcomingTransactions.title,
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column(children: transactions), child: Column(children: transactions),

View File

@ -20,11 +20,8 @@ class BeneficiaryListPage extends StatelessWidget {
leading: ImageWrapper(title: item.name, path: item.imagePath), leading: ImageWrapper(title: item.name, path: item.imagePath),
// TODO: Allow deleting beneficiaries // TODO: Allow deleting beneficiaries
trailing: IconButton( trailing: IconButton(
onPressed: null, onPressed: null,
icon: Icon( icon: Icon(Icons.delete, color: Colors.grey),
Icons.delete,
color: Colors.grey,
),
), ),
title: Text(item.name), title: Text(item.name),
); );

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/collections/budget.dart'; import 'package:okane/database/collections/budget.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
class AddBudgetPopup extends StatefulWidget { class AddBudgetPopup extends StatefulWidget {
@ -23,12 +24,16 @@ class AddBudgetState extends State<AddBudgetPopup> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField( TextField(
decoration: InputDecoration(hintText: "Budget name"), decoration: InputDecoration(
hintText: t.pages.budgets.addBudget.budgetNameHint,
),
controller: _budgetNameEditController, controller: _budgetNameEditController,
), ),
TextField( TextField(
decoration: InputDecoration(hintText: "Income"), decoration: InputDecoration(
hintText: t.pages.budgets.addBudget.income,
),
controller: _budgetIncomeEditController, controller: _budgetIncomeEditController,
keyboardType: TextInputType.numberWithOptions( keyboardType: TextInputType.numberWithOptions(
signed: false, signed: false,
@ -57,7 +62,7 @@ class AddBudgetState extends State<AddBudgetPopup> {
await upsertBudget(budget); await upsertBudget(budget);
widget.onDone(); widget.onDone();
}, },
child: Text("Add"), child: Text(t.modals.add),
), ),
], ],
), ),

View File

@ -3,6 +3,7 @@ import 'package:get_it/get_it.dart';
import 'package:okane/database/collections/budget.dart'; import 'package:okane/database/collections/budget.dart';
import 'package:okane/database/collections/expense_category.dart'; import 'package:okane/database/collections/expense_category.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/add_expense_category.dart'; import 'package:okane/ui/widgets/add_expense_category.dart';
@ -32,7 +33,7 @@ class AddBudgetItemState extends State<AddBudgetItemPopup> {
children: [ children: [
Row( Row(
children: [ children: [
Text("Expense category"), Text(t.common.expenseCategory.name),
OutlinedButton( OutlinedButton(
onPressed: () async { onPressed: () async {
@ -46,13 +47,17 @@ class AddBudgetItemState extends State<AddBudgetItemPopup> {
setState(() => _expenseCategory = category); setState(() => _expenseCategory = category);
}, },
child: Text(_expenseCategory?.name ?? "None"), child: Text(
_expenseCategory?.name ?? t.common.expenseCategory.none,
),
), ),
], ],
), ),
TextField( TextField(
decoration: InputDecoration(hintText: "Amount"), decoration: InputDecoration(
hintText: t.pages.budgets.addBudgetItem.amountHint,
),
controller: _budgetItemAmountEditController, controller: _budgetItemAmountEditController,
keyboardType: TextInputType.numberWithOptions( keyboardType: TextInputType.numberWithOptions(
signed: false, signed: false,
@ -91,7 +96,7 @@ class AddBudgetItemState extends State<AddBudgetItemPopup> {
await upsertBudget(widget.budget); await upsertBudget(widget.budget);
widget.onDone(); widget.onDone();
}, },
child: Text("Add"), child: Text(t.modals.add),
), ),
], ],
), ),

View File

@ -1,14 +1,12 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/collections/budget.dart'; import 'package:okane/database/collections/budget.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/ui/pages/account/breakdown_card.dart'; import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/pages/budgets/add_budget_item.dart'; import 'package:okane/ui/pages/budgets/add_budget_item.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/piechart.dart';
import 'package:okane/ui/widgets/piechart_card.dart'; import 'package:okane/ui/widgets/piechart_card.dart';
class BudgetDetailsPage extends StatelessWidget { class BudgetDetailsPage extends StatelessWidget {
@ -55,13 +53,13 @@ class BudgetDetailsPage extends StatelessWidget {
BlocBuilder<CoreCubit, CoreState>( BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) { builder: (context, state) {
if (state.activeBudget == null) { if (state.activeBudget == null) {
return Text("No budget selected"); return Text(t.pages.budgets.details.noBudgetSelected);
} }
if (state.activeBudget!.items.isEmpty) { if (state.activeBudget!.items.isEmpty) {
return Row( return Row(
children: [ children: [
Text("No budget items added"), Text(t.pages.budgets.details.noBudgetItems),
Padding( Padding(
padding: EdgeInsets.only(left: 16), padding: EdgeInsets.only(left: 16),
child: IconButton( child: IconButton(
@ -92,7 +90,7 @@ class BudgetDetailsPage extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
"Budget items", t.pages.budgets.details.budgetItems,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
ListView.builder( ListView.builder(
@ -105,9 +103,14 @@ class BudgetDetailsPage extends StatelessWidget {
final amount = formatCurrency(item.amount); final amount = formatCurrency(item.amount);
return ListTile( return ListTile(
title: Text( title: Text(
"${item.expenseCategory.value!.name} ($amount)", t.pages.budgets.details.items.title(
name: item.expenseCategory.value!.name,
amount: amount,
),
),
subtitle: Text(
t.pages.budgets.details.items.loading,
), ),
subtitle: Text("..."),
); );
}, },
), ),
@ -167,7 +170,7 @@ class BudgetDetailsPage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
"Days left", t.pages.budgets.details.daysLeft,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: style:
Theme.of( Theme.of(
@ -199,7 +202,7 @@ class BudgetDetailsPage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
"Budget left", t.pages.budgets.details.budgetLeft,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: style:
Theme.of( Theme.of(
@ -233,7 +236,7 @@ class BudgetDetailsPage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
"Budget total", t.pages.budgets.details.totalBudget,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: style:
Theme.of( Theme.of(
@ -276,14 +279,21 @@ class BudgetDetailsPage extends StatelessWidget {
), ),
) )
.toList(), .toList(),
titleText: "Budget breakdown", titleText:
t.pages.budgets.details.budgetBreakdown.title,
), ),
), ),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 8), padding: EdgeInsets.symmetric(horizontal: 8),
child: PieChartCard( child: PieChartCard(
fallbackText: "No spending available", fallbackText:
t
.pages
.budgets
.details
.budgetBreakdown
.noSpendingAvailable,
valueConverter: formatCurrency, valueConverter: formatCurrency,
items: items:
spending.entries spending.entries
@ -295,7 +305,13 @@ class BudgetDetailsPage extends StatelessWidget {
), ),
) )
.toList(), .toList(),
titleText: "Spending Breakdown", titleText:
t
.pages
.budgets
.details
.spendingBreakdown
.title,
), ),
), ),
], ],
@ -306,7 +322,7 @@ class BudgetDetailsPage extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Text( Text(
"Budget items", t.pages.budgets.details.budgetItems,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
Padding( Padding(
@ -337,8 +353,12 @@ class BudgetDetailsPage extends StatelessWidget {
: item.amount + spent; : item.amount + spent;
final subtitleText = final subtitleText =
left < 0 left < 0
? "${formatCurrency(left)} over" ? t.pages.budgets.details.items.over(
: "${formatCurrency(left)} left"; amount: formatCurrency(left),
)
: t.pages.budgets.details.items.remaining(
amount: formatCurrency(left),
);
return ListTile( return ListTile(
title: Text( title: Text(
"${item.expenseCategory.value!.name} ($amount)", "${item.expenseCategory.value!.name} ($amount)",

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/screen.dart'; import 'package:okane/screen.dart';
import 'package:okane/ui/pages/budgets/add_budget.dart'; import 'package:okane/ui/pages/budgets/add_budget.dart';
import 'package:okane/ui/pages/budgets/edit_budget.dart'; import 'package:okane/ui/pages/budgets/edit_budget.dart';
@ -19,7 +20,7 @@ class BudgetListPage extends StatelessWidget {
if (state.budgets.isEmpty) { if (state.budgets.isEmpty) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [Text("No budgets")], children: [Text(t.pages.budgets.noBudgets)],
); );
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:okane/database/collections/budget.dart'; import 'package:okane/database/collections/budget.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
class EditBudgetPopup extends StatefulWidget { class EditBudgetPopup extends StatefulWidget {
final Budget budget; final Budget budget;
@ -36,12 +37,14 @@ class EditBudgetState extends State<EditBudgetPopup> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField( TextField(
decoration: InputDecoration(hintText: "Name"), decoration: InputDecoration(
hintText: t.pages.budgets.addBudget.budgetNameHint,
),
controller: _budgetNameEditController, controller: _budgetNameEditController,
), ),
Row( Row(
children: [ children: [
Text("Include other spendings"), Text(t.pages.budgets.addBudget.includeOtherSpendings),
Switch( Switch(
value: _includeOtherSpendings, value: _includeOtherSpendings,
onChanged: (value) { onChanged: (value) {
@ -71,7 +74,7 @@ class EditBudgetState extends State<EditBudgetPopup> {
await upsertBudget(widget.budget); await upsertBudget(widget.budget);
widget.onDone(); widget.onDone();
}, },
child: Text("Save"), child: Text(t.modals.save),
), ),
], ],
), ),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/settings.dart'; import 'package:okane/ui/state/settings.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
@ -14,11 +15,17 @@ class SettingsPage extends StatelessWidget {
BlocBuilder<SettingsCubit, SettingsWrapper>( BlocBuilder<SettingsCubit, SettingsWrapper>(
builder: builder:
(context, state) => ListTile( (context, state) => ListTile(
title: Text("Color Scheme"), title: Text(t.pages.settings.colorSchemes.title),
subtitle: switch (state.settings.colorScheme) { subtitle: switch (state.settings.colorScheme) {
ColorSchemeSettings.dark => Text("Dark"), ColorSchemeSettings.dark => Text(
ColorSchemeSettings.light => Text("Light"), t.pages.settings.colorSchemes.dark,
ColorSchemeSettings.system => Text("System"), ),
ColorSchemeSettings.light => Text(
t.pages.settings.colorSchemes.light,
),
ColorSchemeSettings.system => Text(
t.pages.settings.colorSchemes.system,
),
}, },
onTap: () async { onTap: () async {
final colorScheme = await showDialogOrModal( final colorScheme = await showDialogOrModal(
@ -35,9 +42,12 @@ class SettingsPage extends StatelessWidget {
? Icon(Icons.check) ? Icon(Icons.check)
: null, : null,
title: Text(switch (s) { title: Text(switch (s) {
ColorSchemeSettings.dark => "Dark", ColorSchemeSettings.dark =>
ColorSchemeSettings.light => "Light", t.pages.settings.colorSchemes.dark,
ColorSchemeSettings.system => "System", ColorSchemeSettings.light =>
t.pages.settings.colorSchemes.light,
ColorSchemeSettings.system =>
t.pages.settings.colorSchemes.system,
}), }),
onTap: () { onTap: () {
Navigator.of(context).pop(s); Navigator.of(context).pop(s);

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/add_template.dart'; import 'package:okane/ui/widgets/add_template.dart';
@ -25,7 +26,9 @@ class TemplateListState extends State<TemplateListPage> {
children: [ children: [
CustomScrollView( CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter(child: Text("Non-recurring")), SliverToBoxAdapter(
child: Text(t.pages.templates.nonRecurring.title),
),
SliverList.builder( SliverList.builder(
itemCount: nonRecurringTemplates.length, itemCount: nonRecurringTemplates.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -38,8 +41,10 @@ class TemplateListState extends State<TemplateListPage> {
onPressed: () async { onPressed: () async {
final result = await confirm( final result = await confirm(
context, context,
"Remove Template", t.pages.templates.removeTemplate.title,
"Are you sure you want to remove the template '${template.name}'", t.pages.templates.removeTemplate.body(
name: template.name,
),
); );
if (!result) { if (!result) {
return; return;
@ -51,7 +56,9 @@ class TemplateListState extends State<TemplateListPage> {
); );
}, },
), ),
SliverToBoxAdapter(child: Text("Recurring")), SliverToBoxAdapter(
child: Text(t.pages.templates.recurring.title),
),
SliverList.builder( SliverList.builder(
itemCount: state.recurringTransactions.length, itemCount: state.recurringTransactions.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -63,8 +70,10 @@ class TemplateListState extends State<TemplateListPage> {
onPressed: () async { onPressed: () async {
final result = await confirm( final result = await confirm(
context, context,
"Remove Template", t.pages.templates.removeTemplate.title,
"Are you sure you want to remove the template '${template.template.value!.name}'", t.pages.templates.removeTemplate.body(
name: template.template.value!.name,
),
); );
if (!result) { if (!result) {
return; return;

View File

@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/collections/beneficiary.dart'; import 'package:okane/database/collections/beneficiary.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/image_wrapper.dart'; import 'package:okane/ui/widgets/image_wrapper.dart';
@ -69,7 +70,9 @@ class TransactionDetailsPage extends StatelessWidget {
child: BlocBuilder<CoreCubit, CoreState>( child: BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) { builder: (context, state) {
if (state.activeTransaction == null) { if (state.activeTransaction == null) {
return Text("No transaction selected"); return Text(
t.pages.transactions.details.noTransactionSelected,
);
} }
return Padding( return Padding(
@ -162,7 +165,7 @@ class TransactionDetailsPage extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Text("Expense category"), Text(t.common.expenseCategory.name),
Padding( Padding(
padding: EdgeInsets.only(left: 16), padding: EdgeInsets.only(left: 16),
child: Chip( child: Chip(

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:okane/database/collections/expense_category.dart'; import 'package:okane/database/collections/expense_category.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
class AddExpenseCategory extends StatefulWidget { class AddExpenseCategory extends StatefulWidget {
@ -39,7 +40,9 @@ class AddExpenseCategoryState extends State<AddExpenseCategory> {
), ),
TextField( TextField(
decoration: InputDecoration(hintText: "Category name"), decoration: InputDecoration(
hintText: t.common.expenseCategory.name,
),
controller: _categoryNameController, controller: _categoryNameController,
), ),
Row( Row(
@ -54,7 +57,7 @@ class AddExpenseCategoryState extends State<AddExpenseCategory> {
_categoryNameController.text = ""; _categoryNameController.text = "";
Navigator.of(context).pop(category); Navigator.of(context).pop(category);
}, },
child: Text("Add"), child: Text(t.modals.add),
), ),
], ],
), ),

View File

@ -1,291 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_picker_plus/picker.dart';
import 'package:get_it/get_it.dart';
import 'package:okane/database/collections/account.dart';
import 'package:okane/database/collections/beneficiary.dart';
import 'package:okane/database/collections/recurrent.dart';
import 'package:okane/database/collections/template.dart';
import 'package:okane/database/database.dart';
import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/transaction.dart';
import 'package:okane/ui/utils.dart';
import 'package:searchfield/searchfield.dart';
enum Period { days, weeks, months, years }
class AddRecurringTransactionTemplateWidget extends StatefulWidget {
final VoidCallback onAdd;
final Account activeAccountItem;
const AddRecurringTransactionTemplateWidget({
super.key,
required this.activeAccountItem,
required this.onAdd,
});
@override
State<AddRecurringTransactionTemplateWidget> createState() =>
_AddRecurringTransactionTemplateWidgetState();
}
class _AddRecurringTransactionTemplateWidgetState
extends State<AddRecurringTransactionTemplateWidget> {
final TextEditingController _beneficiaryTextController =
TextEditingController();
final TextEditingController _amountTextController = TextEditingController();
final TextEditingController _templateNameController = TextEditingController();
List<Beneficiary> beneficiaries = [];
SearchFieldListItem<Beneficiary>? _selectedBeneficiary;
TransactionDirection _selectedDirection = TransactionDirection.send;
Period _selectedPeriod = Period.months;
int _periodSize = 1;
String getBeneficiaryName(Beneficiary item) {
return switch (item.type) {
BeneficiaryType.account => "${item.name} (Account)",
BeneficiaryType.other => item.name,
};
}
Future<void> _submit(BuildContext context) async {
final beneficiaryName = _beneficiaryTextController.text;
if (_selectedBeneficiary == null && beneficiaryName.isEmpty) {
return;
}
if (_templateNameController.text.isEmpty) {
return;
}
Beneficiary? beneficiary = _selectedBeneficiary?.item;
if (beneficiary == null ||
getBeneficiaryName(beneficiary) != beneficiaryName) {
// Add a new beneficiary, if none was selected
final result = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: const Text("Add Beneficiary"),
content: Text(
"The beneficiary '$beneficiaryName' does not exist. Do you want to add it?",
),
actions: [
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Add'),
onPressed: () => Navigator.of(context).pop(true),
),
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(false),
),
],
),
);
if (result == null || !result) {
return;
}
beneficiary =
Beneficiary()
..name = beneficiaryName
..type = BeneficiaryType.other;
await upsertBeneficiary(beneficiary);
}
final days = switch (_selectedPeriod) {
Period.days => _periodSize,
Period.weeks => _periodSize * 7,
Period.months => _periodSize * 31,
Period.years => _periodSize * 365,
};
final factor = switch (_selectedDirection) {
TransactionDirection.send => -1,
TransactionDirection.receive => 1,
};
final amount = factor * double.parse(_amountTextController.text).abs();
final template =
TransactionTemplate()
..name = _templateNameController.text
..beneficiary.value = beneficiary
..account.value = widget.activeAccountItem
..recurring = true
..amount = amount;
await upsertTransactionTemplate(template);
final transaction =
RecurringTransaction()
..lastExecution = null
..template.value = template
..account.value = widget.activeAccountItem
..days = days;
await upsertRecurringTransaction(transaction);
_periodSize = 1;
_selectedPeriod = Period.weeks;
_amountTextController.text = "";
_templateNameController.text = "";
widget.onAdd();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: ListView(
shrinkWrap: true,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: TextField(
controller: _templateNameController,
decoration: InputDecoration(label: Text("Template name")),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SegmentedButton<TransactionDirection>(
segments: [
ButtonSegment(
value: TransactionDirection.send,
label: Text("Send"),
icon: Icon(Icons.remove),
),
ButtonSegment(
value: TransactionDirection.receive,
label: Text("Receive"),
icon: Icon(Icons.add),
),
],
selected: <TransactionDirection>{_selectedDirection},
multiSelectionEnabled: false,
onSelectionChanged: (selection) {
setState(() => _selectedDirection = selection.first);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SearchField<Beneficiary>(
suggestions:
beneficiaries
.where((el) {
final bloc = GetIt.I.get<CoreCubit>();
if (el.type == BeneficiaryType.account) {
return el.account.value?.id != bloc.activeAccount?.id;
}
return true;
})
.map((el) {
return SearchFieldListItem(
getBeneficiaryName(el),
item: el,
);
})
.toList(),
hint: switch (_selectedDirection) {
TransactionDirection.send => "Payee",
TransactionDirection.receive => "Payer",
},
controller: _beneficiaryTextController,
selectedValue: _selectedBeneficiary,
onSuggestionTap: (beneficiary) {
setState(() => _selectedBeneficiary = beneficiary);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: TextField(
controller: _amountTextController,
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: false,
),
decoration: InputDecoration(
hintText: "Amount",
icon: Icon(Icons.euro),
),
),
),
Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: SegmentedButton<Period>(
segments: [
ButtonSegment(value: Period.days, label: Text("Days")),
ButtonSegment(value: Period.weeks, label: Text("Weeks")),
ButtonSegment(value: Period.months, label: Text("Months")),
ButtonSegment(value: Period.years, label: Text("Years")),
],
selected: <Period>{_selectedPeriod},
multiSelectionEnabled: false,
onSelectionChanged: (selection) {
setState(() => _selectedPeriod = selection.first);
},
),
),
Text.rich(
TextSpan(
text: "Repeat every ",
children: [
WidgetSpan(
child: TextButton(
onPressed: () {
Picker(
adapter: NumberPickerAdapter(
data: [
NumberPickerColumn(
begin: 1,
end: 999,
initValue: _periodSize,
),
],
),
hideHeader: true,
selectedTextStyle: TextStyle(color: Colors.blue),
onConfirm: (Picker picker, List value) {
setState(() {
_periodSize = (value.first as int) + 1;
});
},
).showDialog(context);
},
child: Text(_periodSize.toString()),
),
),
TextSpan(
text: switch (_selectedPeriod) {
Period.days => " days",
Period.weeks => " weeks",
Period.months => " months",
Period.years => " years",
},
),
],
),
),
Align(
alignment: Alignment.centerRight,
child: OutlinedButton(
onPressed: () => _submit(context),
child: Text("Add"),
),
),
],
),
);
}
}

View File

@ -7,6 +7,7 @@ import 'package:okane/database/collections/expense_category.dart';
import 'package:okane/database/collections/recurrent.dart'; import 'package:okane/database/collections/recurrent.dart';
import 'package:okane/database/collections/template.dart'; import 'package:okane/database/collections/template.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/transaction.dart'; import 'package:okane/ui/transaction.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
@ -51,7 +52,9 @@ class _AddTransactionTemplateWidgetState
String getBeneficiaryName(Beneficiary item) { String getBeneficiaryName(Beneficiary item) {
return switch (item.type) { return switch (item.type) {
BeneficiaryType.account => "${item.name} (Account)", BeneficiaryType.account => t.common.beneficiary.nameWithAccount(
name: item.name,
),
BeneficiaryType.other => item.name, BeneficiaryType.other => item.name,
}; };
} }
@ -59,7 +62,6 @@ class _AddTransactionTemplateWidgetState
Future<void> _submit(BuildContext context) async { Future<void> _submit(BuildContext context) async {
final beneficiaryName = _beneficiaryTextController.text; final beneficiaryName = _beneficiaryTextController.text;
if (_selectedBeneficiary == null && beneficiaryName.isEmpty) { if (_selectedBeneficiary == null && beneficiaryName.isEmpty) {
print("No beneficiary");
return; return;
} }
if (_templateNameController.text.isEmpty) { if (_templateNameController.text.isEmpty) {
@ -74,23 +76,23 @@ class _AddTransactionTemplateWidgetState
context: context, context: context,
builder: builder:
(context) => AlertDialog( (context) => AlertDialog(
title: const Text("Add Beneficiary"), title: Text(t.common.beneficiary.addBeneficiary.title),
content: Text( content: Text(
"The beneficiary '$beneficiaryName' does not exist. Do you want to add it?", t.common.beneficiary.addBeneficiary.body(name: beneficiaryName),
), ),
actions: [ actions: [
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge, textStyle: Theme.of(context).textTheme.labelLarge,
), ),
child: const Text('Add'), child: Text(t.modals.add),
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
), ),
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge, textStyle: Theme.of(context).textTheme.labelLarge,
), ),
child: const Text('Cancel'), child: Text(t.modals.cancel),
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
), ),
], ],
@ -151,7 +153,7 @@ class _AddTransactionTemplateWidgetState
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: TextField( child: TextField(
controller: _templateNameController, controller: _templateNameController,
decoration: InputDecoration(label: Text("Template name")), decoration: InputDecoration(label: Text(t.common.templateName)),
), ),
), ),
@ -161,12 +163,12 @@ class _AddTransactionTemplateWidgetState
segments: [ segments: [
ButtonSegment( ButtonSegment(
value: TransactionDirection.send, value: TransactionDirection.send,
label: Text("Send"), label: Text(t.common.transaction.directionSend),
icon: Icon(Icons.remove), icon: Icon(Icons.remove),
), ),
ButtonSegment( ButtonSegment(
value: TransactionDirection.receive, value: TransactionDirection.receive,
label: Text("Receive"), label: Text(t.common.transaction.directionReceive),
icon: Icon(Icons.add), icon: Icon(Icons.add),
), ),
], ],
@ -201,8 +203,10 @@ class _AddTransactionTemplateWidgetState
}) })
.toList(), .toList(),
hint: switch (_selectedDirection) { hint: switch (_selectedDirection) {
TransactionDirection.send => "Payee", TransactionDirection.send =>
TransactionDirection.receive => "Payer", t.common.transaction.beneficiaryTextfieldHintSend,
TransactionDirection.receive =>
t.common.transaction.beneficiaryTextfieldHintReceive,
}, },
controller: _beneficiaryTextController, controller: _beneficiaryTextController,
selectedValue: _selectedBeneficiary, selectedValue: _selectedBeneficiary,
@ -222,7 +226,7 @@ class _AddTransactionTemplateWidgetState
decimal: false, decimal: false,
), ),
decoration: InputDecoration( decoration: InputDecoration(
hintText: "Amount", hintText: t.common.amount,
icon: Icon(Icons.euro), icon: Icon(Icons.euro),
), ),
), ),
@ -230,7 +234,7 @@ class _AddTransactionTemplateWidgetState
Row( Row(
children: [ children: [
Text("Expense category"), Text(t.common.expenseCategory.name),
Padding( Padding(
padding: EdgeInsets.only(left: 16), padding: EdgeInsets.only(left: 16),
@ -246,7 +250,9 @@ class _AddTransactionTemplateWidgetState
setState(() => _expenseCategory = category); setState(() => _expenseCategory = category);
}, },
child: Text(_expenseCategory?.name ?? "None"), child: Text(
_expenseCategory?.name ?? t.common.expenseCategory.none,
),
), ),
), ),
], ],
@ -254,7 +260,7 @@ class _AddTransactionTemplateWidgetState
Row( Row(
children: [ children: [
Text("Is recurring"), Text(t.pages.templates.addTemplate.isRecurring),
Padding( Padding(
padding: EdgeInsets.only(left: 16), padding: EdgeInsets.only(left: 16),
child: Switch( child: Switch(
@ -272,10 +278,22 @@ class _AddTransactionTemplateWidgetState
padding: EdgeInsets.only(left: 16, right: 16, top: 16), padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: SegmentedButton<Period>( child: SegmentedButton<Period>(
segments: [ segments: [
ButtonSegment(value: Period.days, label: Text("Days")), ButtonSegment(
ButtonSegment(value: Period.weeks, label: Text("Weeks")), value: Period.days,
ButtonSegment(value: Period.months, label: Text("Months")), label: Text(t.common.period.days),
ButtonSegment(value: Period.years, label: Text("Years")), ),
ButtonSegment(
value: Period.weeks,
label: Text(t.common.period.weeks),
),
ButtonSegment(
value: Period.months,
label: Text(t.common.period.months),
),
ButtonSegment(
value: Period.years,
label: Text(t.common.period.years),
),
], ],
selected: <Period>{_selectedPeriod}, selected: <Period>{_selectedPeriod},
multiSelectionEnabled: false, multiSelectionEnabled: false,
@ -311,10 +329,18 @@ class _AddTransactionTemplateWidgetState
child: Center( child: Center(
child: Text( child: Text(
switch (_selectedPeriod) { switch (_selectedPeriod) {
Period.days => "$_periodSize days", Period.days => t.common.period.daysNumber(
Period.weeks => "$_periodSize weeks", number: _periodSize,
Period.months => "$_periodSize months", ),
Period.years => "$_periodSize years", Period.weeks => t.common.period.weeksNumber(
number: _periodSize,
),
Period.months => t.common.period.monthsNumber(
number: _periodSize,
),
Period.years => t.common.period.yearsNumber(
number: _periodSize,
),
}, },
style: TextStyle( style: TextStyle(
color: _isRecurring ? Colors.black : Colors.grey, color: _isRecurring ? Colors.black : Colors.grey,
@ -341,7 +367,7 @@ class _AddTransactionTemplateWidgetState
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: OutlinedButton( child: OutlinedButton(
onPressed: () => _submit(context), onPressed: () => _submit(context),
child: Text("Add"), child: Text(t.modals.add),
), ),
), ),
], ],

View File

@ -7,6 +7,7 @@ import 'package:okane/database/collections/expense_category.dart';
import 'package:okane/database/collections/template.dart'; import 'package:okane/database/collections/template.dart';
import 'package:okane/database/collections/transaction.dart'; import 'package:okane/database/collections/transaction.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/transaction.dart'; import 'package:okane/ui/transaction.dart';
import 'package:okane/ui/utils.dart'; import 'package:okane/ui/utils.dart';
@ -67,7 +68,9 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
String getBeneficiaryName(Beneficiary item) { String getBeneficiaryName(Beneficiary item) {
return switch (item.type) { return switch (item.type) {
BeneficiaryType.account => "${item.name} (Account)", BeneficiaryType.account => t.common.beneficiary.nameWithAccount(
name: item.name,
),
BeneficiaryType.other => item.name, BeneficiaryType.other => item.name,
}; };
} }
@ -86,23 +89,23 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
context: context, context: context,
builder: builder:
(context) => AlertDialog( (context) => AlertDialog(
title: const Text("Add Beneficiary"), title: Text(t.common.beneficiary.addBeneficiary.title),
content: Text( content: Text(
"The beneficiary '$beneficiaryName' does not exist. Do you want to add it?", t.common.beneficiary.addBeneficiary.body(name: beneficiaryName),
), ),
actions: [ actions: [
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge, textStyle: Theme.of(context).textTheme.labelLarge,
), ),
child: const Text('Add'), child: Text(t.modals.add),
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
), ),
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge, textStyle: Theme.of(context).textTheme.labelLarge,
), ),
child: const Text('Cancel'), child: Text(t.modals.cancel),
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
), ),
], ],
@ -177,7 +180,7 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
template.beneficiary.value!, template.beneficiary.value!,
); );
}, },
child: Text("Use template"), child: Text(t.pages.transactions.addTransaction.useTemplate),
), ),
Padding( Padding(
@ -186,12 +189,12 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
segments: [ segments: [
ButtonSegment( ButtonSegment(
value: TransactionDirection.send, value: TransactionDirection.send,
label: Text("Send"), label: Text(t.common.transaction.directionSend),
icon: Icon(Icons.remove), icon: Icon(Icons.remove),
), ),
ButtonSegment( ButtonSegment(
value: TransactionDirection.receive, value: TransactionDirection.receive,
label: Text("Receive"), label: Text(t.common.transaction.directionReceive),
icon: Icon(Icons.add), icon: Icon(Icons.add),
), ),
], ],
@ -226,8 +229,10 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
}) })
.toList(), .toList(),
hint: switch (_selectedDirection) { hint: switch (_selectedDirection) {
TransactionDirection.send => "Payee", TransactionDirection.send =>
TransactionDirection.receive => "Payer", t.common.transaction.beneficiaryTextfieldHintSend,
TransactionDirection.receive =>
t.common.transaction.beneficiaryTextfieldHintReceive,
}, },
controller: _beneficiaryTextController, controller: _beneficiaryTextController,
selectedValue: _selectedBeneficiary, selectedValue: _selectedBeneficiary,
@ -247,7 +252,7 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
decimal: false, decimal: false,
), ),
decoration: InputDecoration( decoration: InputDecoration(
hintText: "Amount", hintText: t.common.amount,
icon: Icon(Icons.euro), icon: Icon(Icons.euro),
), ),
), ),
@ -258,7 +263,7 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Text("Date"), Text(t.common.date),
Padding( Padding(
padding: EdgeInsets.only(left: 16), padding: EdgeInsets.only(left: 16),
child: OutlinedButton( child: OutlinedButton(
@ -289,7 +294,7 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
Row( Row(
children: [ children: [
Text("Expense category"), Text(t.common.expenseCategory.name),
Padding( Padding(
padding: EdgeInsets.only(left: 16), padding: EdgeInsets.only(left: 16),
child: OutlinedButton( child: OutlinedButton(
@ -304,7 +309,9 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
setState(() => _expenseCategory = category); setState(() => _expenseCategory = category);
}, },
child: Text(_expenseCategory?.name ?? "None"), child: Text(
_expenseCategory?.name ?? t.common.expenseCategory.none,
),
), ),
), ),
], ],
@ -314,7 +321,7 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: OutlinedButton( child: OutlinedButton(
onPressed: () => _submit(context), onPressed: () => _submit(context),
child: Text("Add"), child: Text(t.modals.add),
), ),
), ),
], ],

View File

@ -177,6 +177,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" version: "3.0.6"
csv:
dependency: transitive
description:
name: csv
sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
url: "https://pub.dev"
source: hosted
version: "6.0.0"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -237,10 +245,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "8986dec4581b4bcd4b6df5d75a2ea0bede3db802f500635d05fa8be298f9467f" sha256: a222f231db4f822fc49e3b753674bda630e981873c84bf8604bceeb77fce0b24
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.2" version: "10.1.7"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -416,6 +424,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.7" version: "0.6.7"
json2yaml:
dependency: transitive
description:
name: json2yaml
sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51
url: "https://pub.dev"
source: hosted
version: "3.0.1"
json_annotation: json_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
@ -636,10 +652,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: searchfield name: searchfield
sha256: "223fca0828ec95f45501db93feac7b120b93600760c0d8c04039fb2eeed9cc20" sha256: "98fa29165366ec178e86a370918b084c9830cdf6663126fbd11b8c6f77cdcd0f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.7" version: "1.2.9"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@ -717,6 +733,30 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
slang:
dependency: "direct main"
description:
name: slang
sha256: a466773de768eb95bdf681e0a92e7c8010d44bb247b62130426c83ece33aeaed
url: "https://pub.dev"
source: hosted
version: "3.32.0"
slang_build_runner:
dependency: "direct dev"
description:
name: slang_build_runner
sha256: b2e0c63f3c801a4aa70b4ca43173893d6eb7d5a421fc9d97ad983527397631b3
url: "https://pub.dev"
source: hosted
version: "3.32.0"
slang_flutter:
dependency: "direct main"
description:
name: slang_flutter
sha256: "1a98e878673996902fa5ef0b61ce5c245e41e4d25640d18af061c6aab917b0c7"
url: "https://pub.dev"
source: hosted
version: "3.32.0"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
@ -849,10 +889,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: web_socket name: web_socket
sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: transitive
description: description:
@ -865,10 +905,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.12.0" version: "5.13.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View File

@ -26,6 +26,8 @@ dependencies:
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
json_annotation: ^4.9.0 json_annotation: ^4.9.0
more: 4.5.0 more: 4.5.0
slang: ^3.0.0
slang_flutter: ^3.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -35,6 +37,7 @@ dev_dependencies:
freezed: 2.5.0 freezed: 2.5.0
isar_generator: ^3.1.0+1 isar_generator: ^3.1.0+1
json_serializable: ^6.4.0 json_serializable: ^6.4.0
slang_build_runner: ^3.0.0
flutter: flutter:
uses-material-design: true uses-material-design: true