v0.2.0: i18n 18 idiomas + pantalla ajustes + bancos multiidioma

Internacionalización completa:
- 18 ficheros .arb: es, en, fr, pt, de, it, ru, ja, ko, zh, zh_TW, ar, hi, tr, pl, nl, ca, eu
- Todos los strings extraídos de todas las pantallas
- Detección automática de idioma del sistema
- Selector manual en pantalla de ajustes

Pantalla de ajustes nueva:
- Selector de idioma con banderas emoji
- Vibración ON/OFF
- Acerca de (versión, desarrollador)

Bancos de palabras multiidioma:
- palabras.json (castellano, 1000 palabras)
- palabras_en.json (inglés, 1000 palabras)
- palabras_fr.json (francés, 1000 palabras)
- Fallback a castellano si no hay banco del idioma

13138 líneas Dart, 39 ficheros, 0 issues en flutter analyze
This commit is contained in:
ShanaiaBot
2026-04-04 01:18:09 +02:00
parent de2c8ffa18
commit 1bca50af1d
56 changed files with 14389 additions and 201 deletions

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart';
import '../estado/estado_juego.dart';
import '../tema/tema_app.dart';
@@ -30,13 +31,14 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final estado = context.watch<EstadoJuego>();
final partida = estado.partida;
if (partida == null) return const SizedBox.shrink();
return Scaffold(
appBar: AppBar(
title: const Text('🎯 Adivinanza del impostor'),
title: Text(l10n.impostorGuessTitle),
automaticallyImplyLeading: false,
),
body: Center(
@@ -48,13 +50,13 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
const Text('🎭', style: TextStyle(fontSize: 64)),
const SizedBox(height: 16),
Text(
'El impostor eliminado puede\nintentar adivinar la palabra',
l10n.impostorCanGuess,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Si acierta, ¡los impostores ganan!',
l10n.ifCorrectImpostorsWin,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: TemaApp.colorNaranja,
),
@@ -64,9 +66,9 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
if (_acierto == null) ...[
TextField(
controller: _controlador,
decoration: const InputDecoration(
hintText: '¿Cuál crees que es la palabra?',
prefixIcon: Icon(Icons.search),
decoration: InputDecoration(
hintText: l10n.guessWordHint,
prefixIcon: const Icon(Icons.search),
),
textCapitalization: TextCapitalization.sentences,
textAlign: TextAlign.center,
@@ -98,7 +100,7 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
);
}
},
child: const Text('No intentar'),
child: Text(l10n.dontGuess),
),
),
const SizedBox(width: 12),
@@ -109,7 +111,7 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
? _intentarAdivinar
: null,
icon: const Icon(Icons.send),
label: const Text('Adivinar'),
label: Text(l10n.guess),
),
),
],
@@ -130,7 +132,7 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
const Text('🎭🎉', style: TextStyle(fontSize: 48)),
const SizedBox(height: 12),
Text(
'¡Ha acertado!',
l10n.correctGuess,
style: Theme.of(context)
.textTheme
.headlineMedium
@@ -138,12 +140,12 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
),
const SizedBox(height: 8),
Text(
'La palabra era: ${partida.palabraSecreta}',
l10n.theWordWas(partida.palabraSecreta),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'¡Los impostores ganan!',
l10n.impostorsWin,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: TemaApp.colorNaranja,
),
@@ -165,7 +167,7 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
);
},
icon: const Icon(Icons.emoji_events),
label: const Text('Ver resultado final'),
label: Text(l10n.seeEndResult),
),
),
],
@@ -184,7 +186,7 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
const Text('', style: TextStyle(fontSize: 48)),
const SizedBox(height: 12),
Text(
'¡No ha acertado!',
l10n.wrongGuess,
style: Theme.of(context)
.textTheme
.headlineMedium
@@ -192,7 +194,7 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
),
const SizedBox(height: 8),
Text(
'La partida continúa...',
l10n.gameContinues,
style: Theme.of(context).textTheme.bodyLarge,
),
],
@@ -223,7 +225,7 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
}
},
icon: const Icon(Icons.skip_next),
label: const Text('Siguiente ronda'),
label: Text(l10n.nextRound),
),
),
],

View File

@@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart';
import '../servicios/servicio_idioma.dart';
import '../tema/tema_app.dart';
class PantallaAjustes extends StatefulWidget {
const PantallaAjustes({super.key});
@override
State<PantallaAjustes> createState() => _PantallaAjustesState();
}
class _PantallaAjustesState extends State<PantallaAjustes> {
double _volumen = 0.7;
bool _vibracion = true;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final servicioIdioma = context.watch<ServicioIdioma>();
return Scaffold(
appBar: AppBar(title: Text(l10n.settingsTitle)),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Selector de idioma
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.language,
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
// Opción automática (sistema)
_opcionIdioma(
context,
bandera: '🌐',
nombre: 'Auto (${_nombreIdiomaDelSistema()})',
codigo: 'sistema',
seleccionado: servicioIdioma.codigoActual == 'sistema',
onTap: () => servicioIdioma.cambiarIdioma('sistema'),
),
const Divider(height: 1),
// Lista de idiomas
...ServicioIdioma.idiomasSoportados.entries.map((entrada) {
return _opcionIdioma(
context,
bandera: entrada.value.bandera,
nombre: entrada.value.nombre,
codigo: entrada.key,
seleccionado:
servicioIdioma.codigoActual == entrada.key,
onTap: () =>
servicioIdioma.cambiarIdioma(entrada.key),
);
}),
],
),
),
),
const SizedBox(height: 12),
// Volumen de efectos de sonido
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.soundVolume,
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Slider(
value: _volumen,
onChanged: (v) => setState(() => _volumen = v),
activeColor: TemaApp.colorAcento,
inactiveColor: TemaApp.colorTarjeta,
),
],
),
),
),
const SizedBox(height: 12),
// Vibración
Card(
child: SwitchListTile(
title: Text(l10n.vibration),
value: _vibracion,
onChanged: (v) => setState(() => _vibracion = v),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
),
),
const SizedBox(height: 12),
// Acerca de
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.about,
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
_filaInfo(context, l10n.version, '1.0.0'),
const SizedBox(height: 8),
_filaInfo(context, l10n.developer, 'FreeTTimeLab'),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () {
showLicensePage(
context: context,
applicationName: 'El Impostor',
applicationVersion: '1.0.0',
);
},
child: Text(l10n.licenses),
),
),
],
),
),
),
const SizedBox(height: 16),
],
),
),
);
}
Widget _opcionIdioma(
BuildContext context, {
required String bandera,
required String nombre,
required String codigo,
required bool seleccionado,
required VoidCallback onTap,
}) {
return ListTile(
leading: Text(bandera, style: const TextStyle(fontSize: 24)),
title: Text(nombre),
trailing: seleccionado
? const Icon(Icons.check_circle, color: TemaApp.colorAcento)
: null,
onTap: onTap,
dense: true,
selected: seleccionado,
selectedTileColor: TemaApp.colorAcento.withValues(alpha: 0.1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
);
}
Widget _filaInfo(BuildContext context, String etiqueta, String valor) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(etiqueta, style: Theme.of(context).textTheme.bodyMedium),
Text(valor, style: Theme.of(context).textTheme.bodyLarge),
],
);
}
String _nombreIdiomaDelSistema() {
final locale = WidgetsBinding.instance.platformDispatcher.locale;
final codigo = locale.countryCode != null && locale.countryCode!.isNotEmpty
? '${locale.languageCode}_${locale.countryCode}'
: locale.languageCode;
final info = ServicioIdioma.idiomasSoportados[codigo] ??
ServicioIdioma.idiomasSoportados[locale.languageCode];
return info?.nombre ?? locale.languageCode;
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart';
import '../estado/estado_juego.dart';
import '../modelos/palabra.dart';
@@ -23,22 +24,25 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
final _controladorNombre = TextEditingController();
final _opcionesTiempo = <int?>[null, 60, 120, 180, 300];
final _etiquetasTiempo = ['Sin límite', '1 min', '2 min', '3 min', '5 min'];
int get _maxImpostores => (_jugadores.length / 3).floor().clamp(1, 4);
List<String> _etiquetasTiempo(AppLocalizations l10n) =>
[l10n.noLimit, l10n.oneMin, l10n.twoMin, l10n.threeMin, l10n.fiveMin];
void _agregarJugador() {
final l10n = AppLocalizations.of(context)!;
final nombre = _controladorNombre.text.trim();
if (nombre.isEmpty) return;
if (_jugadores.contains(nombre)) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ya existe un jugador con ese nombre')),
SnackBar(content: Text(l10n.playerAlreadyExists)),
);
return;
}
if (_jugadores.length >= 20) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Máximo 20 jugadores')),
SnackBar(content: Text(l10n.maxPlayersReached)),
);
return;
}
@@ -61,9 +65,10 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
}
void _iniciarPartida() {
final l10n = AppLocalizations.of(context)!;
if (_jugadores.length < 3) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Se necesitan al menos 3 jugadores')),
SnackBar(content: Text(l10n.minPlayersRequired)),
);
return;
}
@@ -94,11 +99,13 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final estado = context.watch<EstadoJuego>();
final categorias = ['todas', ...?estado.banco?.nombresCategorias];
final etiquetas = _etiquetasTiempo(l10n);
return Scaffold(
appBar: AppBar(title: const Text('Crear partida')),
appBar: AppBar(title: Text(l10n.createGame)),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
@@ -111,20 +118,20 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Modo de juego',
Text(l10n.gameMode,
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
SegmentedButton<bool>(
segments: const [
segments: [
ButtonSegment(
value: false,
label: Text('Un solo móvil'),
icon: Icon(Icons.phone_android),
label: Text(l10n.singleDevice),
icon: const Icon(Icons.phone_android),
),
ButtonSegment(
value: true,
label: Text('Multimóvil'),
icon: Icon(Icons.devices),
label: Text(l10n.multiDevice),
icon: const Icon(Icons.devices),
),
],
selected: {_modoMultimovil},
@@ -145,7 +152,7 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Categoría',
Text(l10n.category,
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
SizedBox(
@@ -158,7 +165,7 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
items: categorias.map((c) {
return DropdownMenuItem(
value: c,
child: Text(BancoPalabras.nombreBonitoCategoria(c)),
child: Text(BancoPalabras.nombreBonitoCategoria(c, l10n)),
);
}).toList(),
onChanged: (v) => setState(() => _categoria = v!),
@@ -180,9 +187,9 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Jugadores (${_jugadores.length})',
Text(l10n.playersCount(_jugadores.length),
style: Theme.of(context).textTheme.titleLarge),
Text('3-20',
Text(l10n.playersRangeHint,
style: Theme.of(context).textTheme.bodyMedium),
],
),
@@ -192,9 +199,9 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
Expanded(
child: TextField(
controller: _controladorNombre,
decoration: const InputDecoration(
hintText: 'Nombre del jugador',
prefixIcon: Icon(Icons.person_add),
decoration: InputDecoration(
hintText: l10n.playerNameHint,
prefixIcon: const Icon(Icons.person_add),
),
textCapitalization: TextCapitalization.words,
onSubmitted: (_) => _agregarJugador(),
@@ -237,7 +244,7 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Configuración',
Text(l10n.configuration,
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
@@ -245,7 +252,7 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('🎭 Impostores'),
Text(l10n.impostors),
Row(
children: [
IconButton(
@@ -271,9 +278,8 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
// Pista para impostor
SwitchListTile(
title: const Text('🔍 Pista para impostor'),
subtitle: const Text(
'El impostor conoce la categoría'),
title: Text(l10n.impostorClue),
subtitle: Text(l10n.impostorClueDescription),
value: _pistaImpostor,
onChanged: (v) =>
setState(() => _pistaImpostor = v),
@@ -284,14 +290,14 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('⏱️ Tiempo de debate'),
Text(l10n.debateTime),
DropdownButton<int?>(
value: _tiempoDebate,
items: List.generate(
_opcionesTiempo.length,
(i) => DropdownMenuItem(
value: _opcionesTiempo[i],
child: Text(_etiquetasTiempo[i]),
child: Text(etiquetas[i]),
),
),
onChanged: (v) =>
@@ -312,7 +318,7 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
child: ElevatedButton.icon(
onPressed: _jugadores.length >= 3 ? _iniciarPartida : null,
icon: const Icon(Icons.play_arrow),
label: const Text('Iniciar partida'),
label: Text(l10n.startGame),
style: ElevatedButton.styleFrom(
textStyle: const TextStyle(
fontSize: 18,

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart';
import '../estado/estado_juego.dart';
import '../tema/tema_app.dart';
@@ -59,6 +60,7 @@ class _PantallaDebateState extends State<PantallaDebate> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final estado = context.watch<EstadoJuego>();
final partida = estado.partida;
if (partida == null) return const SizedBox.shrink();
@@ -70,7 +72,7 @@ class _PantallaDebateState extends State<PantallaDebate> {
return Scaffold(
appBar: AppBar(
title: Text('Debate - Ronda ${partida.rondaActual}'),
title: Text(l10n.debateRound(partida.rondaActual)),
automaticallyImplyLeading: false,
),
body: Padding(
@@ -94,7 +96,7 @@ class _PantallaDebateState extends State<PantallaDebate> {
child: Column(
children: [
Text(
_tiempoAgotado ? '⏰ ¡Tiempo agotado!' : '⏱️ Tiempo restante',
_tiempoAgotado ? l10n.timeUp : l10n.timeRemaining,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: _tiempoAgotado
? TemaApp.colorAcento
@@ -141,12 +143,12 @@ class _PantallaDebateState extends State<PantallaDebate> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jugadores en debate',
l10n.playersInDebate,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(
'${partida.jugadoresActivos.length} activos • ${partida.impostoresActivos.length} impostor(es) ocultos',
l10n.activePlayersInfo(partida.jugadoresActivos.length, partida.impostoresActivos.length),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
@@ -178,7 +180,7 @@ class _PantallaDebateState extends State<PantallaDebate> {
),
),
subtitle: j.eliminado
? const Text('Eliminado')
? Text(l10n.eliminated)
: null,
dense: true,
);
@@ -206,7 +208,7 @@ class _PantallaDebateState extends State<PantallaDebate> {
);
},
icon: const Text('📝', style: TextStyle(fontSize: 18)),
label: const Text('Notas'),
label: Text(l10n.notes),
),
),
const SizedBox(width: 12),
@@ -215,7 +217,7 @@ class _PantallaDebateState extends State<PantallaDebate> {
child: ElevatedButton.icon(
onPressed: _irAVotacion,
icon: const Text('🗳️', style: TextStyle(fontSize: 18)),
label: const Text('Ir a votación'),
label: Text(l10n.goToVoting),
),
),
],

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart';
import '../estado/estado_juego.dart';
import '../modelos/palabra.dart';
@@ -11,6 +12,7 @@ class PantallaFinPartida extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final estado = context.watch<EstadoJuego>();
final partida = estado.partida;
if (partida == null) return const SizedBox.shrink();
@@ -21,7 +23,7 @@ class PantallaFinPartida extends StatelessWidget {
return Scaffold(
appBar: AppBar(
title: const Text('Fin de partida'),
title: Text(l10n.gameOver),
automaticallyImplyLeading: false,
),
body: SingleChildScrollView(
@@ -56,8 +58,8 @@ class PantallaFinPartida extends StatelessWidget {
const SizedBox(height: 16),
Text(
ganaronJugadores
? '¡Los jugadores ganan!'
: '¡Los impostores ganan!',
? l10n.playersWin
: l10n.impostorsWin,
style: Theme.of(context)
.textTheme
.headlineMedium
@@ -79,7 +81,7 @@ class PantallaFinPartida extends StatelessWidget {
padding: const EdgeInsets.all(20),
child: Column(
children: [
Text('🔍 La palabra era:',
Text(l10n.theSecretWordWas,
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text(
@@ -94,7 +96,7 @@ class PantallaFinPartida extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
'Categoría: ${BancoPalabras.nombreBonitoCategoria(partida.categoriaReal)}',
l10n.categoryLabel(BancoPalabras.nombreBonitoCategoria(partida.categoriaReal, l10n)),
style: Theme.of(context).textTheme.bodyMedium,
),
],
@@ -110,7 +112,7 @@ class PantallaFinPartida extends StatelessWidget {
child: Column(
children: [
Text(
'🎭 ${impostores.length == 1 ? 'El impostor era:' : 'Los impostores eran:'}',
impostores.length == 1 ? l10n.theImpostorWas : l10n.theImpostorsWere,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
@@ -119,8 +121,8 @@ class PantallaFinPartida extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('🎭 ',
style: const TextStyle(fontSize: 18)),
const Text('🎭 ',
style: TextStyle(fontSize: 18)),
Text(
j.nombre,
style: Theme.of(context)
@@ -150,7 +152,7 @@ class PantallaFinPartida extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('📊 Historial de votaciones',
Text(l10n.votingHistory,
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
...partida.historialVotaciones
@@ -165,7 +167,7 @@ class PantallaFinPartida extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ronda $ronda: ${resultado.eliminadoNombre} ${resultado.eraImpostor ? '🎭' : '😇'}',
'${l10n.roundElimination(ronda, resultado.eliminadoNombre)} ${resultado.eraImpostor ? '🎭' : '😇'}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: resultado.eraImpostor
@@ -210,7 +212,7 @@ class PantallaFinPartida extends StatelessWidget {
);
},
icon: const Icon(Icons.replay),
label: const Text('Revancha'),
label: Text(l10n.rematch),
),
),
const SizedBox(height: 12),
@@ -229,7 +231,7 @@ class PantallaFinPartida extends StatelessWidget {
);
},
icon: const Icon(Icons.home),
label: const Text('Menú principal'),
label: Text(l10n.mainMenu),
),
),
const SizedBox(height: 16),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart';
import '../estado/estado_juego.dart';
import '../servicios/servicio_notas.dart';
@@ -57,6 +58,7 @@ class _PantallaNotasState extends State<PantallaNotas> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final estado = context.watch<EstadoJuego>();
final partida = estado.partida;
if (partida == null) return const SizedBox.shrink();
@@ -68,7 +70,7 @@ class _PantallaNotasState extends State<PantallaNotas> {
return Scaffold(
appBar: AppBar(
title: const Text('📝 Notas'),
title: Text(l10n.notesTitle),
actions: [
if (_jugadorSeleccionadoId != null)
IconButton(
@@ -77,7 +79,7 @@ class _PantallaNotasState extends State<PantallaNotas> {
await _guardarNotas();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Notas guardadas')),
SnackBar(content: Text(l10n.notesSaved)),
);
}
},
@@ -91,18 +93,20 @@ class _PantallaNotasState extends State<PantallaNotas> {
}
Widget _construirSelectorJugador(dynamic partida) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'¿Quién eres?',
l10n.whoAreYou,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
Text(
'Selecciona tu nombre para ver tus notas privadas',
l10n.selectYourName,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
@@ -142,6 +146,7 @@ class _PantallaNotasState extends State<PantallaNotas> {
return const Center(child: CircularProgressIndicator());
}
final l10n = AppLocalizations.of(context)!;
final jugadorActual = partida.jugadores
.firstWhere((j) => j.id == _jugadorSeleccionadoId);
@@ -167,7 +172,7 @@ class _PantallaNotasState extends State<PantallaNotas> {
},
),
Text(
'Notas de ${jugadorActual.nombre}',
l10n.notesOf(jugadorActual.nombre),
style: Theme.of(context).textTheme.titleLarge,
),
],
@@ -176,7 +181,7 @@ class _PantallaNotasState extends State<PantallaNotas> {
// Notas por jugador
Text(
'Apuntes sobre cada jugador',
l10n.notesAboutPlayers,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: TemaApp.colorTextoSecundario,
),
@@ -190,7 +195,7 @@ class _PantallaNotasState extends State<PantallaNotas> {
decoration: InputDecoration(
labelText: j.nombre,
prefixIcon: const Icon(Icons.person, size: 20),
hintText: '¿Qué ha dicho? ¿Sospechoso?',
hintText: l10n.playerNoteHint,
),
maxLines: 2,
minLines: 1,
@@ -200,7 +205,7 @@ class _PantallaNotasState extends State<PantallaNotas> {
const SizedBox(height: 16),
Text(
'Nota libre',
l10n.freeNote,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: TemaApp.colorTextoSecundario,
),
@@ -208,9 +213,9 @@ class _PantallaNotasState extends State<PantallaNotas> {
const SizedBox(height: 8),
TextField(
controller: _controladorNotaLibre,
decoration: const InputDecoration(
hintText: 'Apuntes personales...',
prefixIcon: Icon(Icons.note, size: 20),
decoration: InputDecoration(
hintText: l10n.freeNoteHint,
prefixIcon: const Icon(Icons.note, size: 20),
),
maxLines: 5,
minLines: 3,

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import '../tema/tema_app.dart';
import 'pantalla_ajustes.dart';
import 'pantalla_crear_partida.dart';
import 'pantalla_reglas.dart';
import 'pantalla_unirse.dart';
@@ -9,6 +11,8 @@ class PantallaPrincipal extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: SafeArea(
child: Center(
@@ -47,7 +51,7 @@ class PantallaPrincipal extends StatelessWidget {
// Título
Text(
'El Impostor',
l10n.appTitle,
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontSize: 36,
letterSpacing: 1.2,
@@ -55,7 +59,7 @@ class PantallaPrincipal extends StatelessWidget {
),
const SizedBox(height: 8),
Text(
'Juego de deducción social',
l10n.subtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
),
@@ -75,7 +79,7 @@ class PantallaPrincipal extends StatelessWidget {
);
},
icon: const Text('🎮', style: TextStyle(fontSize: 20)),
label: const Text('Crear partida'),
label: Text(l10n.createGame),
),
),
const SizedBox(height: 16),
@@ -92,7 +96,7 @@ class PantallaPrincipal extends StatelessWidget {
);
},
icon: const Text('📱', style: TextStyle(fontSize: 20)),
label: const Text('Unirse a partida'),
label: Text(l10n.joinGame),
),
),
const SizedBox(height: 16),
@@ -109,13 +113,30 @@ class PantallaPrincipal extends StatelessWidget {
);
},
icon: const Text('📖', style: TextStyle(fontSize: 20)),
label: const Text('Cómo jugar'),
label: Text(l10n.howToPlay),
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const PantallaAjustes(),
),
);
},
icon: const Icon(Icons.settings, size: 20),
label: Text(l10n.settings),
),
),
const SizedBox(height: 48),
Text(
'3-20 jugadores • Sin internet',
l10n.playersRange,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 12,
),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import '../tema/tema_app.dart';
class PantallaReglas extends StatelessWidget {
@@ -6,78 +7,22 @@ class PantallaReglas extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: const Text('📖 Cómo jugar')),
appBar: AppBar(title: Text(l10n.rulesTitle)),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_seccion(
context,
'🎭 ¿Qué es El Impostor?',
'Un juego de deducción social para 3-20 jugadores. '
'Todos reciben una palabra secreta... ¡excepto el impostor! '
'Tu misión: descubrir quién finge.',
),
_seccion(
context,
'🔍 ¿Cómo se juega?',
'1. Se reparten los roles: todos reciben la misma palabra, '
'excepto el/los impostores.\n\n'
'2. Debate: por turnos, cada jugador describe la palabra '
'SIN decirla directamente. El impostor debe fingir que la conoce.\n\n'
'3. Votación: al terminar el debate, todos votan a quién '
'creen que es el impostor.\n\n'
'4. Eliminación: el más votado queda eliminado y se revela '
'si era impostor o no.\n\n'
'5. Si era impostor, puede intentar adivinar la palabra. '
'Si acierta, ¡los impostores ganan!',
),
_seccion(
context,
'🏆 ¿Quién gana?',
'• Jugadores: ganan si eliminan a TODOS los impostores.\n'
'• Impostores: ganan si no son descubiertos hasta que '
'queden igual o menos jugadores normales que impostores, '
'o si adivinan la palabra al ser eliminados.',
),
_seccion(
context,
'💡 Consejos para jugadores',
'• Da pistas sutiles que demuestren que conoces la palabra, '
'pero no tan obvias que el impostor las use.\n'
'• Observa quién da respuestas vagas o genéricas.\n'
'• Usa las notas para apuntar lo que dice cada uno.\n'
'• No digas la palabra directamente, ¡eso ayuda al impostor!',
),
_seccion(
context,
'🎭 Consejos para el impostor',
'• Escucha atentamente las pistas de los demás.\n'
'• Intenta deducir la palabra para dar pistas creíbles.\n'
'• No seas el primero en hablar si no estás seguro.\n'
'• Si te dan la categoría como pista, úsala a tu favor.\n'
'• Acusa a otros para desviar la atención.',
),
_seccion(
context,
'📱 Modos de juego',
'• Un solo móvil: todos comparten el dispositivo. '
'Cada jugador ve su palabra pulsando y manteniendo un botón.\n\n'
'• Multimóvil: cada jugador usa su propio dispositivo. '
'Se conectan por Bluetooth/WiFi Direct sin necesidad de internet.',
),
_ejemplo(
context,
'✏️ Ejemplo de partida',
'Palabra secreta: "Pizza"\n\n'
'• Ana: "Se come caliente" ✓\n'
'• Carlos: "Viene en una caja" ✓\n'
'• Eva (impostor): "Es muy popular" 🤔\n'
'• David: "Tiene queso" ✓\n\n'
'Eva dio una respuesta muy genérica... ¡Sospechosa!',
),
_seccion(context, l10n.rulesWhatIsTitle, l10n.rulesWhatIsBody),
_seccion(context, l10n.rulesHowToPlayTitle, l10n.rulesHowToPlayBody),
_seccion(context, l10n.rulesWhoWinsTitle, l10n.rulesWhoWinsBody),
_seccion(context, l10n.rulesTipsPlayersTitle, l10n.rulesTipsPlayersBody),
_seccion(context, l10n.rulesTipsImpostorTitle, l10n.rulesTipsImpostorBody),
_seccion(context, l10n.rulesModesTitle, l10n.rulesModesBody),
_ejemplo(context, l10n.rulesExampleTitle, l10n.rulesExampleBody),
const SizedBox(height: 32),
],
),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart';
import '../estado/estado_juego.dart';
import '../modelos/partida.dart';
@@ -52,12 +53,13 @@ class _PantallaResultadoState extends State<PantallaResultado>
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final estado = context.read<EstadoJuego>();
final partida = estado.partida;
return Scaffold(
appBar: AppBar(
title: const Text('Resultado'),
title: Text(l10n.result),
automaticallyImplyLeading: false,
),
body: Center(
@@ -71,7 +73,7 @@ class _PantallaResultadoState extends State<PantallaResultado>
const Text('🥁', style: TextStyle(fontSize: 64)),
const SizedBox(height: 16),
Text(
'Revelando...',
l10n.revealing,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 24),
@@ -113,8 +115,8 @@ class _PantallaResultadoState extends State<PantallaResultado>
),
child: Text(
widget.resultado.eraImpostor
? '¡Era IMPOSTOR! 🎉'
: 'Era INOCENTE 😱',
? l10n.wasImpostor
: l10n.wasInnocent,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@@ -133,7 +135,7 @@ class _PantallaResultadoState extends State<PantallaResultado>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Votos de esta ronda',
Text(l10n.votesThisRound,
style: Theme.of(context)
.textTheme
.titleMedium),
@@ -177,6 +179,7 @@ class _PantallaResultadoState extends State<PantallaResultado>
}
Widget _construirBotones(BuildContext context, EstadoJuego estado) {
final l10n = AppLocalizations.of(context)!;
final partida = estado.partida;
if (partida == null) return const SizedBox.shrink();
@@ -195,7 +198,7 @@ class _PantallaResultadoState extends State<PantallaResultado>
);
},
icon: const Icon(Icons.emoji_events),
label: const Text('Ver resultado final'),
label: Text(l10n.seeEndResult),
),
);
}
@@ -217,7 +220,7 @@ class _PantallaResultadoState extends State<PantallaResultado>
);
},
icon: const Text('🎯', style: TextStyle(fontSize: 18)),
label: const Text('¿El impostor adivina la palabra?'),
label: Text(l10n.impostorGuessWord),
),
),
const SizedBox(height: 12),
@@ -227,7 +230,7 @@ class _PantallaResultadoState extends State<PantallaResultado>
child: ElevatedButton.icon(
onPressed: () => _siguienteRonda(context, estado),
icon: const Icon(Icons.skip_next),
label: const Text('Siguiente ronda'),
label: Text(l10n.nextRound),
),
),
],
@@ -240,7 +243,7 @@ class _PantallaResultadoState extends State<PantallaResultado>
child: ElevatedButton.icon(
onPressed: () => _siguienteRonda(context, estado),
icon: const Icon(Icons.skip_next),
label: const Text('Siguiente ronda'),
label: Text(l10n.nextRound),
),
);
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import '../tema/tema_app.dart';
class PantallaUnirse extends StatelessWidget {
@@ -6,8 +7,10 @@ class PantallaUnirse extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: const Text('Unirse a partida')),
appBar: AppBar(title: Text(l10n.joinGameTitle)),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32),
@@ -17,13 +20,12 @@ class PantallaUnirse extends StatelessWidget {
const Text('📱', style: TextStyle(fontSize: 64)),
const SizedBox(height: 24),
Text(
'Modo multimóvil',
l10n.multiDeviceMode,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 16),
Text(
'Escanea el código QR que muestra el host '
'para conectarte a la partida vía Bluetooth/WiFi Direct.',
l10n.scanQrDescription,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
@@ -41,17 +43,14 @@ class PantallaUnirse extends StatelessWidget {
const Text('🚧', style: TextStyle(fontSize: 32)),
const SizedBox(height: 8),
Text(
'Próximamente',
l10n.comingSoon,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: TemaApp.colorNaranja,
),
),
const SizedBox(height: 8),
Text(
'La conexión multimóvil con Nearby Connections '
'requiere dispositivos Android físicos.\n\n'
'Por ahora, usa el modo "Un solo móvil" '
'para jugar en un dispositivo compartido.',
l10n.nearbyNotAvailable,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
@@ -64,7 +63,7 @@ class PantallaUnirse extends StatelessWidget {
child: OutlinedButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back),
label: const Text('Volver'),
label: Text(l10n.back),
),
),
],

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart';
import '../estado/estado_juego.dart';
import '../modelos/palabra.dart';
@@ -17,6 +18,7 @@ class _PantallaVerPalabraState extends State<PantallaVerPalabra> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final estado = context.watch<EstadoJuego>();
final partida = estado.partida;
if (partida == null) return const SizedBox.shrink();
@@ -26,7 +28,7 @@ class _PantallaVerPalabraState extends State<PantallaVerPalabra> {
return Scaffold(
appBar: AppBar(
title: const Text('Ver tu palabra'),
title: Text(l10n.seeYourWord),
automaticallyImplyLeading: false,
),
body: Padding(
@@ -34,13 +36,13 @@ class _PantallaVerPalabraState extends State<PantallaVerPalabra> {
child: Column(
children: [
Text(
'Cada jugador debe ver su palabra en secreto',
l10n.eachPlayerMustSee,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Ronda ${partida.rondaActual}',
l10n.roundNumber(partida.rondaActual),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: TemaApp.colorNaranja,
),
@@ -69,7 +71,7 @@ class _PantallaVerPalabraState extends State<PantallaVerPalabra> {
),
title: Text(jugador.nombre),
subtitle: Text(
haVisto ? 'Ya ha visto su palabra' : 'Pulsa para ver',
haVisto ? l10n.alreadySeen : l10n.tapToSee,
),
trailing: haVisto
? const Icon(Icons.check_circle,
@@ -102,8 +104,8 @@ class _PantallaVerPalabraState extends State<PantallaVerPalabra> {
: null,
icon: const Icon(Icons.forum),
label: Text(todosHanVisto
? 'Todos han visto → Iniciar debate'
: 'Faltan ${partida.jugadores.length - _hanVisto.length} jugadores'),
? l10n.allSeenStartDebate
: l10n.playersRemaining(partida.jugadores.length - _hanVisto.length)),
),
),
],
@@ -163,6 +165,8 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(widget.nombre)),
body: Center(
@@ -208,8 +212,8 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
const SizedBox(height: 16),
Text(
widget.esImpostor
? '¡Eres el impostor!'
: 'Tu palabra es:',
? l10n.youAreImpostor
: l10n.yourWordIs,
style: Theme.of(context)
.textTheme
.titleLarge
@@ -236,7 +240,7 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
if (widget.esImpostor && widget.pistaActiva) ...[
const SizedBox(height: 12),
Text(
'Pista: ${BancoPalabras.nombreBonitoCategoria(widget.categoria)}',
l10n.clueCategory(BancoPalabras.nombreBonitoCategoria(widget.categoria, l10n)),
style: Theme.of(context)
.textTheme
.bodyLarge
@@ -252,13 +256,13 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
const Text('🔒', style: TextStyle(fontSize: 48)),
const SizedBox(height: 16),
Text(
'Mantén pulsado para ver tu palabra',
l10n.holdToSeeWord,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Asegúrate de que nadie más mira',
l10n.makeSureNoOneLooks,
style: Theme.of(context).textTheme.bodyMedium,
),
],
@@ -291,8 +295,8 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
child: Center(
child: Text(
_manteniendo
? '👁️ Mostrando...'
: '👆 Mantén pulsado para ver',
? l10n.showingWord
: l10n.holdToSee,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
@@ -313,7 +317,7 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
Navigator.pop(context);
},
icon: const Icon(Icons.check),
label: const Text('He visto mi palabra'),
label: Text(l10n.seenMyWord),
),
),
],

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:el_impostor/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart';
import '../estado/estado_juego.dart';
import '../tema/tema_app.dart';
@@ -48,12 +49,13 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
return _construirTodosVotaron(context, estado);
}
final l10n = AppLocalizations.of(context)!;
final votanteActual = sinVotar.isNotEmpty ? sinVotar[0] : activos[0];
final puedenRecibir = activos.where((j) => j.id != votanteActual.id).toList();
return Scaffold(
appBar: AppBar(
title: const Text('🗳️ Votación'),
title: Text(l10n.voting),
automaticallyImplyLeading: false,
),
body: Padding(
@@ -71,7 +73,7 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
child: Column(
children: [
Text(
'Turno de votar:',
l10n.turnToVote,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 4),
@@ -83,7 +85,7 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
),
const SizedBox(height: 8),
Text(
'Votos: ${estado.votos.length}/${activos.length}',
l10n.votesProgress(estado.votos.length, activos.length),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 4),
@@ -102,7 +104,7 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
const SizedBox(height: 16),
Text(
'¿Quién crees que es el impostor?',
l10n.whoIsImpostor,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
@@ -155,7 +157,7 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
}
: null,
icon: const Icon(Icons.how_to_vote),
label: const Text('Confirmar voto'),
label: Text(l10n.confirmVote),
),
),
],
@@ -165,9 +167,11 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
}
Widget _construirTodosVotaron(BuildContext context, EstadoJuego estado) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: const Text('🗳️ Votación completa'),
title: Text(l10n.votingComplete),
automaticallyImplyLeading: false,
),
body: Center(
@@ -179,12 +183,12 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
const Text('🗳️', style: TextStyle(fontSize: 64)),
const SizedBox(height: 24),
Text(
'¡Todos han votado!',
l10n.allVoted,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 16),
Text(
'Pulsa para revelar el resultado',
l10n.tapToReveal,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
@@ -205,7 +209,7 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
}
},
icon: const Icon(Icons.visibility),
label: const Text('Revelar resultado'),
label: Text(l10n.revealResult),
),
),
],