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
+45 -43
View File
@@ -1,47 +1,61 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:farolero/l10n/generated/app_localizations.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 { class BancoPalabras {
final Map<String, List<String>> categorias; 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 final Map<String, BancoPalabras> _instancias = {};
static Future<BancoPalabras> cargar({String idioma = 'es'}) async { static Future<BancoPalabras> cargar({String idioma = 'es'}) async {
if (_instancias.containsKey(idioma)) return _instancias[idioma]!; if (_instancias.containsKey(idioma)) return _instancias[idioma]!;
// Intentar cargar el banco del idioma solicitado, fallback a castellano
String jsonStr; String jsonStr;
try { try {
final archivo = idioma == 'es' jsonStr = await rootBundle.loadString(
'assets/words/palabras_$idioma.json',
);
} catch (_) {
try {
final archivoLegacy = idioma == 'es'
? 'assets/palabras.json' ? 'assets/palabras.json'
: 'assets/palabras_$idioma.json'; : 'assets/palabras_$idioma.json';
jsonStr = await rootBundle.loadString(archivo); jsonStr = await rootBundle.loadString(archivoLegacy);
} catch (_) { } catch (_) {
// Fallback a castellano si no existe el banco para ese idioma if (idioma != 'es') return cargar(idioma: 'es');
if (idioma != 'es') {
return cargar(idioma: 'es');
}
rethrow; rethrow;
} }
}
final data = json.decode(jsonStr) as Map<String, dynamic>; final data = json.decode(jsonStr) as Map<String, dynamic>;
final cats = data['categorias'] as Map<String, dynamic>; final cats = data['categorias'] as Map<String, dynamic>;
final mapa = <String, List<String>>{}; final mapa = <String, List<String>>{};
final pistas = <String, String>{};
for (final entrada in cats.entries) { 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]!; return _instancias[idioma]!;
} }
List<String> get nombresCategorias => categorias.keys.toList(); 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) { String palabraAleatoria(String? categoria) {
final rng = Random(); final rng = Random();
if (categoria == null || categoria == 'todas') { if (categoria == null || categoria == 'todas') {
@@ -52,7 +66,7 @@ class BancoPalabras {
return lista[rng.nextInt(lista.length)]; 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) { String? categoriaDepalabra(String palabra) {
for (final entrada in categorias.entries) { for (final entrada in categorias.entries) {
if (entrada.value.contains(palabra)) return entrada.key; if (entrada.value.contains(palabra)) return entrada.key;
@@ -60,7 +74,10 @@ class BancoPalabras {
return null; 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]) { static String nombreBonitoCategoria(String clave, [AppLocalizations? l10n]) {
if (l10n != null) { if (l10n != null) {
final nombres = { final nombres = {
@@ -78,7 +95,6 @@ class BancoPalabras {
}; };
return nombres[clave] ?? clave; return nombres[clave] ?? clave;
} }
// Fallback a castellano si no hay l10n
const nombres = { const nombres = {
'todas': 'Todas', 'todas': 'Todas',
'animales': 'Animales', 'animales': 'Animales',
@@ -98,12 +114,9 @@ class BancoPalabras {
class EntradaPalabraTraducida { class EntradaPalabraTraducida {
final String palabra; final String palabra;
final Map<String, String> traducciones; final String pista;
const EntradaPalabraTraducida({ const EntradaPalabraTraducida({required this.palabra, required this.pista});
required this.palabra,
required this.traducciones,
});
} }
class BancoPalabrasTraducidas { class BancoPalabrasTraducidas {
@@ -111,32 +124,21 @@ class BancoPalabrasTraducidas {
const BancoPalabrasTraducidas(this.categorias); const BancoPalabrasTraducidas(this.categorias);
static BancoPalabrasTraducidas? _instancia; static final Map<String, BancoPalabrasTraducidas> _instancias = {};
static Future<BancoPalabrasTraducidas> cargar() async { static Future<BancoPalabrasTraducidas> cargar({String idioma = 'es'}) async {
if (_instancia != null) return _instancia!; if (_instancias.containsKey(idioma)) return _instancias[idioma]!;
final jsonStr = await rootBundle.loadString('assets/palabras_i18n.json'); final banco = await BancoPalabras.cargar(idioma: idioma);
final data = json.decode(jsonStr) as Map<String, dynamic>;
final cats = data['categorias'] as Map<String, dynamic>;
final mapa = <String, List<EntradaPalabraTraducida>>{}; final mapa = <String, List<EntradaPalabraTraducida>>{};
for (final categoria in banco.categorias.entries) {
for (final categoria in cats.entries) { final pista = banco.pistaDeCategoria(categoria.key) ?? categoria.key;
final entradas = categoria.value as List<dynamic>; mapa[categoria.key] = categoria.value
mapa[categoria.key] = entradas.map((entradaRaw) { .map((palabra) => EntradaPalabraTraducida(palabra: palabra, pista: pista))
final entrada = entradaRaw as Map<String, dynamic>; .toList();
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();
} }
_instancia = BancoPalabrasTraducidas(mapa); _instancias[idioma] = BancoPalabrasTraducidas(mapa);
return _instancia!; return _instancias[idioma]!;
} }
} }
+1 -1
View File
@@ -30,7 +30,7 @@ flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/palabras.json - assets/palabras.json
- assets/palabras_i18n.json
- assets/palabras_en.json - assets/palabras_en.json
- assets/palabras_fr.json - assets/palabras_fr.json
- assets/words/
- assets/avatars/ - assets/avatars/
+216
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"