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

@@ -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;
}
}