feat(i18n): add localization foundation
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 1m52s
Build & Deploy Pluriwave / Análisis de código (push) Successful in 24s

This commit is contained in:
2026-05-22 13:29:52 +02:00
parent d85dee6fa8
commit 3f548fd53e
13 changed files with 986 additions and 65 deletions
+119 -17
View File
@@ -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,
),
],