From 6f051252b5c2ef66837ee7b8bf92e657417a54e0 Mon Sep 17 00:00:00 2001 From: Biz Date: Mon, 7 Jul 2025 18:20:45 -0700 Subject: [PATCH 1/5] fix: remove hardcoded Spanish locale override in favor of automatic detection --- lib/core/app.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/core/app.dart b/lib/core/app.dart index 43111a8f..0ffec7d1 100644 --- a/lib/core/app.dart +++ b/lib/core/app.dart @@ -54,8 +54,7 @@ class _MostroAppState extends ConsumerState { theme: AppTheme.theme, darkTheme: AppTheme.theme, routerConfig: goRouter, - // Force Spanish locale for testing if device is Spanish - locale: systemLocale.languageCode == 'es' ? const Locale('es') : null, + // Let localeResolutionCallback handle all locale detection localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, From a2281f754f5adb693762b4690fd9a3fb8a72f642 Mon Sep 17 00:00:00 2001 From: Biz Date: Mon, 7 Jul 2025 18:47:06 -0700 Subject: [PATCH 2/5] feat: add language selection with system default option and localization support --- lib/core/app.dart | 7 +- lib/features/settings/settings.dart | 6 ++ lib/features/settings/settings_notifier.dart | 5 ++ lib/features/settings/settings_screen.dart | 28 ++++++- lib/l10n/intl_en.arb | 7 +- lib/l10n/intl_es.arb | 6 ++ lib/l10n/intl_it.arb | 6 ++ lib/shared/widgets/language_selector.dart | 81 ++++++++++++++++++++ test/mocks.mocks.dart | 1 + 9 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 lib/shared/widgets/language_selector.dart diff --git a/lib/core/app.dart b/lib/core/app.dart index 0ffec7d1..d9f3bafa 100644 --- a/lib/core/app.dart +++ b/lib/core/app.dart @@ -10,6 +10,7 @@ import 'package:mostro_mobile/generated/l10n.dart'; import 'package:mostro_mobile/features/auth/notifiers/auth_state.dart'; import 'package:mostro_mobile/services/lifecycle_manager.dart'; import 'package:mostro_mobile/shared/providers/app_init_provider.dart'; +import 'package:mostro_mobile/features/settings/settings_provider.dart'; class MostroApp extends ConsumerStatefulWidget { const MostroApp({super.key}); @@ -48,13 +49,17 @@ class _MostroAppState extends ConsumerState { }); final systemLocale = ui.PlatformDispatcher.instance.locale; + final settings = ref.watch(settingsProvider); return MaterialApp.router( title: 'Mostro', theme: AppTheme.theme, darkTheme: AppTheme.theme, routerConfig: goRouter, - // Let localeResolutionCallback handle all locale detection + // Use language override from settings if available, otherwise let callback handle detection + locale: settings.selectedLanguage != null + ? Locale(settings.selectedLanguage!) + : null, localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, diff --git a/lib/features/settings/settings.dart b/lib/features/settings/settings.dart index 1bd5d40c..4faf4b1a 100644 --- a/lib/features/settings/settings.dart +++ b/lib/features/settings/settings.dart @@ -3,12 +3,14 @@ class Settings { final List relays; final String mostroPublicKey; final String? defaultFiatCode; + final String? selectedLanguage; // null means use system locale Settings({ required this.relays, required this.fullPrivacyMode, required this.mostroPublicKey, this.defaultFiatCode, + this.selectedLanguage, }); Settings copyWith({ @@ -16,12 +18,14 @@ class Settings { bool? privacyModeSetting, String? mostroInstance, String? defaultFiatCode, + String? selectedLanguage, }) { return Settings( relays: relays ?? this.relays, fullPrivacyMode: privacyModeSetting ?? fullPrivacyMode, mostroPublicKey: mostroInstance ?? mostroPublicKey, defaultFiatCode: defaultFiatCode ?? this.defaultFiatCode, + selectedLanguage: selectedLanguage ?? this.selectedLanguage, ); } @@ -30,6 +34,7 @@ class Settings { 'fullPrivacyMode': fullPrivacyMode, 'mostroPublicKey': mostroPublicKey, 'defaultFiatCode': defaultFiatCode, + 'selectedLanguage': selectedLanguage, }; factory Settings.fromJson(Map json) { @@ -38,6 +43,7 @@ class Settings { fullPrivacyMode: json['fullPrivacyMode'] as bool, mostroPublicKey: json['mostroPublicKey'], defaultFiatCode: json['defaultFiatCode'], + selectedLanguage: json['selectedLanguage'], ); } } diff --git a/lib/features/settings/settings_notifier.dart b/lib/features/settings/settings_notifier.dart index 55107834..db9c87b2 100644 --- a/lib/features/settings/settings_notifier.dart +++ b/lib/features/settings/settings_notifier.dart @@ -52,6 +52,11 @@ class SettingsNotifier extends StateNotifier { await _saveToPrefs(); } + Future updateSelectedLanguage(String? newValue) async { + state = state.copyWith(selectedLanguage: newValue); + await _saveToPrefs(); + } + Future _saveToPrefs() async { final jsonString = jsonEncode(state.toJson()); await _prefs.setString(_storageKey, jsonString); diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index ece5ddf0..2994b770 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -7,6 +7,7 @@ import 'package:mostro_mobile/features/relays/widgets/relay_selector.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/shared/widgets/currency_combo_box.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; +import 'package:mostro_mobile/shared/widgets/language_selector.dart'; import 'package:mostro_mobile/generated/l10n.dart'; class SettingsScreen extends ConsumerWidget { @@ -44,7 +45,32 @@ class SettingsScreen extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, spacing: 24, children: [ - // General Settings + // Language Settings + CustomCard( + color: AppTheme.dark2, + padding: const EdgeInsets.all(16), + child: Column( + spacing: 16, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8, + children: [ + const Icon( + Icons.language, + color: AppTheme.mostroGreen, + ), + Text(S.of(context)!.language, style: textTheme.titleLarge), + ], + ), + Text(S.of(context)!.chooseLanguageDescription, + style: textTheme.bodyMedium + ?.copyWith(color: AppTheme.grey2)), + const LanguageSelector(), + ], + ), + ), + // Currency Settings CustomCard( color: AppTheme.dark2, padding: const EdgeInsets.all(16), diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 121a4019..61a05f1d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -399,5 +399,10 @@ "share": "Share", "failedToShareInvoice": "Failed to share invoice. Please try copying instead.", "openWallet": "OPEN WALLET", - "done": "DONE" + "language": "Language", + "systemDefault": "System Default", + "english": "English", + "spanish": "Spanish", + "italian": "Italian", + "chooseLanguageDescription": "Choose your preferred language or use system default" } diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 71238fc7..e5695dec 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -399,5 +399,11 @@ "share": "Compartir", "failedToShareInvoice": "Error al compartir factura. Por favor intenta copiarla en su lugar.", "openWallet": "ABRIR BILLETERA", + "language": "Idioma", + "systemDefault": "Predeterminado del sistema", + "english": "Inglés", + "spanish": "Español", + "italian": "Italiano", + "chooseLanguageDescription": "Elige tu idioma preferido o usa el predeterminado del sistema", "done": "HECHO" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index aab46113..95b1c512 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -399,5 +399,11 @@ "share": "Condividi", "failedToShareInvoice": "Errore nel condividere la fattura. Per favore prova a copiarla invece.", "openWallet": "APRI PORTAFOGLIO", + "language": "Lingua", + "systemDefault": "Predefinito di sistema", + "english": "Inglese", + "spanish": "Spagnolo", + "italian": "Italiano", + "chooseLanguageDescription": "Scegli la tua lingua preferita o usa il predefinito di sistema", "done": "FATTO" } diff --git a/lib/shared/widgets/language_selector.dart b/lib/shared/widgets/language_selector.dart new file mode 100644 index 00000000..57059aa0 --- /dev/null +++ b/lib/shared/widgets/language_selector.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/core/app_theme.dart'; +import 'package:mostro_mobile/features/settings/settings_provider.dart'; + + +class LanguageSelector extends ConsumerWidget { + const LanguageSelector({super.key}); + + static const Map _languageOptions = { + null: 'System Default', // Will be localized in build method + 'en': 'English', + 'es': 'Español', + 'it': 'Italiano', + }; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + final currentLanguage = settings.selectedLanguage; + + return Container( + decoration: BoxDecoration( + color: AppTheme.backgroundInput, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppTheme.grey2, width: 1), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: currentLanguage, + isExpanded: true, + dropdownColor: AppTheme.backgroundInput, + style: const TextStyle(color: AppTheme.cream1), + icon: const Icon(Icons.arrow_drop_down, color: AppTheme.cream1), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + items: _languageOptions.entries.map((entry) { + final languageCode = entry.key; + final languageName = entry.value; + + // Localize "System Default" text + final displayName = languageCode == null + ? 'System Default' // Fallback text since systemDefault may not exist + : languageName; + + return DropdownMenuItem( + value: languageCode, + child: Row( + children: [ + Icon( + languageCode == null ? Icons.phone_android : Icons.language, + color: AppTheme.mostroGreen, + size: 20, + ), + const SizedBox(width: 12), + Text( + displayName, + style: const TextStyle( + color: AppTheme.cream1, + fontSize: 16, + ), + ), + if (languageCode == currentLanguage) ...[ + const Spacer(), + const Icon( + Icons.check, + color: AppTheme.mostroGreen, + size: 20, + ), + ], + ], + ), + ); + }).toList(), + onChanged: (String? newLanguage) { + ref.read(settingsProvider.notifier).updateSelectedLanguage(newLanguage); + }, + ), + ), + ); + } +} diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index 760793a9..e2757ae2 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -401,6 +401,7 @@ class MockOpenOrdersRepository extends _i1.Mock /// A class which mocks [SharedPreferencesAsync]. /// /// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable class MockSharedPreferencesAsync extends _i1.Mock implements _i11.SharedPreferencesAsync { MockSharedPreferencesAsync() { From 237df6e511181bdd00054225141dbf05025e1ae7 Mon Sep 17 00:00:00 2001 From: Biz Date: Mon, 7 Jul 2025 23:24:19 -0700 Subject: [PATCH 3/5] refactor: implement localized language names in language selector dropdown --- lib/shared/widgets/language_selector.dart | 44 +++++++++++++++-------- test/mocks.mocks.dart | 1 - 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/lib/shared/widgets/language_selector.dart b/lib/shared/widgets/language_selector.dart index 57059aa0..855bef2a 100644 --- a/lib/shared/widgets/language_selector.dart +++ b/lib/shared/widgets/language_selector.dart @@ -2,16 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; - +import 'package:mostro_mobile/generated/l10n.dart'; class LanguageSelector extends ConsumerWidget { const LanguageSelector({super.key}); - static const Map _languageOptions = { - null: 'System Default', // Will be localized in build method - 'en': 'English', - 'es': 'Español', - 'it': 'Italiano', + static const Map _languageKeys = { + null: 'systemDefault', + 'en': 'english', + 'es': 'spanish', + 'it': 'italian', }; @override @@ -33,15 +33,12 @@ class LanguageSelector extends ConsumerWidget { style: const TextStyle(color: AppTheme.cream1), icon: const Icon(Icons.arrow_drop_down, color: AppTheme.cream1), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - items: _languageOptions.entries.map((entry) { + items: _languageKeys.entries.map((entry) { final languageCode = entry.key; - final languageName = entry.value; - - // Localize "System Default" text - final displayName = languageCode == null - ? 'System Default' // Fallback text since systemDefault may not exist - : languageName; - + final languageKey = entry.value; + + final displayName = _getLocalizedLanguageName(context, languageKey); + return DropdownMenuItem( value: languageCode, child: Row( @@ -72,10 +69,27 @@ class LanguageSelector extends ConsumerWidget { ); }).toList(), onChanged: (String? newLanguage) { - ref.read(settingsProvider.notifier).updateSelectedLanguage(newLanguage); + ref + .read(settingsProvider.notifier) + .updateSelectedLanguage(newLanguage); }, ), ), ); } + + String _getLocalizedLanguageName(BuildContext context, String key) { + switch (key) { + case 'systemDefault': + return S.of(context)!.systemDefault; + case 'english': + return S.of(context)!.english; + case 'spanish': + return S.of(context)!.spanish; + case 'italian': + return S.of(context)!.italian; + default: + return key; + } + } } diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index e2757ae2..760793a9 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -401,7 +401,6 @@ class MockOpenOrdersRepository extends _i1.Mock /// A class which mocks [SharedPreferencesAsync]. /// /// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable class MockSharedPreferencesAsync extends _i1.Mock implements _i11.SharedPreferencesAsync { MockSharedPreferencesAsync() { From 3470b6ba812c8a523b7514349153ff79c133a9a5 Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 8 Jul 2025 11:39:09 -0700 Subject: [PATCH 4/5] feat: implement system locale tracking and language selection handling --- lib/core/app.dart | 13 +++++---- lib/features/settings/settings_notifier.dart | 1 + lib/shared/notifiers/locale_notifier.dart | 30 ++++++++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 lib/shared/notifiers/locale_notifier.dart diff --git a/lib/core/app.dart b/lib/core/app.dart index d9f3bafa..404dd252 100644 --- a/lib/core/app.dart +++ b/lib/core/app.dart @@ -1,4 +1,4 @@ -import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -11,6 +11,7 @@ import 'package:mostro_mobile/features/auth/notifiers/auth_state.dart'; import 'package:mostro_mobile/services/lifecycle_manager.dart'; import 'package:mostro_mobile/shared/providers/app_init_provider.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; +import 'package:mostro_mobile/shared/notifiers/locale_notifier.dart'; class MostroApp extends ConsumerStatefulWidget { const MostroApp({super.key}); @@ -48,7 +49,8 @@ class _MostroAppState extends ConsumerState { }); }); - final systemLocale = ui.PlatformDispatcher.instance.locale; + // Watch both system locale and settings for changes + final systemLocale = ref.watch(systemLocaleProvider); final settings = ref.watch(settingsProvider); return MaterialApp.router( @@ -59,7 +61,7 @@ class _MostroAppState extends ConsumerState { // Use language override from settings if available, otherwise let callback handle detection locale: settings.selectedLanguage != null ? Locale(settings.selectedLanguage!) - : null, + : systemLocale, localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, @@ -68,6 +70,7 @@ class _MostroAppState extends ConsumerState { ], supportedLocales: S.supportedLocales, localeResolutionCallback: (locale, supportedLocales) { + // Use the current system locale from our provider final deviceLocale = locale ?? systemLocale; // Check for Spanish language code (es) - includes es_AR, es_ES, etc. @@ -82,8 +85,8 @@ class _MostroAppState extends ConsumerState { } } - // If no match found, return English as fallback - return const Locale('en'); + // If no match found, return Spanish as fallback + return const Locale('es'); }, ); }, diff --git a/lib/features/settings/settings_notifier.dart b/lib/features/settings/settings_notifier.dart index db9c87b2..a87525c7 100644 --- a/lib/features/settings/settings_notifier.dart +++ b/lib/features/settings/settings_notifier.dart @@ -16,6 +16,7 @@ class SettingsNotifier extends StateNotifier { relays: Config.nostrRelays, fullPrivacyMode: Config.fullPrivacyMode, mostroPublicKey: Config.mostroPubKey, + selectedLanguage: null, ); } diff --git a/lib/shared/notifiers/locale_notifier.dart b/lib/shared/notifiers/locale_notifier.dart new file mode 100644 index 00000000..052544e2 --- /dev/null +++ b/lib/shared/notifiers/locale_notifier.dart @@ -0,0 +1,30 @@ +import 'dart:ui' as ui; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// Notifier that tracks system locale changes +class LocaleNotifier extends StateNotifier { + LocaleNotifier() : super(ui.PlatformDispatcher.instance.locale) { + // Listen to system locale changes + ui.PlatformDispatcher.instance.onLocaleChanged = _onLocaleChanged; + } + + void _onLocaleChanged() { + final newLocale = ui.PlatformDispatcher.instance.locale; + if (state != newLocale) { + state = newLocale; + } + } + + @override + void dispose() { + // Clean up the listener + ui.PlatformDispatcher.instance.onLocaleChanged = null; + super.dispose(); + } +} + +/// Provider for system locale changes +final systemLocaleProvider = StateNotifierProvider((ref) { + return LocaleNotifier(); +}); From f66da7de76b46c62bae575127e550f074cab4910 Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 8 Jul 2025 13:28:24 -0700 Subject: [PATCH 5/5] refactor: simplify language selector UI and update settings handling to properly save selected language --- lib/features/settings/settings.dart | 2 +- lib/shared/widgets/language_selector.dart | 15 +++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/features/settings/settings.dart b/lib/features/settings/settings.dart index 4faf4b1a..0e47abd3 100644 --- a/lib/features/settings/settings.dart +++ b/lib/features/settings/settings.dart @@ -25,7 +25,7 @@ class Settings { fullPrivacyMode: privacyModeSetting ?? fullPrivacyMode, mostroPublicKey: mostroInstance ?? mostroPublicKey, defaultFiatCode: defaultFiatCode ?? this.defaultFiatCode, - selectedLanguage: selectedLanguage ?? this.selectedLanguage, + selectedLanguage: selectedLanguage, ); } diff --git a/lib/shared/widgets/language_selector.dart b/lib/shared/widgets/language_selector.dart index 855bef2a..d1ab54d9 100644 --- a/lib/shared/widgets/language_selector.dart +++ b/lib/shared/widgets/language_selector.dart @@ -21,18 +21,17 @@ class LanguageSelector extends ConsumerWidget { return Container( decoration: BoxDecoration( - color: AppTheme.backgroundInput, + color: AppTheme.dark1, borderRadius: BorderRadius.circular(8), - border: Border.all(color: AppTheme.grey2, width: 1), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: currentLanguage, isExpanded: true, - dropdownColor: AppTheme.backgroundInput, + dropdownColor: AppTheme.dark1, style: const TextStyle(color: AppTheme.cream1), icon: const Icon(Icons.arrow_drop_down, color: AppTheme.cream1), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), items: _languageKeys.entries.map((entry) { final languageCode = entry.key; final languageKey = entry.value; @@ -56,14 +55,6 @@ class LanguageSelector extends ConsumerWidget { fontSize: 16, ), ), - if (languageCode == currentLanguage) ...[ - const Spacer(), - const Icon( - Icons.check, - color: AppTheme.mostroGreen, - size: 20, - ), - ], ], ), );