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 '../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,