traducciones

This commit is contained in:
2026-05-04 22:23:11 +02:00
parent 00dc3ee5e1
commit 3b0b10ea50
22 changed files with 19272 additions and 22072 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,61 @@
import 'dart:convert';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:farolero/l10n/generated/app_localizations.dart';
/// Categorías disponibles en el banco de palabras
/// Categorías disponibles en el banco de palabras.
class BancoPalabras {
final Map<String, List<String>> categorias;
final Map<String, String> pistasPorCategoria;
BancoPalabras(this.categorias);
BancoPalabras(this.categorias, {Map<String, String>? pistasPorCategoria})
: pistasPorCategoria = pistasPorCategoria ?? {};
static final Map<String, BancoPalabras> _instancias = {};
static Future<BancoPalabras> cargar({String idioma = 'es'}) async {
if (_instancias.containsKey(idioma)) return _instancias[idioma]!;
// Intentar cargar el banco del idioma solicitado, fallback a castellano
String jsonStr;
try {
final archivo = idioma == 'es'
? 'assets/palabras.json'
: 'assets/palabras_$idioma.json';
jsonStr = await rootBundle.loadString(archivo);
jsonStr = await rootBundle.loadString(
'assets/words/palabras_$idioma.json',
);
} catch (_) {
// Fallback a castellano si no existe el banco para ese idioma
if (idioma != 'es') {
return cargar(idioma: 'es');
try {
final archivoLegacy = idioma == 'es'
? 'assets/palabras.json'
: 'assets/palabras_$idioma.json';
jsonStr = await rootBundle.loadString(archivoLegacy);
} catch (_) {
if (idioma != 'es') return cargar(idioma: 'es');
rethrow;
}
rethrow;
}
final data = json.decode(jsonStr) as Map<String, dynamic>;
final cats = data['categorias'] as Map<String, dynamic>;
final mapa = <String, List<String>>{};
final pistas = <String, String>{};
for (final entrada in cats.entries) {
mapa[entrada.key] = List<String>.from(entrada.value);
final valor = entrada.value;
if (valor is Map<String, dynamic>) {
mapa[entrada.key] = List<String>.from(valor['palabras'] as List);
final pista = valor['pista'];
if (pista is String && pista.isNotEmpty) pistas[entrada.key] = pista;
} else {
mapa[entrada.key] = List<String>.from(valor as List);
}
}
_instancias[idioma] = BancoPalabras(mapa);
_instancias[idioma] = BancoPalabras(mapa, pistasPorCategoria: pistas);
return _instancias[idioma]!;
}
List<String> get nombresCategorias => categorias.keys.toList();
/// Obtiene una palabra aleatoria de la categoría dada (o de todas si es null)
/// Obtiene una palabra aleatoria de la categoría dada (o de todas si es null).
String palabraAleatoria(String? categoria) {
final rng = Random();
if (categoria == null || categoria == 'todas') {
@@ -52,7 +66,7 @@ class BancoPalabras {
return lista[rng.nextInt(lista.length)];
}
/// Devuelve la categoría a la que pertenece una palabra
/// Devuelve la categoría a la que pertenece una palabra.
String? categoriaDepalabra(String palabra) {
for (final entrada in categorias.entries) {
if (entrada.value.contains(palabra)) return entrada.key;
@@ -60,7 +74,10 @@ class BancoPalabras {
return null;
}
/// Devuelve el nombre localizado de la categoría usando AppLocalizations
/// Devuelve la pista localizada de una categoría si el banco la trae.
String? pistaDeCategoria(String categoria) => pistasPorCategoria[categoria];
/// Devuelve el nombre localizado de la categoría usando AppLocalizations.
static String nombreBonitoCategoria(String clave, [AppLocalizations? l10n]) {
if (l10n != null) {
final nombres = {
@@ -78,7 +95,6 @@ class BancoPalabras {
};
return nombres[clave] ?? clave;
}
// Fallback a castellano si no hay l10n
const nombres = {
'todas': 'Todas',
'animales': 'Animales',
@@ -98,12 +114,9 @@ class BancoPalabras {
class EntradaPalabraTraducida {
final String palabra;
final Map<String, String> traducciones;
final String pista;
const EntradaPalabraTraducida({
required this.palabra,
required this.traducciones,
});
const EntradaPalabraTraducida({required this.palabra, required this.pista});
}
class BancoPalabrasTraducidas {
@@ -111,32 +124,21 @@ class BancoPalabrasTraducidas {
const BancoPalabrasTraducidas(this.categorias);
static BancoPalabrasTraducidas? _instancia;
static final Map<String, BancoPalabrasTraducidas> _instancias = {};
static Future<BancoPalabrasTraducidas> cargar() async {
if (_instancia != null) return _instancia!;
static Future<BancoPalabrasTraducidas> cargar({String idioma = 'es'}) async {
if (_instancias.containsKey(idioma)) return _instancias[idioma]!;
final jsonStr = await rootBundle.loadString('assets/palabras_i18n.json');
final data = json.decode(jsonStr) as Map<String, dynamic>;
final cats = data['categorias'] as Map<String, dynamic>;
final banco = await BancoPalabras.cargar(idioma: idioma);
final mapa = <String, List<EntradaPalabraTraducida>>{};
for (final categoria in cats.entries) {
final entradas = categoria.value as List<dynamic>;
mapa[categoria.key] = entradas.map((entradaRaw) {
final entrada = entradaRaw as Map<String, dynamic>;
final traduccionesRaw =
entrada['traducciones'] as Map<String, dynamic>? ?? {};
return EntradaPalabraTraducida(
palabra: entrada['es'] as String,
traducciones: traduccionesRaw.map(
(idioma, valor) => MapEntry(idioma, valor?.toString() ?? ''),
)..removeWhere((_, valor) => valor.isEmpty),
);
}).toList();
for (final categoria in banco.categorias.entries) {
final pista = banco.pistaDeCategoria(categoria.key) ?? categoria.key;
mapa[categoria.key] = categoria.value
.map((palabra) => EntradaPalabraTraducida(palabra: palabra, pista: pista))
.toList();
}
_instancia = BancoPalabrasTraducidas(mapa);
return _instancia!;
_instancias[idioma] = BancoPalabrasTraducidas(mapa);
return _instancias[idioma]!;
}
}

View File

@@ -30,7 +30,7 @@ flutter:
uses-material-design: true
assets:
- assets/palabras.json
- assets/palabras_i18n.json
- assets/palabras_en.json
- assets/palabras_fr.json
- assets/words/
- assets/avatars/

View File

@@ -0,0 +1,216 @@
param(
[string]$OutputDir = 'assets/words',
[int]$BatchSize = 100
)
$ErrorActionPreference = 'Stop'
$langMap = [ordered]@{
ar = 'ar'
ca = 'ca'
de = 'de'
en = 'en'
es = 'es'
eu = 'eu'
fr = 'fr'
hi = 'hi'
it = 'it'
ja = 'ja'
ko = 'ko'
nl = 'nl'
pl = 'pl'
pt = 'pt'
ru = 'ru'
tr = 'tr'
zh = 'zh-CN'
zh_TW = 'zh-TW'
}
$categoryKeyMap = [ordered]@{
animales = 'categoryAnimals'
comida = 'categoryFood'
paises = 'categoryCountries'
deportes = 'categorySports'
profesiones = 'categoryProfessions'
objetos = 'categoryObjects'
lugares = 'categoryPlaces'
peliculas = 'categoryMovies'
musica = 'categoryMusic'
tecnologia = 'categoryTechnology'
}
$contextMap = [ordered]@{
animales = 'animal'
comida = 'food'
paises = 'country'
deportes = 'sport'
profesiones = 'profession'
objetos = 'object'
lugares = 'place'
peliculas = 'movie'
musica = 'music'
tecnologia = 'technology'
}
$utf8Strict = [System.Text.UTF8Encoding]::new($false, $true)
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
function Read-Utf8Json([string]$path) {
$text = $utf8Strict.GetString([System.IO.File]::ReadAllBytes((Resolve-Path $path))).TrimStart([char]0xFEFF)
return $text | ConvertFrom-Json
}
function Write-Utf8Json([object]$obj, [string]$path) {
$full = Join-Path (Get-Location) $path
$dir = Split-Path -Parent $full
if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Force $dir | Out-Null }
[System.IO.File]::WriteAllText($full, ($obj | ConvertTo-Json -Depth 10) + "`n", $utf8NoBom)
}
function Strip-Context([string]$value) {
$clean = $value.Trim()
if ($clean -match '^\s*[^:]{1,30}\s*[:]\s*') {
return ($clean -replace '^\s*[^:]{1,30}\s*[:]\s*', '').Trim()
}
return $clean
}
function Translate-Batch([string[]]$terms, [string]$target) {
if ($terms.Count -eq 0) { return @() }
$numbered = New-Object System.Collections.Generic.List[string]
for ($i = 0; $i -lt $terms.Count; $i++) {
$numbered.Add("[$i] $($terms[$i])")
}
$query = [uri]::EscapeDataString(($numbered -join "`n"))
$url = "https://translate.googleapis.com/translate_a/single?client=gtx&sl=es&tl=$target&dt=t&q=$query"
try {
$response = Invoke-RestMethod -Uri $url -TimeoutSec 45
$translated = (($response[0] | ForEach-Object { $_[0] }) -join '')
$matches = [regex]::Matches(
$translated,
'(?s)\[(\d+)\]\s*(.*?)(?=\s*\[\d+\]\s*|$)'
)
if ($matches.Count -eq $terms.Count) {
$out = New-Object string[] $terms.Count
foreach ($match in $matches) {
$index = [int]$match.Groups[1].Value
if ($index -ge 0 -and $index -lt $terms.Count) {
$out[$index] = Strip-Context $match.Groups[2].Value
}
}
if (($out | Where-Object { $_ -eq $null -or $_.Trim().Length -eq 0 }).Count -eq 0) {
return $out
}
}
} catch {
Start-Sleep -Milliseconds 250
}
$out = New-Object System.Collections.Generic.List[string]
foreach ($term in $terms) {
$queryOne = [uri]::EscapeDataString($term)
$urlOne = "https://translate.googleapis.com/translate_a/single?client=gtx&sl=es&tl=$target&dt=t&q=$queryOne"
$translatedOne = $null
for ($attempt = 1; $attempt -le 4; $attempt++) {
try {
$responseOne = Invoke-RestMethod -Uri $urlOne -TimeoutSec 45
$translatedOne = (($responseOne[0] | ForEach-Object { $_[0] }) -join '')
break
} catch {
if ($attempt -eq 4) { throw }
Start-Sleep -Milliseconds (250 * $attempt)
}
}
$out.Add((Strip-Context $translatedOne))
Start-Sleep -Milliseconds 35
}
return $out.ToArray()
}
$sourceEs = Read-Utf8Json 'assets/palabras.json'
$sourceEn = Read-Utf8Json 'assets/palabras_en.json'
$sourceFr = Read-Utf8Json 'assets/palabras_fr.json'
$arbByLang = @{}
foreach ($lang in $langMap.Keys) {
$arbByLang[$lang] = Read-Utf8Json ("lib/l10n/app_{0}.arb" -f $lang)
}
function New-LanguageBank([string]$lang) {
$bank = [ordered]@{
version = 2
idioma = $lang
categorias = [ordered]@{}
}
foreach ($category in $categoryKeyMap.Keys) {
$labelKey = $categoryKeyMap[$category]
$words = switch ($lang) {
'es' { @($sourceEs.categorias.$category) }
'en' { @($sourceEn.categorias.$category) }
'fr' { @($sourceFr.categorias.$category) }
default { @() }
}
$bank.categorias[$category] = [ordered]@{
pista = [string]$arbByLang[$lang].$labelKey
palabras = @($words | ForEach-Object { [string]$_ })
}
}
return $bank
}
foreach ($lang in @('es', 'en', 'fr')) {
Write-Utf8Json (New-LanguageBank $lang) (Join-Path $OutputDir "palabras_$lang.json")
}
$targets = @($langMap.Keys | Where-Object { $_ -notin @('es', 'en', 'fr') })
foreach ($lang in $targets) {
Write-Host "Generating $lang..."
$bank = New-LanguageBank $lang
$targetCode = $langMap[$lang]
foreach ($category in $categoryKeyMap.Keys) {
$spanishWords = @($sourceEs.categorias.$category)
$context = $contextMap[$category]
$translatedWords = New-Object System.Collections.Generic.List[string]
for ($offset = 0; $offset -lt $spanishWords.Count; $offset += $BatchSize) {
$last = [Math]::Min($offset + $BatchSize - 1, $spanishWords.Count - 1)
$terms = New-Object System.Collections.Generic.List[string]
for ($index = $offset; $index -le $last; $index++) {
$terms.Add("${context}: $($spanishWords[$index])")
}
$translated = @(Translate-Batch $terms.ToArray() $targetCode)
foreach ($word in $translated) { $translatedWords.Add($word) }
Start-Sleep -Milliseconds 70
}
$bank.categorias[$category].palabras = $translatedWords.ToArray()
}
Write-Utf8Json $bank (Join-Path $OutputDir "palabras_$lang.json")
}
Write-Host 'Validating UTF-8 and sample accents...'
foreach ($lang in $langMap.Keys) {
$file = Join-Path $OutputDir "palabras_$lang.json"
$bytes = [System.IO.File]::ReadAllBytes((Resolve-Path $file))
$null = $utf8Strict.GetString($bytes)
if ($bytes.Length -ge 3 -and $bytes[0] -eq 239 -and $bytes[1] -eq 187 -and $bytes[2] -eq 191) {
throw "Unexpected UTF-8 BOM in $file"
}
$bank = Read-Utf8Json $file
foreach ($category in $categoryKeyMap.Keys) {
$expected = @($sourceEs.categorias.$category).Count
$actual = @($bank.categorias.$category.palabras).Count
if ($actual -ne $expected) { throw "Word count mismatch in $file / $category. Expected $expected, got $actual" }
}
}
$esBank = Read-Utf8Json (Join-Path $OutputDir 'palabras_es.json')
$leon = @($esBank.categorias.animales.palabras) | Where-Object { $_ -eq 'León' } | Select-Object -First 1
if ($leon -ne 'León') { throw 'León accent validation failed' }
Write-Host "OK: generated split word banks in $OutputDir"