Add i18n support and integrate localized strings across modules (Login, AppShell, etc.)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'package:app/core/i18n/translations.g.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -23,53 +24,55 @@ class _AppShellState extends State<AppShell> {
|
||||
|
||||
// --- NAV ITEMS -------------------------------------------------------------
|
||||
|
||||
final _items =
|
||||
const <
|
||||
({IconData icon, IconData? selectedIcon, String label, String route})
|
||||
>[
|
||||
(
|
||||
icon: Icons.dashboard_outlined,
|
||||
selectedIcon: Icons.dashboard,
|
||||
label: 'Dashboard',
|
||||
route: '/home',
|
||||
),
|
||||
(
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
selectedIcon: Icons.account_balance_wallet,
|
||||
label: 'Budgets',
|
||||
route: '/budget',
|
||||
),
|
||||
(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
selectedIcon: Icons.inventory_2,
|
||||
label: 'Inventar',
|
||||
route: '/inventory',
|
||||
),
|
||||
(
|
||||
icon: Icons.bar_chart_outlined,
|
||||
selectedIcon: Icons.bar_chart,
|
||||
label: 'Reports',
|
||||
route: '/reports',
|
||||
),
|
||||
(
|
||||
List<({IconData icon, IconData? selectedIcon, String label, String route})> _getItems(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
return [
|
||||
(
|
||||
icon: Icons.dashboard_outlined,
|
||||
selectedIcon: Icons.dashboard,
|
||||
label: t.app.navigationDashboard,
|
||||
route: '/home',
|
||||
),
|
||||
(
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
selectedIcon: Icons.account_balance_wallet,
|
||||
label: t.app.navigationBudgets,
|
||||
route: '/budget',
|
||||
),
|
||||
(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
selectedIcon: Icons.inventory_2,
|
||||
label: t.app.navigationInventory,
|
||||
route: '/inventory',
|
||||
),
|
||||
(
|
||||
icon: Icons.bar_chart_outlined,
|
||||
selectedIcon: Icons.bar_chart,
|
||||
label: t.app.navigationReports,
|
||||
route: '/reports',
|
||||
),
|
||||
(
|
||||
icon: Icons.settings,
|
||||
selectedIcon: Icons.settings_outlined,
|
||||
label: 'Settings',
|
||||
label: t.app.navigationSettings,
|
||||
route: '/settings',
|
||||
),
|
||||
];
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
int _indexForPath(String p) {
|
||||
for (var i = 0; i < _items.length; i++) {
|
||||
if (p.startsWith(_items[i].route)) return i;
|
||||
int _indexForPath(String p, List<({IconData icon, IconData? selectedIcon, String label, String route})> items) {
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (p.startsWith(items[i].route)) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void _goForIndex(BuildContext ctx, int i) => ctx.go(_items[i].route);
|
||||
void _goForIndex(BuildContext ctx, int i, List<({IconData icon, IconData? selectedIcon, String label, String route})> items) => ctx.go(items[i].route);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
final items = _getItems(context);
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final isRail = width >= _railBreakpoint;
|
||||
final currentPath = GoRouterState.of(context).matchedLocation;
|
||||
@@ -89,17 +92,17 @@ class _AppShellState extends State<AppShell> {
|
||||
builder: (ctx) => IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
onPressed: () => Scaffold.of(ctx).openDrawer(),
|
||||
tooltip: 'Menü',
|
||||
tooltip: t.app.tooltipMenu,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Benachrichtigungen',
|
||||
tooltip: t.app.tooltipNotifications,
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.notifications_none),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Benutzer-Einstellungen',
|
||||
tooltip: t.app.tooltipUserSettings,
|
||||
onPressed: () => context.push('/settings'),
|
||||
icon: const Icon(Icons.account_circle_outlined),
|
||||
),
|
||||
@@ -108,16 +111,16 @@ class _AppShellState extends State<AppShell> {
|
||||
|
||||
if (!isRail) {
|
||||
// ------------------- MOBILE: Drawer -------------------
|
||||
final selectedIndex = _indexForPath(currentPath);
|
||||
final selectedIndex = _indexForPath(currentPath, items);
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
drawer: _AppDrawer(items: _items, selectedIndex: selectedIndex),
|
||||
drawer: _AppDrawer(items: items, selectedIndex: selectedIndex),
|
||||
body: SafeArea(child: widget.child),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------- TABLET/DESKTOP: NavigationRail -------------------
|
||||
final selected = _indexForPath(currentPath);
|
||||
final selected = _indexForPath(currentPath, items);
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
@@ -143,13 +146,13 @@ class _AppShellState extends State<AppShell> {
|
||||
child: NavigationRail(
|
||||
extended: _railExtended,
|
||||
selectedIndex: selected,
|
||||
onDestinationSelected: (i) => _goForIndex(context, i),
|
||||
onDestinationSelected: (i) => _goForIndex(context, i, items),
|
||||
// leading: const Padding(
|
||||
// padding: EdgeInsets.only(top: 8),
|
||||
// child: _LogoHeader(),
|
||||
// ),
|
||||
destinations: [
|
||||
for (final it in _items)
|
||||
for (final it in items)
|
||||
NavigationRailDestination(
|
||||
icon: Icon(it.icon),
|
||||
selectedIcon: Icon(it.selectedIcon ?? it.icon),
|
||||
@@ -164,8 +167,8 @@ class _AppShellState extends State<AppShell> {
|
||||
// toggle lives at the very bottom so layout doesn't jump
|
||||
IconButton(
|
||||
tooltip: _railExtended
|
||||
? 'Leiste verkleinern'
|
||||
: 'Leiste erweitern',
|
||||
? t.app.tooltipCollapseRail
|
||||
: t.app.tooltipExpandRail,
|
||||
onPressed: () =>
|
||||
setState(() => _railExtended = !_railExtended),
|
||||
icon: Icon(
|
||||
@@ -234,7 +237,7 @@ class _AppDrawer extends StatelessWidget {
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
title: const Text('Einstellungen'),
|
||||
title: Text(Translations.of(context).app.drawerSettings),
|
||||
onTap: () {
|
||||
Navigator.of(context).maybePop();
|
||||
context.go('/settings');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:app/core/app/router.dart';
|
||||
import 'package:app/core/i18n/translations.g.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -28,9 +29,10 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Login')),
|
||||
appBar: AppBar(title: Text(t.login.title)),
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 360),
|
||||
@@ -39,7 +41,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
children: [
|
||||
const FlutterLogo(size: 64),
|
||||
const SizedBox(height: 24),
|
||||
Text('Please sign in', style: theme.textTheme.titleMedium),
|
||||
Text(t.login.pleaseSignIn, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -56,7 +58,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Login', key: ValueKey('text')),
|
||||
: Text(t.login.title, key: const ValueKey('text')),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -67,7 +69,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
'Signing you in…',
|
||||
t.login.signingIn,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:app/core/i18n/translations.g.dart';
|
||||
import 'package:app/core/ui/controller/locale_controller.dart';
|
||||
import 'package:app/core/ui/controller/scale_controller.dart';
|
||||
import 'package:app/core/ui/controller/theme.dart';
|
||||
@@ -61,15 +62,16 @@ class _SystemBackgroundSection extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
final vm = context.watch<AppSettingsViewModel>();
|
||||
final selected = vm.themeMode;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'System-Hintergrundfarbe',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
Text(
|
||||
t.settings.app.systemBackground,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
@@ -94,10 +96,10 @@ class _SystemBackgroundSection extends StatelessWidget {
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
constraints: const BoxConstraints(minHeight: 44, minWidth: 140),
|
||||
children: const [
|
||||
_SegItem(icon: Icons.phone_iphone, label: 'Systemstandard'),
|
||||
_SegItem(emoji: '🌙', label: 'Dark Mode'),
|
||||
_SegItem(emoji: '☀️', label: 'White Mode'),
|
||||
children: [
|
||||
_SegItem(icon: Icons.phone_iphone, label: t.settings.app.systemDefault),
|
||||
_SegItem(emoji: '🌙', label: t.settings.app.darkMode),
|
||||
_SegItem(emoji: '☀️', label: t.settings.app.lightMode),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -134,13 +136,14 @@ class _TextScaleSection extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
final vm = context.watch<AppSettingsViewModel>();
|
||||
final selected = vm.textScale;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Textgröße', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
Text(t.settings.app.textSize, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
ToggleButtons(
|
||||
isSelected: [
|
||||
@@ -167,11 +170,11 @@ class _TextScaleSection extends StatelessWidget {
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
constraints: const BoxConstraints(minHeight: 44, minWidth: 120),
|
||||
children: const [
|
||||
_SegItem(icon: Icons.phone_android, label: 'System'),
|
||||
_SegItem(icon: Icons.text_fields, label: 'Klein'),
|
||||
_SegItem(icon: Icons.text_fields, label: 'Mittel'),
|
||||
_SegItem(icon: Icons.text_fields, label: 'Groß'),
|
||||
children: [
|
||||
_SegItem(icon: Icons.phone_android, label: t.settings.app.system),
|
||||
_SegItem(icon: Icons.text_fields, label: t.settings.app.small),
|
||||
_SegItem(icon: Icons.text_fields, label: t.settings.app.medium),
|
||||
_SegItem(icon: Icons.text_fields, label: t.settings.app.large),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -184,25 +187,26 @@ class _LanguageSection extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
final vm = context.watch<AppSettingsViewModel>();
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Sprache', style: Theme.of(context).textTheme.titleMedium),
|
||||
Text(t.settings.app.language, style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
DropdownButtonFormField<LanguagePref>(
|
||||
initialValue: vm.language,
|
||||
onChanged: (v) => vm.setLanguage(v ?? LanguagePref.en),
|
||||
items: const [
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: LanguagePref.system,
|
||||
child: Text('Systemstandard'),
|
||||
child: Text(t.settings.app.systemDefault),
|
||||
),
|
||||
DropdownMenuItem(value: LanguagePref.de, child: Text('Deutsch')),
|
||||
DropdownMenuItem(value: LanguagePref.en, child: Text('Englisch')),
|
||||
DropdownMenuItem(value: LanguagePref.de, child: Text(t.settings.app.german)),
|
||||
DropdownMenuItem(value: LanguagePref.en, child: Text(t.settings.app.english)),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
|
||||
Reference in New Issue
Block a user