From 0a0e421158b2abb7c8a96d3935e393f725d39349 Mon Sep 17 00:00:00 2001 From: Thatsaphorn Atchariyaphap Date: Sat, 27 Sep 2025 11:58:25 +0200 Subject: [PATCH] Feature: Add support for localization, introduce `slang` for translations, and integrate German and English locale support throughout the app --- finlog_app/app/assets/i18n/de.i18n.json | 26 +++ finlog_app/app/assets/i18n/en.i18n.json | 26 +++ finlog_app/app/ios/Runner/Info.plist | 5 + .../app/lib/core/i18n/translations.g.dart | 182 ++++++++++++++++++ .../app/lib/core/i18n/translations_de.g.dart | 128 ++++++++++++ .../app/lib/core/i18n/translations_en.g.dart | 161 ++++++++++++++++ .../core/ui/controller/locale_controller.dart | 64 ++++-- finlog_app/app/lib/main.dart | 12 +- .../lib/modules/settings/settings_view.dart | 42 ++-- finlog_app/app/pubspec.yaml | 15 +- finlog_app/app/slang.yaml | 10 + 11 files changed, 627 insertions(+), 44 deletions(-) create mode 100644 finlog_app/app/assets/i18n/de.i18n.json create mode 100644 finlog_app/app/assets/i18n/en.i18n.json create mode 100644 finlog_app/app/lib/core/i18n/translations.g.dart create mode 100644 finlog_app/app/lib/core/i18n/translations_de.g.dart create mode 100644 finlog_app/app/lib/core/i18n/translations_en.g.dart create mode 100644 finlog_app/app/slang.yaml diff --git a/finlog_app/app/assets/i18n/de.i18n.json b/finlog_app/app/assets/i18n/de.i18n.json new file mode 100644 index 0000000..226efb6 --- /dev/null +++ b/finlog_app/app/assets/i18n/de.i18n.json @@ -0,0 +1,26 @@ +{ + "hello": "Hallo $name", + "login": { + "success": "Login erfolgreich" + }, + "settings": { + "title": "Einstellungen", + "sections": { + "account": "Konto & Daten", + "app": "App", + "help": "Hilfe & Rechtliches" + }, + "items": { + "appSettings": "App-Einstellungen", + "personalData": "Persönliche Daten", + "accountManagement": "Kontoverwaltung", + "helpCenter": "Hilfe", + "feedback": "Feedback", + "legalPrivacy": "Rechtliches & Datenschutz", + "logout": "Abmelden" + }, + "messages": { + "logoutNotImplemented": "Logout… (noch nicht implementiert)" + } + } +} diff --git a/finlog_app/app/assets/i18n/en.i18n.json b/finlog_app/app/assets/i18n/en.i18n.json new file mode 100644 index 0000000..5464d10 --- /dev/null +++ b/finlog_app/app/assets/i18n/en.i18n.json @@ -0,0 +1,26 @@ +{ + "hello": "Hello $name", + "login": { + "success": "Logged in successfully" + }, + "settings": { + "title": "Settings", + "sections": { + "account": "Account & Data", + "app": "App", + "help": "Help & Legal" + }, + "items": { + "appSettings": "App settings", + "personalData": "Personal data", + "accountManagement": "Account management", + "helpCenter": "Help", + "feedback": "Feedback", + "legalPrivacy": "Legal & Privacy", + "logout": "Sign out" + }, + "messages": { + "logoutNotImplemented": "Logout… (not implemented yet)" + } + } +} diff --git a/finlog_app/app/ios/Runner/Info.plist b/finlog_app/app/ios/Runner/Info.plist index c2e05c5..728930c 100644 --- a/finlog_app/app/ios/Runner/Info.plist +++ b/finlog_app/app/ios/Runner/Info.plist @@ -45,5 +45,10 @@ UIApplicationSupportsIndirectInputEvents + CFBundleLocalizations + + de + en + diff --git a/finlog_app/app/lib/core/i18n/translations.g.dart b/finlog_app/app/lib/core/i18n/translations.g.dart new file mode 100644 index 0000000..3bf8b89 --- /dev/null +++ b/finlog_app/app/lib/core/i18n/translations.g.dart @@ -0,0 +1,182 @@ +/// Generated file. Do not edit. +/// +/// Source: assets/i18n +/// To regenerate, run: `dart run slang` +/// +/// Locales: 2 +/// Strings: 28 (14 per locale) +/// +/// Built on 2025-09-27 at 09:55 UTC + +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.dart'; +import 'package:slang_flutter/slang_flutter.dart'; +export 'package:slang_flutter/slang_flutter.dart'; + +import 'translations_de.g.dart' deferred as l_de; +part 'translations_en.g.dart'; + +/// Supported locales. +/// +/// Usage: +/// - LocaleSettings.setLocale(AppLocale.en) // set locale +/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum +/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check +enum AppLocale with BaseAppLocale { + en(languageCode: 'en'), + de(languageCode: 'de'); + + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element, unused_element_parameter + this.countryCode, // ignore: unused_element, unused_element_parameter + }); + + @override final String languageCode; + @override final String? scriptCode; + @override final String? countryCode; + + @override + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + await l_de.loadLibrary(); + return l_de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + return l_de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + /// Gets current instance managed by [LocaleSettings]. + Translations get translations => LocaleSettings.instance.getTranslations(this); +} + +/// Method A: Simple +/// +/// No rebuild after locale change. +/// Translation happens during initialization of the widget (call of t). +/// Configurable via 'translate_var'. +/// +/// Usage: +/// String a = t.someKey.anotherKey; +/// String b = t['someKey.anotherKey']; // Only for edge cases! +Translations get t => LocaleSettings.instance.currentTranslations; + +/// Method B: Advanced +/// +/// All widgets using this method will trigger a rebuild when locale changes. +/// Use this if you have e.g. a settings page where the user can select the locale during runtime. +/// +/// Step 1: +/// wrap your App with +/// TranslationProvider( +/// child: MyApp() +/// ); +/// +/// Step 2: +/// final t = Translations.of(context); // Get t variable. +/// String a = t.someKey.anotherKey; // Use t variable. +/// String b = t['someKey.anotherKey']; // Only for edge cases! +class TranslationProvider extends BaseTranslationProvider { + TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); + + static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); +} + +/// Method B shorthand via [BuildContext] extension method. +/// Configurable via 'translate_var'. +/// +/// Usage (e.g. in a widget's build method): +/// context.t.someKey.anotherKey +extension BuildContextTranslationsExtension on BuildContext { + Translations get t => TranslationProvider.of(this).translations; +} + +/// Manages all translation instances and the current locale +class LocaleSettings extends BaseFlutterLocaleSettings { + LocaleSettings._() : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); + + static final instance = LocaleSettings._(); + + // static aliases (checkout base methods for documentation) + static AppLocale get currentLocale => instance.currentLocale; + static Stream getLocaleStream() => instance.getLocaleStream(); + static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static Future useDeviceLocale() => instance.useDeviceLocale(); + static Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + + // synchronous versions + static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync(); + static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); +} + +/// Provides utility functions without any side effects. +class AppLocaleUtils extends BaseAppLocaleUtils { + AppLocaleUtils._() : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); + + static final instance = AppLocaleUtils._(); + + // static aliases (checkout base methods for documentation) + static AppLocale parse(String rawLocale) => instance.parse(rawLocale); + static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); + static AppLocale findDeviceLocale() => instance.findDeviceLocale(); + static List get supportedLocales => instance.supportedLocales; + static List get supportedLocalesRaw => instance.supportedLocalesRaw; +} diff --git a/finlog_app/app/lib/core/i18n/translations_de.g.dart b/finlog_app/app/lib/core/i18n/translations_de.g.dart new file mode 100644 index 0000000..2f0e987 --- /dev/null +++ b/finlog_app/app/lib/core/i18n/translations_de.g.dart @@ -0,0 +1,128 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.dart'; +import 'translations.g.dart'; + +// Path: +class TranslationsDe implements Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + TranslationsDe({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, TranslationMetadata? meta}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = meta ?? TranslationMetadata( + locale: AppLocale.de, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key); + + late final TranslationsDe _root = this; // ignore: unused_field + + @override + TranslationsDe $copyWith({TranslationMetadata? meta}) => TranslationsDe(meta: meta ?? this.$meta); + + // Translations + @override String hello({required Object name}) => 'Hallo ${name}'; + @override late final _TranslationsLoginDe login = _TranslationsLoginDe._(_root); + @override late final _TranslationsSettingsDe settings = _TranslationsSettingsDe._(_root); +} + +// Path: login +class _TranslationsLoginDe implements TranslationsLoginEn { + _TranslationsLoginDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get success => 'Login erfolgreich'; +} + +// Path: settings +class _TranslationsSettingsDe implements TranslationsSettingsEn { + _TranslationsSettingsDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => 'Einstellungen'; + @override late final _TranslationsSettingsSectionsDe sections = _TranslationsSettingsSectionsDe._(_root); + @override late final _TranslationsSettingsItemsDe items = _TranslationsSettingsItemsDe._(_root); + @override late final _TranslationsSettingsMessagesDe messages = _TranslationsSettingsMessagesDe._(_root); +} + +// Path: settings.sections +class _TranslationsSettingsSectionsDe implements TranslationsSettingsSectionsEn { + _TranslationsSettingsSectionsDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get account => 'Konto & Daten'; + @override String get app => 'App'; + @override String get help => 'Hilfe & Rechtliches'; +} + +// Path: settings.items +class _TranslationsSettingsItemsDe implements TranslationsSettingsItemsEn { + _TranslationsSettingsItemsDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get appSettings => 'App-Einstellungen'; + @override String get personalData => 'Persönliche Daten'; + @override String get accountManagement => 'Kontoverwaltung'; + @override String get helpCenter => 'Hilfe'; + @override String get feedback => 'Feedback'; + @override String get legalPrivacy => 'Rechtliches & Datenschutz'; + @override String get logout => 'Abmelden'; +} + +// Path: settings.messages +class _TranslationsSettingsMessagesDe implements TranslationsSettingsMessagesEn { + _TranslationsSettingsMessagesDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get logoutNotImplemented => 'Logout… (noch nicht implementiert)'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on TranslationsDe { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'hello': return ({required Object name}) => 'Hallo ${name}'; + case 'login.success': return 'Login erfolgreich'; + case 'settings.title': return 'Einstellungen'; + case 'settings.sections.account': return 'Konto & Daten'; + case 'settings.sections.app': return 'App'; + case 'settings.sections.help': return 'Hilfe & Rechtliches'; + case 'settings.items.appSettings': return 'App-Einstellungen'; + case 'settings.items.personalData': return 'Persönliche Daten'; + case 'settings.items.accountManagement': return 'Kontoverwaltung'; + case 'settings.items.helpCenter': return 'Hilfe'; + case 'settings.items.feedback': return 'Feedback'; + case 'settings.items.legalPrivacy': return 'Rechtliches & Datenschutz'; + case 'settings.items.logout': return 'Abmelden'; + case 'settings.messages.logoutNotImplemented': return 'Logout… (noch nicht implementiert)'; + default: return null; + } + } +} + diff --git a/finlog_app/app/lib/core/i18n/translations_en.g.dart b/finlog_app/app/lib/core/i18n/translations_en.g.dart new file mode 100644 index 0000000..787daed --- /dev/null +++ b/finlog_app/app/lib/core/i18n/translations_en.g.dart @@ -0,0 +1,161 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +part of 'translations.g.dart'; + +// Path: +typedef TranslationsEn = Translations; // ignore: unused_element +class Translations implements BaseTranslations { + /// Returns the current translations of the given [context]. + /// + /// Usage: + /// final t = Translations.of(context); + static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations; + + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + Translations({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, TranslationMetadata? meta}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = meta ?? TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + dynamic operator[](String key) => $meta.getTranslation(key); + + late final Translations _root = this; // ignore: unused_field + + Translations $copyWith({TranslationMetadata? meta}) => Translations(meta: meta ?? this.$meta); + + // Translations + + /// en: 'Hello $name' + String hello({required Object name}) => 'Hello ${name}'; + + late final TranslationsLoginEn login = TranslationsLoginEn._(_root); + late final TranslationsSettingsEn settings = TranslationsSettingsEn._(_root); +} + +// Path: login +class TranslationsLoginEn { + TranslationsLoginEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Logged in successfully' + String get success => 'Logged in successfully'; +} + +// Path: settings +class TranslationsSettingsEn { + TranslationsSettingsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Settings' + String get title => 'Settings'; + + late final TranslationsSettingsSectionsEn sections = TranslationsSettingsSectionsEn._(_root); + late final TranslationsSettingsItemsEn items = TranslationsSettingsItemsEn._(_root); + late final TranslationsSettingsMessagesEn messages = TranslationsSettingsMessagesEn._(_root); +} + +// Path: settings.sections +class TranslationsSettingsSectionsEn { + TranslationsSettingsSectionsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Account & Data' + String get account => 'Account & Data'; + + /// en: 'App' + String get app => 'App'; + + /// en: 'Help & Legal' + String get help => 'Help & Legal'; +} + +// Path: settings.items +class TranslationsSettingsItemsEn { + TranslationsSettingsItemsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'App settings' + String get appSettings => 'App settings'; + + /// en: 'Personal data' + String get personalData => 'Personal data'; + + /// en: 'Account management' + String get accountManagement => 'Account management'; + + /// en: 'Help' + String get helpCenter => 'Help'; + + /// en: 'Feedback' + String get feedback => 'Feedback'; + + /// en: 'Legal & Privacy' + String get legalPrivacy => 'Legal & Privacy'; + + /// en: 'Sign out' + String get logout => 'Sign out'; +} + +// Path: settings.messages +class TranslationsSettingsMessagesEn { + TranslationsSettingsMessagesEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Logout… (not implemented yet)' + String get logoutNotImplemented => 'Logout… (not implemented yet)'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on Translations { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'hello': return ({required Object name}) => 'Hello ${name}'; + case 'login.success': return 'Logged in successfully'; + case 'settings.title': return 'Settings'; + case 'settings.sections.account': return 'Account & Data'; + case 'settings.sections.app': return 'App'; + case 'settings.sections.help': return 'Help & Legal'; + case 'settings.items.appSettings': return 'App settings'; + case 'settings.items.personalData': return 'Personal data'; + case 'settings.items.accountManagement': return 'Account management'; + case 'settings.items.helpCenter': return 'Help'; + case 'settings.items.feedback': return 'Feedback'; + case 'settings.items.legalPrivacy': return 'Legal & Privacy'; + case 'settings.items.logout': return 'Sign out'; + case 'settings.messages.logoutNotImplemented': return 'Logout… (not implemented yet)'; + default: return null; + } + } +} + diff --git a/finlog_app/app/lib/core/ui/controller/locale_controller.dart b/finlog_app/app/lib/core/ui/controller/locale_controller.dart index 44e129a..bd0dc26 100644 --- a/finlog_app/app/lib/core/ui/controller/locale_controller.dart +++ b/finlog_app/app/lib/core/ui/controller/locale_controller.dart @@ -1,44 +1,74 @@ +import 'package:app/core/i18n/translations.g.dart'; import 'package:flutter/material.dart'; import 'package:fluttery/fluttery.dart'; import 'package:fluttery/preferences.dart'; class LocaleController extends ChangeNotifier { - final Preferences _prefs; - - LocaleController() : _prefs = App.service(); + final Preferences _prefs = App.service(); static const _key = 'language'; + LanguagePref _current = LanguagePref.system; - LanguagePref _current = LanguagePref.en; // Default = Englisch - + /// Einmal beim App-Start aufrufen Future init() async { final saved = await _prefs.getString(_key); - _current = switch (saved) { - 'de' => LanguagePref.de, - 'system' => LanguagePref.system, - 'en' || _ => LanguagePref.en, // default fallback = en - }; + _current = _fromString(saved) ?? LanguagePref.system; + + _applyToSlang(_current); notifyListeners(); } - void setLanguage(LanguagePref pref) { - if (_current == pref) return; - _current = pref; + /// Sprache ändern (persistieren + sofort anwenden) + Future setLanguage(LanguagePref lang) async { + _current = lang; + await _prefs.setString(_key, lang.name); + + _applyToSlang(lang); notifyListeners(); - _prefs.setString(_key, pref.name); // fire-and-forget + } + + void _applyToSlang(LanguagePref pref) { + if (pref == LanguagePref.system) { + LocaleSettings.useDeviceLocale(); + return; + } + + final code = pref.code; + if (code == null) { + LocaleSettings.useDeviceLocale(); + return; + } + + if (AppLocaleUtils.supportedLocalesRaw.contains(code)) { + LocaleSettings.setLocaleRaw(code); + } else { + LocaleSettings.setLocale(AppLocale.en); + } + } + + LanguagePref? _fromString(String? value) { + if (value == null || value.isEmpty) return null; + return LanguagePref.values.firstWhere( + (e) => e.name == value, + orElse: () => LanguagePref.system, + ); } LanguagePref get language => _current; Locale? get locale => _current.locale; + + bool get isSystem => _current == LanguagePref.system; } enum LanguagePref { - system(null), // folgt System - de(Locale('de')), // Deutsch - en(Locale('en')); // Englisch (default) + system(null), + de(Locale('de')), + en(Locale('en')); final Locale? locale; const LanguagePref(this.locale); + + String? get code => locale?.languageCode; } diff --git a/finlog_app/app/lib/main.dart b/finlog_app/app/lib/main.dart index 0e9e6b6..9d9cad1 100644 --- a/finlog_app/app/lib/main.dart +++ b/finlog_app/app/lib/main.dart @@ -1,9 +1,11 @@ import 'package:app/core/app/router.dart'; import 'package:app/core/app/startup/domain/initialize_app.dart'; +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'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:fluttery/fluttery.dart'; import 'package:fluttery/logger.dart'; import 'package:go_router/go_router.dart'; @@ -37,7 +39,9 @@ Future main() async { ChangeNotifierProvider(create: (context) => scaleController), ChangeNotifierProvider(create: (context) => localeController), ], - child: FinlogApp(router: buildAppRouter(startRoute)), + child: TranslationProvider( + child: FinlogApp(router: buildAppRouter(startRoute)), + ), ), ); } @@ -51,14 +55,14 @@ class FinlogApp extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); final textScale = context.watch(); - final localeCtrl = context.watch(); return AnimatedBuilder( animation: theme, builder: (context, _) => MaterialApp.router( title: 'Finlog', - locale: localeCtrl.locale, - supportedLocales: const [Locale('de'), Locale('en')], + locale: TranslationProvider.of(context).flutterLocale, + supportedLocales: AppLocaleUtils.supportedLocales, + localizationsDelegates: GlobalMaterialLocalizations.delegates, theme: ThemeData.light(), darkTheme: ThemeData.dark(), themeMode: theme.themeMode, diff --git a/finlog_app/app/lib/modules/settings/settings_view.dart b/finlog_app/app/lib/modules/settings/settings_view.dart index 7d9e613..d474ced 100644 --- a/finlog_app/app/lib/modules/settings/settings_view.dart +++ b/finlog_app/app/lib/modules/settings/settings_view.dart @@ -1,3 +1,4 @@ +import 'package:app/core/i18n/translations.g.dart'; import 'package:app/core/ui/panel.dart'; import 'package:app/modules/settings/modules/app/app_settings_view.dart'; import 'package:app/modules/settings/modules/help/feedback_view.dart'; @@ -12,8 +13,10 @@ class SettingsView extends StatelessWidget { @override Widget build(BuildContext context) { + final t = Translations.of(context); + return Scaffold( - appBar: AppBar(title: const Text('Einstellungen')), + appBar: AppBar(title: Text(t.settings.title)), body: PanelNavigator(rootBuilder: (ctx) => _CategoryList()), ); } @@ -24,6 +27,7 @@ class _CategoryList extends StatelessWidget { @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; + final t = Translations.of(context); Widget tile(IconData icon, String label, Widget Function() detail) { return Padding( @@ -53,29 +57,41 @@ class _CategoryList extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const _SectionHeader('App-Einstellungen'), - tile(Icons.tune, 'App-Einstellungen', () => const AppSettingsView()), + _SectionHeader(t.settings.sections.app), + tile( + Icons.tune, + t.settings.items.appSettings, + () => const AppSettingsView(), + ), const SizedBox(height: 12), - const _SectionHeader('Meine Daten'), + _SectionHeader(t.settings.sections.account), tile( Icons.badge_outlined, - 'Persönliche Daten', + t.settings.items.personalData, () => const PersonalPanel(), ), tile( Icons.manage_accounts_outlined, - 'Kontoverwaltung', + t.settings.items.accountManagement, () => const AccountPanel(), ), const SizedBox(height: 12), - const _SectionHeader('Hilfe'), - tile(Icons.help_outline, 'Hilfe', () => const HelpPanel()), - tile(Icons.feedback_outlined, 'Feedback', () => const FeedbackPanel()), + _SectionHeader(t.settings.sections.help), + tile( + Icons.help_outline, + t.settings.items.helpCenter, + () => const HelpPanel(), + ), + tile( + Icons.feedback_outlined, + t.settings.items.feedback, + () => const FeedbackPanel(), + ), tile( Icons.gavel_outlined, - 'Rechtliches & Datenschutz', + t.settings.items.legalPrivacy, // "Rechtliches & Datenschutz" () => const LegalPanel(), ), const SizedBox(height: 24), @@ -83,12 +99,10 @@ class _CategoryList extends StatelessWidget { const Divider(), ListTile( leading: const Icon(Icons.logout), - title: const Text('Abmelden'), + title: Text(t.settings.items.logout), onTap: () { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Logout… (noch nicht implementiert)'), - ), + SnackBar(content: Text(t.settings.messages.logoutNotImplemented)), ); }, ), diff --git a/finlog_app/app/pubspec.yaml b/finlog_app/app/pubspec.yaml index 3942a1b..d6ae67e 100644 --- a/finlog_app/app/pubspec.yaml +++ b/finlog_app/app/pubspec.yaml @@ -31,26 +31,23 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter fluttery: path: ../fluttery - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 go_router: ^16.2.2 animations: ^2.0.11 provider: ^6.1.5+1 + slang: ^4.8.1 + slang_flutter: ^4.8.0 dev_dependencies: flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^5.0.0 + build_runner: ^2.8.0 + slang_build_runner: ^4.8.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/finlog_app/app/slang.yaml b/finlog_app/app/slang.yaml new file mode 100644 index 0000000..1c032a8 --- /dev/null +++ b/finlog_app/app/slang.yaml @@ -0,0 +1,10 @@ +base_locale: en +input_directory: assets/i18n +input_file_pattern: .i18n.json +output_directory: lib/core/i18n +output_file_name: translations.g.dart +flutter_integration: true +locale_handling: true +lazy: true +class_name: Translations +enum_name: AppLocale