feat(i18n): add localization foundation
This commit is contained in:
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user