fix(i18n): normalize translations and fallbacks
Build & Deploy PluriWave / Análisis de código (push) Successful in 38s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m34s

This commit is contained in:
2026-06-03 21:20:08 +02:00
parent a5475ce118
commit 089b8b4227
46 changed files with 17720 additions and 4869 deletions
+21 -5
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../l10n/gen/app_localizations.dart';
import '../modelos/preset_ecualizador.dart';
import '../tema/pluriwave_theme.dart';
import 'pluri_glass_surface.dart';
@@ -41,6 +42,7 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final tokens = context.pluriTokens;
final l10n = AppLocalizations.of(context);
return PluriGlassSurface(
borderRadius: BorderRadius.circular(tokens.radiusLg),
@@ -50,10 +52,10 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
children: [
Row(
children: [
Text('Ecualizador', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)),
Text(l10n.equalizerTitle, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)),
const Spacer(),
Chip(
label: Text(widget.preset.nombre, style: theme.textTheme.labelMedium),
label: Text(_nombrePreset(l10n, widget.preset.nombre), style: theme.textTheme.labelMedium),
backgroundColor: theme.colorScheme.secondaryContainer.withValues(alpha: 0.75),
),
],
@@ -77,8 +79,8 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
height: 152,
child: Semantics(
slider: true,
label: 'Banda ${_etiquetas[i]}',
value: '${_bandas[i].toStringAsFixed(1)} decibelios',
label: l10n.equalizerBandLabel(_etiquetas[i]),
value: l10n.equalizerBandValue(_bandas[i].toStringAsFixed(1)),
child: RotatedBox(
quarterTurns: 3,
child: Slider(
@@ -110,6 +112,19 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
}
}
String _nombrePreset(AppLocalizations l10n, String nombre) {
return switch (nombre) {
'Flat' => l10n.equalizerPresetFlat,
'Rock' => l10n.equalizerPresetRock,
'Pop' => l10n.equalizerPresetPop,
'Bass Boost' => l10n.equalizerPresetBassBoost,
'Jazz' => l10n.equalizerPresetJazz,
'Voz' => l10n.equalizerPresetVoice,
'Personalizado' => l10n.equalizerPresetCustom,
_ => nombre,
};
}
class PresetsEcualizadorWidget extends StatelessWidget {
final PresetEcualizador presetActual;
final void Function(PresetEcualizador) onSeleccionar;
@@ -123,13 +138,14 @@ class PresetsEcualizadorWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context);
return Wrap(
spacing: 8,
runSpacing: 6,
children: PresetEcualizador.presets.map((p) {
final selected = p.nombre == presetActual.nombre;
return ChoiceChip(
label: Text(p.nombre),
label: Text(_nombrePreset(l10n, p.nombre)),
selected: selected,
showCheckmark: false,
selectedColor: theme.colorScheme.primaryContainer,
+20 -15
View File
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../estado/estado_radio.dart';
import '../l10n/display_names.dart';
import '../l10n/gen/app_localizations.dart';
import '../pantallas/pantalla_reproductor.dart';
import '../servicios/servicio_audio.dart';
import '../tema/pluriwave_theme.dart';
@@ -17,11 +19,14 @@ class MiniReproductor extends StatelessWidget {
@override
Widget build(BuildContext context) {
final estado = context.watch<EstadoRadio>();
final l10n = AppLocalizations.of(context);
estado.configurarLocalizaciones(l10n);
final emisora = estado.emisoraActual;
if (emisora == null) return const SizedBox.shrink();
final t = context.pluriTokens;
final stationName = localizedStationName(l10n, emisora.nombre);
return SafeArea(
top: false,
@@ -43,7 +48,7 @@ class MiniReproductor extends StatelessWidget {
Expanded(
child: Semantics(
button: true,
label: 'Abrir reproductor de ${emisora.nombre}',
label: l10n.miniPlayerOpenLabel(stationName),
child: Material(
color: Colors.transparent,
child: InkWell(
@@ -74,7 +79,7 @@ class MiniReproductor extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
emisora.nombre,
stationName,
style: Theme.of(context)
.textTheme
.titleSmall
@@ -91,7 +96,7 @@ class MiniReproductor extends StatelessWidget {
final activo =
s == EstadoReproduccion.reproduciendo;
return Text(
_labelEstado(s),
_labelEstado(l10n, s),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
@@ -117,7 +122,7 @@ class MiniReproductor extends StatelessWidget {
glyph: PluriIconGlyph.player,
variant: PluriIconVariant.activeGlow,
size: 18,
semanticLabel: 'Reproductor',
semanticLabel: l10n.playerIconLabel,
),
],
),
@@ -144,7 +149,7 @@ class MiniReproductor extends StatelessWidget {
if (s == EstadoReproduccion.error) {
final emisoraActual = estado.emisoraActual;
return IconButton(
tooltip: 'Reintentar',
tooltip: l10n.retryAction,
icon: const Icon(Icons.refresh_rounded),
onPressed:
emisoraActual != null
@@ -161,13 +166,13 @@ class MiniReproductor extends StatelessWidget {
button: true,
label:
s == EstadoReproduccion.reproduciendo
? 'Pausar'
: 'Reproducir',
? l10n.pauseAction
: l10n.playAction,
child: IconButton(
tooltip:
s == EstadoReproduccion.reproduciendo
? 'Pausar'
: 'Reproducir',
? l10n.pauseAction
: l10n.playAction,
icon: Icon(
s == EstadoReproduccion.reproduciendo
? Icons.pause_circle_filled_rounded
@@ -190,13 +195,13 @@ class MiniReproductor extends StatelessWidget {
);
}
String _labelEstado(EstadoReproduccion estado) {
String _labelEstado(AppLocalizations l10n, EstadoReproduccion estado) {
return switch (estado) {
EstadoReproduccion.cargando => 'Conectando...',
EstadoReproduccion.reproduciendo => 'En directo',
EstadoReproduccion.pausado => 'Pausado',
EstadoReproduccion.error => 'Error de conexión',
EstadoReproduccion.detenido => 'Detenido',
EstadoReproduccion.cargando => l10n.playbackStatusConnecting,
EstadoReproduccion.reproduciendo => l10n.playbackStatusLive,
EstadoReproduccion.pausado => l10n.playbackStatusPaused,
EstadoReproduccion.error => l10n.playbackStatusConnectionError,
EstadoReproduccion.detenido => l10n.playbackStatusStopped,
};
}
}
+9 -8
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../l10n/gen/app_localizations.dart';
import '../tema/pluriwave_tokens.dart';
import '../tema/pluriwave_theme.dart';
@@ -62,7 +63,7 @@ class PluriIcon extends StatelessWidget {
: icon;
return Semantics(
label: semanticLabel ?? _fallbackLabel(glyph),
label: semanticLabel ?? _fallbackLabel(AppLocalizations.of(context), glyph),
image: true,
child: ExcludeSemantics(child: child),
);
@@ -108,14 +109,14 @@ class PluriIcon extends StatelessWidget {
};
}
String _fallbackLabel(PluriIconGlyph glyph) {
String _fallbackLabel(AppLocalizations l10n, PluriIconGlyph glyph) {
return switch (glyph) {
PluriIconGlyph.home => 'Inicio',
PluriIconGlyph.search => 'Buscar',
PluriIconGlyph.favorites => 'Favoritos',
PluriIconGlyph.alarm => 'Alarmas',
PluriIconGlyph.player => 'Reproductor',
PluriIconGlyph.settings => 'Ajustes',
PluriIconGlyph.home => l10n.navHome,
PluriIconGlyph.search => l10n.navSearch,
PluriIconGlyph.favorites => l10n.navFavorites,
PluriIconGlyph.alarm => l10n.navAlarms,
PluriIconGlyph.player => l10n.playerIconLabel,
PluriIconGlyph.settings => l10n.navSettings,
};
}
}
+6 -60
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../l10n/gen/app_localizations.dart';
import '../servicios/servicio_contenido_app.dart';
import 'pluri_glass_surface.dart';
import 'pluri_markdown.dart';
@@ -41,7 +42,7 @@ class _PluriOnboardingContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
final labels = _labels(Localizations.localeOf(context).languageCode);
final l10n = AppLocalizations.of(context);
final size = MediaQuery.sizeOf(context);
return Dialog(
insetPadding: const EdgeInsets.all(16),
@@ -80,14 +81,14 @@ class _PluriOnboardingContent extends StatelessWidget {
const SizedBox(width: 14),
Expanded(
child: Text(
labels.title,
l10n.onboardingTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w900,
),
),
),
IconButton(
tooltip: labels.close,
tooltip: l10n.onboardingCloseTooltip,
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close_rounded),
),
@@ -102,7 +103,7 @@ class _PluriOnboardingContent extends StatelessWidget {
if (contenido.notas.isNotEmpty) ...[
const SizedBox(height: 18),
Text(
labels.news,
l10n.onboardingNewsTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w900,
),
@@ -131,7 +132,7 @@ class _PluriOnboardingContent extends StatelessWidget {
child: FilledButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.check_rounded),
label: Text(labels.start),
label: Text(l10n.onboardingStartAction),
),
),
],
@@ -141,58 +142,3 @@ class _PluriOnboardingContent extends StatelessWidget {
);
}
}
_OnboardingLabels _labels(String languageCode) {
return switch (languageCode) {
'es' => const _OnboardingLabels(
title: 'Bienvenido a PluriWave',
news: 'Novedades',
start: 'Empezar',
close: 'Cerrar',
),
'fr' => const _OnboardingLabels(
title: 'Bienvenue sur PluriWave',
news: 'Nouveautés',
start: 'Commencer',
close: 'Fermer',
),
'de' => const _OnboardingLabels(
title: 'Willkommen bei PluriWave',
news: 'Neuigkeiten',
start: 'Starten',
close: 'Schließen',
),
'it' => const _OnboardingLabels(
title: 'Benvenuto in PluriWave',
news: 'Novità',
start: 'Inizia',
close: 'Chiudi',
),
'pt' => const _OnboardingLabels(
title: 'Bem-vindo ao PluriWave',
news: 'Novidades',
start: 'Começar',
close: 'Fechar',
),
_ => const _OnboardingLabels(
title: 'Welcome to PluriWave',
news: 'Whats new',
start: 'Start',
close: 'Close',
),
};
}
class _OnboardingLabels {
const _OnboardingLabels({
required this.title,
required this.news,
required this.start,
required this.close,
});
final String title;
final String news;
final String start;
final String close;
}
+28 -9
View File
@@ -4,6 +4,8 @@ import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
import '../estado/estado_radio.dart';
import '../l10n/display_names.dart';
import '../l10n/gen/app_localizations.dart';
import '../modelos/emisora.dart';
import '../tema/pluriwave_theme.dart';
import 'pluri_glass_surface.dart';
@@ -37,12 +39,14 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
final esFav = await estado.toggleFavorito(widget.emisora);
if (mounted) setState(() => _toggling = false);
if (mounted) {
final l10n = AppLocalizations.of(context);
final stationName = localizedStationName(l10n, widget.emisora.nombre);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
esFav
? '${widget.emisora.nombre} añadida a favoritos'
: '${widget.emisora.nombre} eliminada de favoritos',
? l10n.favoritesAddedMessage(stationName)
: l10n.favoritesRemovedMessage(stationName),
),
duration: const Duration(seconds: 2),
),
@@ -53,9 +57,11 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
@override
Widget build(BuildContext context) {
final t = context.pluriTokens;
final l10n = AppLocalizations.of(context);
final stationName = localizedStationName(l10n, widget.emisora.nombre);
return Semantics(
button: widget.onTap != null,
label: 'Emisora ${widget.emisora.nombre}',
label: l10n.stationSemanticLabel(stationName),
child: PluriGlassSurface(
padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(
@@ -74,6 +80,10 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
Widget _buildCompleta() {
final t = context.pluriTokens;
final stationName = localizedStationName(
AppLocalizations.of(context),
widget.emisora.nombre,
);
return Stack(
children: [
Column(
@@ -116,7 +126,7 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.emisora.nombre,
stationName,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
@@ -153,6 +163,10 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
Widget _buildCompacta() {
final t = context.pluriTokens;
final stationName = localizedStationName(
AppLocalizations.of(context),
widget.emisora.nombre,
);
final subtitulo = [
widget.emisora.pais,
widget.emisora.idioma,
@@ -192,7 +206,7 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.emisora.nombre,
stationName,
style: Theme.of(
context,
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
@@ -223,6 +237,7 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
Widget _botonFavorito({required bool mini}) {
final t = context.pluriTokens;
final l10n = AppLocalizations.of(context);
final esFavorito = context.select<EstadoRadio, bool>(
(estado) =>
estado.listaFavoritos.any((e) => e.uuid == widget.emisora.uuid),
@@ -248,13 +263,16 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
: PluriIconVariant.outline,
size: 20,
semanticLabel:
esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos',
esFavorito
? l10n.favoritesRemoveTooltip
: l10n.favoritesAddTooltip,
);
return Semantics(
button: true,
toggled: esFavorito,
label: esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos',
label:
esFavorito ? l10n.favoritesRemoveTooltip : l10n.favoritesAddTooltip,
child: Material(
color: mini ? t.glassSurface : Colors.transparent,
shape: const CircleBorder(),
@@ -318,7 +336,7 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
glyph: PluriIconGlyph.player,
variant: PluriIconVariant.activeGlow,
size: size,
semanticLabel: 'Icono de emisora',
semanticLabel: AppLocalizations.of(context).stationIconLabel,
),
),
],
@@ -345,6 +363,7 @@ class _LiveBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = Theme.of(context).colorScheme.secondary;
final l10n = AppLocalizations.of(context);
return Container(
padding: EdgeInsets.symmetric(horizontal: mini ? 8 : 6, vertical: mini ? 5 : 4),
decoration: BoxDecoration(
@@ -358,7 +377,7 @@ class _LiveBadge extends StatelessWidget {
Icon(Icons.fiber_manual_record_rounded, size: mini ? 10 : 8, color: color),
if (mini) ...[
const SizedBox(width: 5),
Text('Live', style: Theme.of(context).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w900)),
Text(l10n.liveNow, style: Theme.of(context).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w900)),
],
],
),