feat(i18n): add localization foundation
This commit is contained in:
@@ -2,10 +2,11 @@
|
||||
|
||||
## Internacionalización AAA
|
||||
|
||||
- [x] Diseñar una base de internacionalización profesional con ficheros ARB separados por idioma.
|
||||
- [x] Permitir que el usuario cambie el idioma manualmente desde la aplicación, sin depender únicamente del idioma del sistema.
|
||||
- [x] Añadir traducción inicial español/inglés para el shell, navegación, timer de sueño y selector de idioma.
|
||||
- [ ] Repasar absolutamente todos los literales de la aplicación en todas las pantallas, componentes, servicios con mensajes visibles y notificaciones.
|
||||
- [ ] Diseñar una arquitectura de internacionalización profesional con ficheros separados por idioma.
|
||||
- [ ] Permitir que el usuario cambie el idioma manualmente desde la aplicación, sin depender únicamente del idioma del sistema.
|
||||
- [ ] Soportar formatos locales de fecha, hora, números y duración.
|
||||
- [ ] Soportar formatos locales de fecha, hora, números y duración usando helpers centralizados.
|
||||
- [ ] Resolver correctamente singular/plural y variantes por cantidad, por ejemplo `1 emisora` vs `2 emisoras`.
|
||||
- [ ] Preparar traducciones para la mayoría de idiomas más usados del planeta.
|
||||
- [ ] Revisar la aplicación de Farolero como referencia para detectar el conjunto de idiomas que nos interesa mantener.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
arb-dir: lib/l10n
|
||||
template-arb-file: app_es.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
output-class: AppLocalizations
|
||||
output-dir: lib/l10n/gen
|
||||
nullable-getter: false
|
||||
+74
-44
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'estado/estado_radio.dart';
|
||||
import 'estado/estado_alarmas.dart';
|
||||
import 'estado/estado_idioma.dart';
|
||||
import 'l10n/gen/app_localizations.dart';
|
||||
import 'modelos/alarma_musical.dart';
|
||||
import 'pantallas/pantalla_alarmas.dart';
|
||||
import 'pantallas/pantalla_alarma_sonando.dart';
|
||||
@@ -27,14 +29,21 @@ class PluriWaveApp extends StatelessWidget {
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => EstadoRadio()),
|
||||
ChangeNotifierProvider(create: (_) => EstadoAlarmas()),
|
||||
ChangeNotifierProvider(create: (_) => EstadoIdioma()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'PluriWave',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: PluriWaveTheme.dark(),
|
||||
darkTheme: PluriWaveTheme.dark(),
|
||||
themeMode: ThemeMode.dark,
|
||||
home: const _PaginaPrincipal(),
|
||||
child: Consumer<EstadoIdioma>(
|
||||
builder:
|
||||
(context, estadoIdioma, _) => MaterialApp(
|
||||
title: 'PluriWave',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: PluriWaveTheme.dark(),
|
||||
darkTheme: PluriWaveTheme.dark(),
|
||||
themeMode: ThemeMode.dark,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: estadoIdioma.localeSeleccionado,
|
||||
home: const _PaginaPrincipal(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -63,27 +72,12 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
PantallaAjustes(),
|
||||
];
|
||||
|
||||
static const _navItems = [
|
||||
PluriNavItem(
|
||||
glyph: PluriIconGlyph.home,
|
||||
label: 'Inicio',
|
||||
),
|
||||
PluriNavItem(
|
||||
glyph: PluriIconGlyph.search,
|
||||
label: 'Buscar',
|
||||
),
|
||||
PluriNavItem(
|
||||
glyph: PluriIconGlyph.favorites,
|
||||
label: 'Favoritos',
|
||||
),
|
||||
PluriNavItem(
|
||||
glyph: PluriIconGlyph.alarm,
|
||||
label: 'Alarmas',
|
||||
),
|
||||
PluriNavItem(
|
||||
glyph: PluriIconGlyph.settings,
|
||||
label: 'Ajustes',
|
||||
),
|
||||
List<PluriNavItem> _navItems(AppLocalizations l10n) => [
|
||||
PluriNavItem(glyph: PluriIconGlyph.home, label: l10n.navHome),
|
||||
PluriNavItem(glyph: PluriIconGlyph.search, label: l10n.navSearch),
|
||||
PluriNavItem(glyph: PluriIconGlyph.favorites, label: l10n.navFavorites),
|
||||
PluriNavItem(glyph: PluriIconGlyph.alarm, label: l10n.navAlarms),
|
||||
PluriNavItem(glyph: PluriIconGlyph.settings, label: l10n.navSettings),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -101,7 +95,10 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
SnackBar(
|
||||
content: Text(msg),
|
||||
duration: const Duration(seconds: 3),
|
||||
action: SnackBarAction(label: 'OK', onPressed: () {}),
|
||||
action: SnackBarAction(
|
||||
label: AppLocalizations.of(context).actionOk,
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -133,13 +130,15 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return PluriWaveScaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('PluriWave'),
|
||||
title: Text(l10n.appTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bedtime_outlined),
|
||||
tooltip: 'Timer de sueño',
|
||||
tooltip: l10n.sleepTimer,
|
||||
onPressed: () => _mostrarTimerDialog(context),
|
||||
),
|
||||
],
|
||||
@@ -177,7 +176,7 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
children: [
|
||||
const MiniReproductor(),
|
||||
PluriBottomNavigation(
|
||||
items: _navItems,
|
||||
items: _navItems(l10n),
|
||||
selectedIndex: _indice,
|
||||
onSelected: (i) => setState(() => _indice = i),
|
||||
),
|
||||
@@ -212,7 +211,11 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
setState(() => _indice = 3);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Omitida esta ejecución de ${alarma.nombre}.'),
|
||||
content: Text(
|
||||
AppLocalizations.of(context).skipCurrentAlarmExecution(
|
||||
alarma.nombre,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -252,12 +255,12 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Timer de sueño',
|
||||
AppLocalizations.of(ctx).sleepTimer,
|
||||
style: Theme.of(ctx).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: PluriLayout.sectionGap),
|
||||
Text(
|
||||
'Apagado suave de la radio con cuenta atrás exacta.',
|
||||
AppLocalizations.of(ctx).sleepTimerDescription,
|
||||
style: Theme.of(ctx).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: PluriLayout.panelGap),
|
||||
@@ -280,7 +283,9 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
estado.cancelarTimer();
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text('Cancelar timer'),
|
||||
child: Text(
|
||||
AppLocalizations.of(ctx).cancelTimer,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -308,7 +313,9 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
),
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.tune_rounded, size: 18),
|
||||
label: const Text('Otro'),
|
||||
label: Text(
|
||||
AppLocalizations.of(ctx).optionOther,
|
||||
),
|
||||
onPressed: () async {
|
||||
final duracion =
|
||||
await _pedirDuracionPersonalizada(ctx);
|
||||
@@ -382,7 +389,11 @@ class _TimerPersonalizadoSheetState extends State<_TimerPersonalizadoSheet> {
|
||||
);
|
||||
if (duracion <= Duration.zero) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Elegí una duración mayor que cero.')),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).durationGreaterThanZero,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -403,30 +414,49 @@ class _TimerPersonalizadoSheetState extends State<_TimerPersonalizadoSheet> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Duración personalizada',
|
||||
AppLocalizations.of(context).customDurationTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: PluriLayout.sectionGap),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _campoTiempo(_horasCtrl, 'Horas')),
|
||||
Expanded(
|
||||
child: _campoTiempo(
|
||||
_horasCtrl,
|
||||
AppLocalizations.of(context).hoursLabel,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: PluriLayout.compactGap),
|
||||
Expanded(child: _campoTiempo(_minutosCtrl, 'Minutos')),
|
||||
Expanded(
|
||||
child: _campoTiempo(
|
||||
_minutosCtrl,
|
||||
AppLocalizations.of(context).minutesLabel,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: PluriLayout.compactGap),
|
||||
Expanded(child: _campoTiempo(_segundosCtrl, 'Segundos')),
|
||||
Expanded(
|
||||
child: _campoTiempo(
|
||||
_segundosCtrl,
|
||||
AppLocalizations.of(context).secondsLabel,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: PluriLayout.compactGap),
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Guardar como acceso rápido'),
|
||||
title: Text(
|
||||
AppLocalizations.of(context).saveQuickAccess,
|
||||
),
|
||||
value: _guardarPreset,
|
||||
onChanged: (value) => setState(() => _guardarPreset = value),
|
||||
),
|
||||
const SizedBox(height: PluriLayout.sectionGap),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.bedtime_rounded),
|
||||
label: const Text('Iniciar timer'),
|
||||
label: Text(
|
||||
AppLocalizations.of(context).startTimer,
|
||||
),
|
||||
onPressed: _confirmar,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class EstadoIdioma extends ChangeNotifier {
|
||||
EstadoIdioma({SharedPreferences? sharedPreferences})
|
||||
: _sharedPreferences = sharedPreferences {
|
||||
_cargar();
|
||||
}
|
||||
|
||||
static const String _keyLocale = 'idioma_manual_v1';
|
||||
|
||||
final SharedPreferences? _sharedPreferences;
|
||||
|
||||
Locale? _localeSeleccionado;
|
||||
|
||||
Locale? get localeSeleccionado => _localeSeleccionado;
|
||||
bool get usaSistema => _localeSeleccionado == null;
|
||||
|
||||
Future<void> seleccionarSistema() async {
|
||||
_localeSeleccionado = null;
|
||||
notifyListeners();
|
||||
final prefs = await _resolverPrefs();
|
||||
await prefs.remove(_keyLocale);
|
||||
}
|
||||
|
||||
Future<void> seleccionarLocale(Locale locale) async {
|
||||
final tag = _serializarLocale(locale);
|
||||
_localeSeleccionado = locale;
|
||||
notifyListeners();
|
||||
final prefs = await _resolverPrefs();
|
||||
await prefs.setString(_keyLocale, tag);
|
||||
}
|
||||
|
||||
Future<void> _cargar() async {
|
||||
final prefs = await _resolverPrefs();
|
||||
final localeGuardado = prefs.getString(_keyLocale);
|
||||
_localeSeleccionado = _parsearLocale(localeGuardado);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<SharedPreferences> _resolverPrefs() async {
|
||||
return _sharedPreferences ?? SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
Locale? _parsearLocale(String? value) {
|
||||
if (value == null || value.trim().isEmpty) return null;
|
||||
final partes = value.split('_');
|
||||
final languageCode = partes.first;
|
||||
if (languageCode.isEmpty) return null;
|
||||
final countryCode = partes.length > 1 && partes[1].isNotEmpty
|
||||
? partes[1]
|
||||
: null;
|
||||
return Locale.fromSubtags(
|
||||
languageCode: languageCode,
|
||||
countryCode: countryCode,
|
||||
);
|
||||
}
|
||||
|
||||
String _serializarLocale(Locale locale) {
|
||||
final countryCode = locale.countryCode;
|
||||
if (countryCode == null || countryCode.isEmpty) {
|
||||
return locale.languageCode;
|
||||
}
|
||||
return '${locale.languageCode}_$countryCode';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"appTitle": "PluriWave",
|
||||
"navHome": "Home",
|
||||
"navSearch": "Search",
|
||||
"navFavorites": "Favorites",
|
||||
"navAlarms": "Alarms",
|
||||
"navSettings": "Settings",
|
||||
"actionOk": "OK",
|
||||
"sleepTimer": "Sleep timer",
|
||||
"sleepTimerDescription": "Smooth radio shutdown with an exact countdown.",
|
||||
"cancelTimer": "Cancel timer",
|
||||
"optionOther": "Other",
|
||||
"customDurationTitle": "Custom duration",
|
||||
"durationGreaterThanZero": "Choose a duration greater than zero.",
|
||||
"hoursLabel": "Hours",
|
||||
"minutesLabel": "Minutes",
|
||||
"secondsLabel": "Seconds",
|
||||
"saveQuickAccess": "Save as quick access",
|
||||
"startTimer": "Start timer",
|
||||
"skipCurrentAlarmExecution": "Skipped this execution of {alarmName}.",
|
||||
"@skipCurrentAlarmExecution": {
|
||||
"placeholders": {
|
||||
"alarmName": {}
|
||||
}
|
||||
},
|
||||
"settingsTitle": "Settings",
|
||||
"settingsSubtitle": "Fine-grained sound control, backups, and custom stations.",
|
||||
"languageSectionTitle": "Language",
|
||||
"languageSectionDescription": "Choose how the app language is displayed.",
|
||||
"languageSystemDefault": "System",
|
||||
"languageSpanish": "Spanish",
|
||||
"languageEnglish": "English",
|
||||
"languageUpdated": "Language updated: {languageName}",
|
||||
"@languageUpdated": {
|
||||
"placeholders": {
|
||||
"languageName": {}
|
||||
}
|
||||
},
|
||||
"languageUpdatedSystem": "Language updated: System",
|
||||
"timerSectionTitle": "Sleep timer",
|
||||
"timerSectionAdd": "Add",
|
||||
"timerSectionDescription": "Customize the quick presets shown when automatically stopping the radio.",
|
||||
"timerSectionRestoreRecommended": "Restore recommended times",
|
||||
"newQuickAccessTitle": "New quick access",
|
||||
"saveQuickAccessButton": "Save quick access"
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"@@locale": "es",
|
||||
"appTitle": "PluriWave",
|
||||
"navHome": "Inicio",
|
||||
"navSearch": "Buscar",
|
||||
"navFavorites": "Favoritos",
|
||||
"navAlarms": "Alarmas",
|
||||
"navSettings": "Ajustes",
|
||||
"actionOk": "OK",
|
||||
"sleepTimer": "Timer de sueño",
|
||||
"sleepTimerDescription": "Apagado suave de la radio con cuenta atrás exacta.",
|
||||
"cancelTimer": "Cancelar timer",
|
||||
"optionOther": "Otro",
|
||||
"customDurationTitle": "Duración personalizada",
|
||||
"durationGreaterThanZero": "Elegí una duración mayor que cero.",
|
||||
"hoursLabel": "Horas",
|
||||
"minutesLabel": "Minutos",
|
||||
"secondsLabel": "Segundos",
|
||||
"saveQuickAccess": "Guardar como acceso rápido",
|
||||
"startTimer": "Iniciar timer",
|
||||
"skipCurrentAlarmExecution": "Omitida esta ejecución de {alarmName}.",
|
||||
"@skipCurrentAlarmExecution": {
|
||||
"placeholders": {
|
||||
"alarmName": {}
|
||||
}
|
||||
},
|
||||
"settingsTitle": "Ajustes",
|
||||
"settingsSubtitle": "Control fino de sonido, copias de seguridad y emisoras personalizadas.",
|
||||
"languageSectionTitle": "Idioma",
|
||||
"languageSectionDescription": "Elegí cómo se muestra el idioma de la app.",
|
||||
"languageSystemDefault": "Sistema",
|
||||
"languageSpanish": "Español",
|
||||
"languageEnglish": "Inglés",
|
||||
"languageUpdated": "Idioma actualizado: {languageName}",
|
||||
"@languageUpdated": {
|
||||
"placeholders": {
|
||||
"languageName": {}
|
||||
}
|
||||
},
|
||||
"languageUpdatedSystem": "Idioma actualizado: Sistema",
|
||||
"timerSectionTitle": "Timer de sueño",
|
||||
"timerSectionAdd": "Añadir",
|
||||
"timerSectionDescription": "Personalizá los accesos rápidos que aparecen al apagar la radio automáticamente.",
|
||||
"timerSectionRestoreRecommended": "Restaurar tiempos recomendados",
|
||||
"newQuickAccessTitle": "Nuevo acceso rápido",
|
||||
"saveQuickAccessButton": "Guardar acceso rápido"
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_en.dart';
|
||||
import 'app_localizations_es.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||
/// returned by `AppLocalizations.of(context)`.
|
||||
///
|
||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||
/// `localizationDelegates` list, and the locales they support in the app's
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'gen/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||
/// home: MyApplicationHome(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Update pubspec.yaml
|
||||
///
|
||||
/// Please make sure to update your pubspec.yaml to include the following
|
||||
/// packages:
|
||||
///
|
||||
/// ```yaml
|
||||
/// dependencies:
|
||||
/// # Internationalization support.
|
||||
/// flutter_localizations:
|
||||
/// sdk: flutter
|
||||
/// intl: any # Use the pinned version from flutter_localizations
|
||||
///
|
||||
/// # Rest of dependencies
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS Applications
|
||||
///
|
||||
/// iOS applications define key application metadata, including supported
|
||||
/// locales, in an Info.plist file that is built into the application bundle.
|
||||
/// To configure the locales supported by your app, you’ll need to edit this
|
||||
/// file.
|
||||
///
|
||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||
/// project’s Runner folder.
|
||||
///
|
||||
/// Next, select the Information Property List item, select Add Item from the
|
||||
/// Editor menu, then select Localizations from the pop-up menu.
|
||||
///
|
||||
/// Select and expand the newly-created Localizations item then, for each
|
||||
/// locale your application supports, add a new item and select the locale
|
||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
static AppLocalizations of(BuildContext context) {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||
_AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
///
|
||||
/// Returns a list of localizations delegates containing this delegate along with
|
||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||
/// and GlobalWidgetsLocalizations.delegate.
|
||||
///
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('en'),
|
||||
Locale('es'),
|
||||
];
|
||||
|
||||
/// No description provided for @appTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'PluriWave'**
|
||||
String get appTitle;
|
||||
|
||||
/// No description provided for @navHome.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Inicio'**
|
||||
String get navHome;
|
||||
|
||||
/// No description provided for @navSearch.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Buscar'**
|
||||
String get navSearch;
|
||||
|
||||
/// No description provided for @navFavorites.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Favoritos'**
|
||||
String get navFavorites;
|
||||
|
||||
/// No description provided for @navAlarms.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Alarmas'**
|
||||
String get navAlarms;
|
||||
|
||||
/// No description provided for @navSettings.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Ajustes'**
|
||||
String get navSettings;
|
||||
|
||||
/// No description provided for @actionOk.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'OK'**
|
||||
String get actionOk;
|
||||
|
||||
/// No description provided for @sleepTimer.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Timer de sueño'**
|
||||
String get sleepTimer;
|
||||
|
||||
/// No description provided for @sleepTimerDescription.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Apagado suave de la radio con cuenta atrás exacta.'**
|
||||
String get sleepTimerDescription;
|
||||
|
||||
/// No description provided for @cancelTimer.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Cancelar timer'**
|
||||
String get cancelTimer;
|
||||
|
||||
/// No description provided for @optionOther.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Otro'**
|
||||
String get optionOther;
|
||||
|
||||
/// No description provided for @customDurationTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Duración personalizada'**
|
||||
String get customDurationTitle;
|
||||
|
||||
/// No description provided for @durationGreaterThanZero.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Elegí una duración mayor que cero.'**
|
||||
String get durationGreaterThanZero;
|
||||
|
||||
/// No description provided for @hoursLabel.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Horas'**
|
||||
String get hoursLabel;
|
||||
|
||||
/// No description provided for @minutesLabel.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Minutos'**
|
||||
String get minutesLabel;
|
||||
|
||||
/// No description provided for @secondsLabel.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Segundos'**
|
||||
String get secondsLabel;
|
||||
|
||||
/// No description provided for @saveQuickAccess.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Guardar como acceso rápido'**
|
||||
String get saveQuickAccess;
|
||||
|
||||
/// No description provided for @startTimer.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Iniciar timer'**
|
||||
String get startTimer;
|
||||
|
||||
/// No description provided for @skipCurrentAlarmExecution.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Omitida esta ejecución de {alarmName}.'**
|
||||
String skipCurrentAlarmExecution(Object alarmName);
|
||||
|
||||
/// No description provided for @settingsTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Ajustes'**
|
||||
String get settingsTitle;
|
||||
|
||||
/// No description provided for @settingsSubtitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Control fino de sonido, copias de seguridad y emisoras personalizadas.'**
|
||||
String get settingsSubtitle;
|
||||
|
||||
/// No description provided for @languageSectionTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Idioma'**
|
||||
String get languageSectionTitle;
|
||||
|
||||
/// No description provided for @languageSectionDescription.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Elegí cómo se muestra el idioma de la app.'**
|
||||
String get languageSectionDescription;
|
||||
|
||||
/// No description provided for @languageSystemDefault.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Sistema'**
|
||||
String get languageSystemDefault;
|
||||
|
||||
/// No description provided for @languageSpanish.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Español'**
|
||||
String get languageSpanish;
|
||||
|
||||
/// No description provided for @languageEnglish.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Inglés'**
|
||||
String get languageEnglish;
|
||||
|
||||
/// No description provided for @languageUpdated.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Idioma actualizado: {languageName}'**
|
||||
String languageUpdated(Object languageName);
|
||||
|
||||
/// No description provided for @languageUpdatedSystem.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Idioma actualizado: Sistema'**
|
||||
String get languageUpdatedSystem;
|
||||
|
||||
/// No description provided for @timerSectionTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Timer de sueño'**
|
||||
String get timerSectionTitle;
|
||||
|
||||
/// No description provided for @timerSectionAdd.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Añadir'**
|
||||
String get timerSectionAdd;
|
||||
|
||||
/// No description provided for @timerSectionDescription.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Personalizá los accesos rápidos que aparecen al apagar la radio automáticamente.'**
|
||||
String get timerSectionDescription;
|
||||
|
||||
/// No description provided for @timerSectionRestoreRecommended.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Restaurar tiempos recomendados'**
|
||||
String get timerSectionRestoreRecommended;
|
||||
|
||||
/// No description provided for @newQuickAccessTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Nuevo acceso rápido'**
|
||||
String get newQuickAccessTitle;
|
||||
|
||||
/// No description provided for @saveQuickAccessButton.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Guardar acceso rápido'**
|
||||
String get saveQuickAccessButton;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
<String>['en', 'es'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'en':
|
||||
return AppLocalizationsEn();
|
||||
case 'es':
|
||||
return AppLocalizationsEs();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'PluriWave';
|
||||
|
||||
@override
|
||||
String get navHome => 'Home';
|
||||
|
||||
@override
|
||||
String get navSearch => 'Search';
|
||||
|
||||
@override
|
||||
String get navFavorites => 'Favorites';
|
||||
|
||||
@override
|
||||
String get navAlarms => 'Alarms';
|
||||
|
||||
@override
|
||||
String get navSettings => 'Settings';
|
||||
|
||||
@override
|
||||
String get actionOk => 'OK';
|
||||
|
||||
@override
|
||||
String get sleepTimer => 'Sleep timer';
|
||||
|
||||
@override
|
||||
String get sleepTimerDescription =>
|
||||
'Smooth radio shutdown with an exact countdown.';
|
||||
|
||||
@override
|
||||
String get cancelTimer => 'Cancel timer';
|
||||
|
||||
@override
|
||||
String get optionOther => 'Other';
|
||||
|
||||
@override
|
||||
String get customDurationTitle => 'Custom duration';
|
||||
|
||||
@override
|
||||
String get durationGreaterThanZero => 'Choose a duration greater than zero.';
|
||||
|
||||
@override
|
||||
String get hoursLabel => 'Hours';
|
||||
|
||||
@override
|
||||
String get minutesLabel => 'Minutes';
|
||||
|
||||
@override
|
||||
String get secondsLabel => 'Seconds';
|
||||
|
||||
@override
|
||||
String get saveQuickAccess => 'Save as quick access';
|
||||
|
||||
@override
|
||||
String get startTimer => 'Start timer';
|
||||
|
||||
@override
|
||||
String skipCurrentAlarmExecution(Object alarmName) {
|
||||
return 'Skipped this execution of $alarmName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Settings';
|
||||
|
||||
@override
|
||||
String get settingsSubtitle =>
|
||||
'Fine-grained sound control, backups, and custom stations.';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'Language';
|
||||
|
||||
@override
|
||||
String get languageSectionDescription =>
|
||||
'Choose how the app language is displayed.';
|
||||
|
||||
@override
|
||||
String get languageSystemDefault => 'System';
|
||||
|
||||
@override
|
||||
String get languageSpanish => 'Spanish';
|
||||
|
||||
@override
|
||||
String get languageEnglish => 'English';
|
||||
|
||||
@override
|
||||
String languageUpdated(Object languageName) {
|
||||
return 'Language updated: $languageName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get languageUpdatedSystem => 'Language updated: System';
|
||||
|
||||
@override
|
||||
String get timerSectionTitle => 'Sleep timer';
|
||||
|
||||
@override
|
||||
String get timerSectionAdd => 'Add';
|
||||
|
||||
@override
|
||||
String get timerSectionDescription =>
|
||||
'Customize the quick presets shown when automatically stopping the radio.';
|
||||
|
||||
@override
|
||||
String get timerSectionRestoreRecommended => 'Restore recommended times';
|
||||
|
||||
@override
|
||||
String get newQuickAccessTitle => 'New quick access';
|
||||
|
||||
@override
|
||||
String get saveQuickAccessButton => 'Save quick access';
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for Spanish Castilian (`es`).
|
||||
class AppLocalizationsEs extends AppLocalizations {
|
||||
AppLocalizationsEs([String locale = 'es']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'PluriWave';
|
||||
|
||||
@override
|
||||
String get navHome => 'Inicio';
|
||||
|
||||
@override
|
||||
String get navSearch => 'Buscar';
|
||||
|
||||
@override
|
||||
String get navFavorites => 'Favoritos';
|
||||
|
||||
@override
|
||||
String get navAlarms => 'Alarmas';
|
||||
|
||||
@override
|
||||
String get navSettings => 'Ajustes';
|
||||
|
||||
@override
|
||||
String get actionOk => 'OK';
|
||||
|
||||
@override
|
||||
String get sleepTimer => 'Timer de sueño';
|
||||
|
||||
@override
|
||||
String get sleepTimerDescription =>
|
||||
'Apagado suave de la radio con cuenta atrás exacta.';
|
||||
|
||||
@override
|
||||
String get cancelTimer => 'Cancelar timer';
|
||||
|
||||
@override
|
||||
String get optionOther => 'Otro';
|
||||
|
||||
@override
|
||||
String get customDurationTitle => 'Duración personalizada';
|
||||
|
||||
@override
|
||||
String get durationGreaterThanZero => 'Elegí una duración mayor que cero.';
|
||||
|
||||
@override
|
||||
String get hoursLabel => 'Horas';
|
||||
|
||||
@override
|
||||
String get minutesLabel => 'Minutos';
|
||||
|
||||
@override
|
||||
String get secondsLabel => 'Segundos';
|
||||
|
||||
@override
|
||||
String get saveQuickAccess => 'Guardar como acceso rápido';
|
||||
|
||||
@override
|
||||
String get startTimer => 'Iniciar timer';
|
||||
|
||||
@override
|
||||
String skipCurrentAlarmExecution(Object alarmName) {
|
||||
return 'Omitida esta ejecución de $alarmName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Ajustes';
|
||||
|
||||
@override
|
||||
String get settingsSubtitle =>
|
||||
'Control fino de sonido, copias de seguridad y emisoras personalizadas.';
|
||||
|
||||
@override
|
||||
String get languageSectionTitle => 'Idioma';
|
||||
|
||||
@override
|
||||
String get languageSectionDescription =>
|
||||
'Elegí cómo se muestra el idioma de la app.';
|
||||
|
||||
@override
|
||||
String get languageSystemDefault => 'Sistema';
|
||||
|
||||
@override
|
||||
String get languageSpanish => 'Español';
|
||||
|
||||
@override
|
||||
String get languageEnglish => 'Inglés';
|
||||
|
||||
@override
|
||||
String languageUpdated(Object languageName) {
|
||||
return 'Idioma actualizado: $languageName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get languageUpdatedSystem => 'Idioma actualizado: Sistema';
|
||||
|
||||
@override
|
||||
String get timerSectionTitle => 'Timer de sueño';
|
||||
|
||||
@override
|
||||
String get timerSectionAdd => 'Añadir';
|
||||
|
||||
@override
|
||||
String get timerSectionDescription =>
|
||||
'Personalizá los accesos rápidos que aparecen al apagar la radio automáticamente.';
|
||||
|
||||
@override
|
||||
String get timerSectionRestoreRecommended => 'Restaurar tiempos recomendados';
|
||||
|
||||
@override
|
||||
String get newQuickAccessTitle => 'Nuevo acceso rápido';
|
||||
|
||||
@override
|
||||
String get saveQuickAccessButton => 'Guardar acceso rápido';
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart' show Share, XFile;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../estado/estado_idioma.dart';
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../widgets/ecualizador_widget.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
@@ -22,20 +24,21 @@ class PantallaAjustes extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return ListView(
|
||||
padding: PluriLayout.pageListPadding,
|
||||
children: const [
|
||||
children: [
|
||||
PluriScreenHeader(
|
||||
title: 'Ajustes',
|
||||
subtitle:
|
||||
'Control fino de sonido, copias de seguridad y emisoras personalizadas.',
|
||||
title: l10n.settingsTitle,
|
||||
subtitle: l10n.settingsSubtitle,
|
||||
glyph: PluriIconGlyph.settings,
|
||||
trailing: PluriStatusPill(
|
||||
trailing: const PluriStatusPill(
|
||||
icon: Icons.security_rounded,
|
||||
label: 'Seguro',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
const Padding(
|
||||
padding: PluriLayout.pageContentPadding,
|
||||
child: _AjustesContent(),
|
||||
),
|
||||
@@ -57,6 +60,8 @@ class _AjustesContent extends StatelessWidget {
|
||||
SizedBox(height: 12),
|
||||
_SeccionTimerSueno(),
|
||||
SizedBox(height: 12),
|
||||
_SeccionIdioma(),
|
||||
SizedBox(height: 12),
|
||||
_SeccionEmisoraPreferida(),
|
||||
SizedBox(height: 12),
|
||||
_SeccionEmisoras(),
|
||||
@@ -165,6 +170,8 @@ class _SeccionTimerSueno extends StatelessWidget {
|
||||
const _SeccionTimerSueno();
|
||||
|
||||
Future<void> _anadirPreset(BuildContext context) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
final duracion = await showModalBottomSheet<Duration>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -177,7 +184,7 @@ class _SeccionTimerSueno extends StatelessWidget {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Acceso rápido añadido: ${_formatearDuracionTimer(duracion)}',
|
||||
'${l10n.saveQuickAccessButton}: ${_formatearDuracionTimer(duracion)}',
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -185,6 +192,8 @@ class _SeccionTimerSueno extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
final estado = context.watch<EstadoRadio>();
|
||||
final presets = estado.timerSuenoPresetsSegundos;
|
||||
return PluriGlassSurface(
|
||||
@@ -196,20 +205,20 @@ class _SeccionTimerSueno extends StatelessWidget {
|
||||
const Icon(Icons.bedtime_rounded),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Timer de sueño',
|
||||
l10n.timerSectionTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: const Text('Añadir'),
|
||||
label: Text(l10n.timerSectionAdd),
|
||||
onPressed: () => _anadirPreset(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Personalizá los accesos rápidos que aparecen al apagar la radio automáticamente.',
|
||||
l10n.timerSectionDescription,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -236,7 +245,7 @@ class _SeccionTimerSueno extends StatelessWidget {
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.restore_rounded),
|
||||
label: const Text('Restaurar tiempos recomendados'),
|
||||
label: Text(l10n.timerSectionRestoreRecommended),
|
||||
onPressed:
|
||||
() => context.read<EstadoRadio>().restaurarTimerSuenoPresets(),
|
||||
),
|
||||
@@ -247,6 +256,95 @@ class _SeccionTimerSueno extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SeccionIdioma extends StatelessWidget {
|
||||
const _SeccionIdioma();
|
||||
|
||||
static const _codigoSistema = 'system';
|
||||
static const _codigoEspanol = 'es';
|
||||
static const _codigoIngles = 'en';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final estadoIdioma = context.watch<EstadoIdioma>();
|
||||
final locale = estadoIdioma.localeSeleccionado;
|
||||
final valorActual = switch (locale?.languageCode) {
|
||||
_codigoEspanol => _codigoEspanol,
|
||||
_codigoIngles => _codigoIngles,
|
||||
_ => _codigoSistema,
|
||||
};
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.language_rounded),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n.languageSectionTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.languageSectionDescription,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: valorActual,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.languageSectionTitle,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: _codigoSistema,
|
||||
child: Text(l10n.languageSystemDefault),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: _codigoEspanol,
|
||||
child: Text(l10n.languageSpanish),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: _codigoIngles,
|
||||
child: Text(l10n.languageEnglish),
|
||||
),
|
||||
],
|
||||
onChanged: (codigo) async {
|
||||
if (codigo == null) return;
|
||||
if (codigo == _codigoSistema) {
|
||||
await context.read<EstadoIdioma>().seleccionarSistema();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.languageUpdatedSystem)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final locale = Locale(codigo);
|
||||
await context.read<EstadoIdioma>().seleccionarLocale(locale);
|
||||
if (!context.mounted) return;
|
||||
|
||||
final nombre = switch (codigo) {
|
||||
_codigoEspanol => l10n.languageSpanish,
|
||||
_codigoIngles => l10n.languageEnglish,
|
||||
_ => l10n.languageSystemDefault,
|
||||
};
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.languageUpdated(nombre))),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FormularioDuracionTimer extends StatefulWidget {
|
||||
const _FormularioDuracionTimer();
|
||||
|
||||
@@ -270,6 +368,8 @@ class _FormularioDuracionTimerState extends State<_FormularioDuracionTimer> {
|
||||
int _leer(TextEditingController ctrl) => int.tryParse(ctrl.text.trim()) ?? 0;
|
||||
|
||||
void _guardar() {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
final duracion = Duration(
|
||||
hours: _leer(_horasCtrl),
|
||||
minutes: _leer(_minutosCtrl),
|
||||
@@ -277,7 +377,7 @@ class _FormularioDuracionTimerState extends State<_FormularioDuracionTimer> {
|
||||
);
|
||||
if (duracion <= Duration.zero) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Elegí una duración mayor que cero.')),
|
||||
SnackBar(content: Text(l10n.durationGreaterThanZero)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -286,6 +386,8 @@ class _FormularioDuracionTimerState extends State<_FormularioDuracionTimer> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
final bottom = MediaQuery.viewInsetsOf(context).bottom;
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
@@ -295,23 +397,23 @@ class _FormularioDuracionTimerState extends State<_FormularioDuracionTimer> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Nuevo acceso rápido',
|
||||
l10n.newQuickAccessTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _campo(_horasCtrl, 'Horas')),
|
||||
Expanded(child: _campo(_horasCtrl, l10n.hoursLabel)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _campo(_minutosCtrl, 'Minutos')),
|
||||
Expanded(child: _campo(_minutosCtrl, l10n.minutesLabel)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _campo(_segundosCtrl, 'Segundos')),
|
||||
Expanded(child: _campo(_segundosCtrl, l10n.secondsLabel)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.save_rounded),
|
||||
label: const Text('Guardar acceso rápido'),
|
||||
label: Text(l10n.saveQuickAccessButton),
|
||||
onPressed: _guardar,
|
||||
),
|
||||
],
|
||||
|
||||
+1
-1
@@ -350,7 +350,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
intl:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
|
||||
@@ -11,6 +11,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: ^0.20.2
|
||||
|
||||
# Audio
|
||||
just_audio: ^0.9.42
|
||||
@@ -60,6 +61,7 @@ dev_dependencies:
|
||||
flutter_lints: ^5.0.0
|
||||
|
||||
flutter:
|
||||
generate: true
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_idioma.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('EstadoIdioma', () {
|
||||
test('arranca en modo sistema cuando no hay preferencia', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
final estado = EstadoIdioma();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(estado.usaSistema, isTrue);
|
||||
expect(estado.localeSeleccionado, isNull);
|
||||
});
|
||||
|
||||
test('persiste locale manual y permite volver a sistema', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
final estado = EstadoIdioma();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
await estado.seleccionarLocale(const Locale('en'));
|
||||
|
||||
expect(estado.usaSistema, isFalse);
|
||||
expect(estado.localeSeleccionado?.languageCode, 'en');
|
||||
|
||||
final recargado = EstadoIdioma();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(recargado.localeSeleccionado?.languageCode, 'en');
|
||||
|
||||
await recargado.seleccionarSistema();
|
||||
expect(recargado.usaSistema, isTrue);
|
||||
expect(recargado.localeSeleccionado, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user