Feature: Add support for localization, introduce slang for translations, and integrate German and English locale support throughout the app
This commit is contained in:
26
finlog_app/app/assets/i18n/de.i18n.json
Normal file
26
finlog_app/app/assets/i18n/de.i18n.json
Normal file
@@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
finlog_app/app/assets/i18n/en.i18n.json
Normal file
26
finlog_app/app/assets/i18n/en.i18n.json
Normal file
@@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,5 +45,10 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>CFBundleLocalizations</key>
|
||||||
|
<array>
|
||||||
|
<string>de</string>
|
||||||
|
<string>en</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
182
finlog_app/app/lib/core/i18n/translations.g.dart
Normal file
182
finlog_app/app/lib/core/i18n/translations.g.dart
Normal file
@@ -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<AppLocale, Translations> {
|
||||||
|
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<Translations> build({
|
||||||
|
Map<String, Node>? 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<String, Node>? 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<AppLocale, Translations> {
|
||||||
|
TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance);
|
||||||
|
|
||||||
|
static InheritedLocaleData<AppLocale, Translations> of(BuildContext context) => InheritedLocaleData.of<AppLocale, Translations>(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<AppLocale, Translations> {
|
||||||
|
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<AppLocale> getLocaleStream() => instance.getLocaleStream();
|
||||||
|
static Future<AppLocale> setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale);
|
||||||
|
static Future<AppLocale> setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
|
||||||
|
static Future<AppLocale> useDeviceLocale() => instance.useDeviceLocale();
|
||||||
|
static Future<void> 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<AppLocale, Translations> {
|
||||||
|
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<Locale> get supportedLocales => instance.supportedLocales;
|
||||||
|
static List<String> get supportedLocalesRaw => instance.supportedLocalesRaw;
|
||||||
|
}
|
||||||
128
finlog_app/app/lib/core/i18n/translations_de.g.dart
Normal file
128
finlog_app/app/lib/core/i18n/translations_de.g.dart
Normal file
@@ -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: <root>
|
||||||
|
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<String, Node>? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, TranslationMetadata<AppLocale, Translations>? 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 <de>.
|
||||||
|
@override final TranslationMetadata<AppLocale, Translations> $meta;
|
||||||
|
|
||||||
|
/// Access flat map
|
||||||
|
@override dynamic operator[](String key) => $meta.getTranslation(key);
|
||||||
|
|
||||||
|
late final TranslationsDe _root = this; // ignore: unused_field
|
||||||
|
|
||||||
|
@override
|
||||||
|
TranslationsDe $copyWith({TranslationMetadata<AppLocale, Translations>? 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
161
finlog_app/app/lib/core/i18n/translations_en.g.dart
Normal file
161
finlog_app/app/lib/core/i18n/translations_en.g.dart
Normal file
@@ -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: <root>
|
||||||
|
typedef TranslationsEn = Translations; // ignore: unused_element
|
||||||
|
class Translations implements BaseTranslations<AppLocale, Translations> {
|
||||||
|
/// Returns the current translations of the given [context].
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// final t = Translations.of(context);
|
||||||
|
static Translations of(BuildContext context) => InheritedLocaleData.of<AppLocale, Translations>(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<String, Node>? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, TranslationMetadata<AppLocale, Translations>? 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 <en>.
|
||||||
|
@override final TranslationMetadata<AppLocale, Translations> $meta;
|
||||||
|
|
||||||
|
/// Access flat map
|
||||||
|
dynamic operator[](String key) => $meta.getTranslation(key);
|
||||||
|
|
||||||
|
late final Translations _root = this; // ignore: unused_field
|
||||||
|
|
||||||
|
Translations $copyWith({TranslationMetadata<AppLocale, Translations>? 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,44 +1,74 @@
|
|||||||
|
import 'package:app/core/i18n/translations.g.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttery/fluttery.dart';
|
import 'package:fluttery/fluttery.dart';
|
||||||
import 'package:fluttery/preferences.dart';
|
import 'package:fluttery/preferences.dart';
|
||||||
|
|
||||||
class LocaleController extends ChangeNotifier {
|
class LocaleController extends ChangeNotifier {
|
||||||
final Preferences _prefs;
|
final Preferences _prefs = App.service<Preferences>();
|
||||||
|
|
||||||
LocaleController() : _prefs = App.service<Preferences>();
|
|
||||||
|
|
||||||
static const _key = 'language';
|
static const _key = 'language';
|
||||||
|
LanguagePref _current = LanguagePref.system;
|
||||||
|
|
||||||
LanguagePref _current = LanguagePref.en; // Default = Englisch
|
/// Einmal beim App-Start aufrufen
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
final saved = await _prefs.getString(_key);
|
final saved = await _prefs.getString(_key);
|
||||||
_current = switch (saved) {
|
_current = _fromString(saved) ?? LanguagePref.system;
|
||||||
'de' => LanguagePref.de,
|
|
||||||
'system' => LanguagePref.system,
|
_applyToSlang(_current);
|
||||||
'en' || _ => LanguagePref.en, // default fallback = en
|
|
||||||
};
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLanguage(LanguagePref pref) {
|
/// Sprache ändern (persistieren + sofort anwenden)
|
||||||
if (_current == pref) return;
|
Future<void> setLanguage(LanguagePref lang) async {
|
||||||
_current = pref;
|
_current = lang;
|
||||||
|
await _prefs.setString(_key, lang.name);
|
||||||
|
|
||||||
|
_applyToSlang(lang);
|
||||||
notifyListeners();
|
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;
|
LanguagePref get language => _current;
|
||||||
|
|
||||||
Locale? get locale => _current.locale;
|
Locale? get locale => _current.locale;
|
||||||
|
|
||||||
|
bool get isSystem => _current == LanguagePref.system;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LanguagePref {
|
enum LanguagePref {
|
||||||
system(null), // folgt System
|
system(null),
|
||||||
de(Locale('de')), // Deutsch
|
de(Locale('de')),
|
||||||
en(Locale('en')); // Englisch (default)
|
en(Locale('en'));
|
||||||
|
|
||||||
final Locale? locale;
|
final Locale? locale;
|
||||||
|
|
||||||
const LanguagePref(this.locale);
|
const LanguagePref(this.locale);
|
||||||
|
|
||||||
|
String? get code => locale?.languageCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'package:app/core/app/router.dart';
|
import 'package:app/core/app/router.dart';
|
||||||
import 'package:app/core/app/startup/domain/initialize_app.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/locale_controller.dart';
|
||||||
import 'package:app/core/ui/controller/scale_controller.dart';
|
import 'package:app/core/ui/controller/scale_controller.dart';
|
||||||
import 'package:app/core/ui/controller/theme.dart';
|
import 'package:app/core/ui/controller/theme.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:fluttery/fluttery.dart';
|
import 'package:fluttery/fluttery.dart';
|
||||||
import 'package:fluttery/logger.dart';
|
import 'package:fluttery/logger.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -37,7 +39,9 @@ Future<void> main() async {
|
|||||||
ChangeNotifierProvider(create: (context) => scaleController),
|
ChangeNotifierProvider(create: (context) => scaleController),
|
||||||
ChangeNotifierProvider(create: (context) => localeController),
|
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) {
|
Widget build(BuildContext context) {
|
||||||
final theme = context.watch<ThemeController>();
|
final theme = context.watch<ThemeController>();
|
||||||
final textScale = context.watch<ScaleController>();
|
final textScale = context.watch<ScaleController>();
|
||||||
final localeCtrl = context.watch<LocaleController>();
|
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: theme,
|
animation: theme,
|
||||||
builder: (context, _) => MaterialApp.router(
|
builder: (context, _) => MaterialApp.router(
|
||||||
title: 'Finlog',
|
title: 'Finlog',
|
||||||
locale: localeCtrl.locale,
|
locale: TranslationProvider.of(context).flutterLocale,
|
||||||
supportedLocales: const [Locale('de'), Locale('en')],
|
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||||
|
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||||
theme: ThemeData.light(),
|
theme: ThemeData.light(),
|
||||||
darkTheme: ThemeData.dark(),
|
darkTheme: ThemeData.dark(),
|
||||||
themeMode: theme.themeMode,
|
themeMode: theme.themeMode,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:app/core/i18n/translations.g.dart';
|
||||||
import 'package:app/core/ui/panel.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/app/app_settings_view.dart';
|
||||||
import 'package:app/modules/settings/modules/help/feedback_view.dart';
|
import 'package:app/modules/settings/modules/help/feedback_view.dart';
|
||||||
@@ -12,8 +13,10 @@ class SettingsView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final t = Translations.of(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Einstellungen')),
|
appBar: AppBar(title: Text(t.settings.title)),
|
||||||
body: PanelNavigator(rootBuilder: (ctx) => _CategoryList()),
|
body: PanelNavigator(rootBuilder: (ctx) => _CategoryList()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -24,6 +27,7 @@ class _CategoryList extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final scheme = Theme.of(context).colorScheme;
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
final t = Translations.of(context);
|
||||||
|
|
||||||
Widget tile(IconData icon, String label, Widget Function() detail) {
|
Widget tile(IconData icon, String label, Widget Function() detail) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -53,29 +57,41 @@ class _CategoryList extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const _SectionHeader('App-Einstellungen'),
|
_SectionHeader(t.settings.sections.app),
|
||||||
tile(Icons.tune, 'App-Einstellungen', () => const AppSettingsView()),
|
tile(
|
||||||
|
Icons.tune,
|
||||||
|
t.settings.items.appSettings,
|
||||||
|
() => const AppSettingsView(),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
const _SectionHeader('Meine Daten'),
|
_SectionHeader(t.settings.sections.account),
|
||||||
tile(
|
tile(
|
||||||
Icons.badge_outlined,
|
Icons.badge_outlined,
|
||||||
'Persönliche Daten',
|
t.settings.items.personalData,
|
||||||
() => const PersonalPanel(),
|
() => const PersonalPanel(),
|
||||||
),
|
),
|
||||||
tile(
|
tile(
|
||||||
Icons.manage_accounts_outlined,
|
Icons.manage_accounts_outlined,
|
||||||
'Kontoverwaltung',
|
t.settings.items.accountManagement,
|
||||||
() => const AccountPanel(),
|
() => const AccountPanel(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
const _SectionHeader('Hilfe'),
|
_SectionHeader(t.settings.sections.help),
|
||||||
tile(Icons.help_outline, 'Hilfe', () => const HelpPanel()),
|
tile(
|
||||||
tile(Icons.feedback_outlined, 'Feedback', () => const FeedbackPanel()),
|
Icons.help_outline,
|
||||||
|
t.settings.items.helpCenter,
|
||||||
|
() => const HelpPanel(),
|
||||||
|
),
|
||||||
|
tile(
|
||||||
|
Icons.feedback_outlined,
|
||||||
|
t.settings.items.feedback,
|
||||||
|
() => const FeedbackPanel(),
|
||||||
|
),
|
||||||
tile(
|
tile(
|
||||||
Icons.gavel_outlined,
|
Icons.gavel_outlined,
|
||||||
'Rechtliches & Datenschutz',
|
t.settings.items.legalPrivacy, // "Rechtliches & Datenschutz"
|
||||||
() => const LegalPanel(),
|
() => const LegalPanel(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -83,12 +99,10 @@ class _CategoryList extends StatelessWidget {
|
|||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.logout),
|
leading: const Icon(Icons.logout),
|
||||||
title: const Text('Abmelden'),
|
title: Text(t.settings.items.logout),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(content: Text(t.settings.messages.logoutNotImplemented)),
|
||||||
content: Text('Logout… (noch nicht implementiert)'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -31,26 +31,23 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
fluttery:
|
fluttery:
|
||||||
path: ../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
|
cupertino_icons: ^1.0.8
|
||||||
go_router: ^16.2.2
|
go_router: ^16.2.2
|
||||||
animations: ^2.0.11
|
animations: ^2.0.11
|
||||||
provider: ^6.1.5+1
|
provider: ^6.1.5+1
|
||||||
|
slang: ^4.8.1
|
||||||
|
slang_flutter: ^4.8.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
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
|
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
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|||||||
10
finlog_app/app/slang.yaml
Normal file
10
finlog_app/app/slang.yaml
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user