traducciones
This commit is contained in:
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
@@ -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'
|
||||
jsonStr = await rootBundle.loadString(
|
||||
'assets/words/palabras_$idioma.json',
|
||||
);
|
||||
} catch (_) {
|
||||
try {
|
||||
final archivoLegacy = idioma == 'es'
|
||||
? 'assets/palabras.json'
|
||||
: 'assets/palabras_$idioma.json';
|
||||
jsonStr = await rootBundle.loadString(archivo);
|
||||
jsonStr = await rootBundle.loadString(archivoLegacy);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
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]!;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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/
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user