Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions lib/core/app.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,6 +10,8 @@ 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';
import 'package:mostro_mobile/shared/notifiers/locale_notifier.dart';

class MostroApp extends ConsumerStatefulWidget {
const MostroApp({super.key});
Expand Down Expand Up @@ -47,15 +49,19 @@ class _MostroAppState extends ConsumerState<MostroApp> {
});
});

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(
title: 'Mostro',
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,
// Use language override from settings if available, otherwise let callback handle detection
locale: settings.selectedLanguage != null
? Locale(settings.selectedLanguage!)
: systemLocale,
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
Expand All @@ -64,6 +70,7 @@ class _MostroAppState extends ConsumerState<MostroApp> {
],
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.
Expand All @@ -78,8 +85,8 @@ class _MostroAppState extends ConsumerState<MostroApp> {
}
}

// If no match found, return English as fallback
return const Locale('en');
// If no match found, return Spanish as fallback
return const Locale('es');
},
);
},
Expand Down
6 changes: 6 additions & 0 deletions lib/features/settings/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,29 @@ class Settings {
final List<String> 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({
List<String>? relays,
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,
);
}

Expand All @@ -30,6 +34,7 @@ class Settings {
'fullPrivacyMode': fullPrivacyMode,
'mostroPublicKey': mostroPublicKey,
'defaultFiatCode': defaultFiatCode,
'selectedLanguage': selectedLanguage,
};

factory Settings.fromJson(Map<String, dynamic> json) {
Expand All @@ -38,6 +43,7 @@ class Settings {
fullPrivacyMode: json['fullPrivacyMode'] as bool,
mostroPublicKey: json['mostroPublicKey'],
defaultFiatCode: json['defaultFiatCode'],
selectedLanguage: json['selectedLanguage'],
);
}
}
6 changes: 6 additions & 0 deletions lib/features/settings/settings_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class SettingsNotifier extends StateNotifier<Settings> {
relays: Config.nostrRelays,
fullPrivacyMode: Config.fullPrivacyMode,
mostroPublicKey: Config.mostroPubKey,
selectedLanguage: null,
);
}

Expand Down Expand Up @@ -52,6 +53,11 @@ class SettingsNotifier extends StateNotifier<Settings> {
await _saveToPrefs();
}

Future<void> updateSelectedLanguage(String? newValue) async {
state = state.copyWith(selectedLanguage: newValue);
await _saveToPrefs();
}

Future<void> _saveToPrefs() async {
final jsonString = jsonEncode(state.toJson());
await _prefs.setString(_storageKey, jsonString);
Expand Down
28 changes: 27 additions & 1 deletion lib/features/settings/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down
7 changes: 6 additions & 1 deletion lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
6 changes: 6 additions & 0 deletions lib/l10n/intl_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
6 changes: 6 additions & 0 deletions lib/l10n/intl_it.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
30 changes: 30 additions & 0 deletions lib/shared/notifiers/locale_notifier.dart
Original file line number Diff line number Diff line change
@@ -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<Locale> {
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<LocaleNotifier, Locale>((ref) {
return LocaleNotifier();
});
86 changes: 86 additions & 0 deletions lib/shared/widgets/language_selector.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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<String?, String> _languageKeys = {
null: 'systemDefault',
'en': 'english',
'es': 'spanish',
'it': 'italian',
};

@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(settingsProvider);
final currentLanguage = settings.selectedLanguage;

return Container(
decoration: BoxDecoration(
color: AppTheme.dark1,
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String?>(
value: currentLanguage,
isExpanded: true,
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: 6),
items: _languageKeys.entries.map((entry) {
final languageCode = entry.key;
final languageKey = entry.value;

final displayName = _getLocalizedLanguageName(context, languageKey);

return DropdownMenuItem<String?>(
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,
),
),
],
),
);
}).toList(),
onChanged: (String? 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;
}
}
}