traducciones
This commit is contained in:
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_ar.json
Normal file
1056
assets/words/palabras_ar.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_ca.json
Normal file
1056
assets/words/palabras_ca.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_de.json
Normal file
1056
assets/words/palabras_de.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_en.json
Normal file
1056
assets/words/palabras_en.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_es.json
Normal file
1056
assets/words/palabras_es.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_eu.json
Normal file
1056
assets/words/palabras_eu.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_fr.json
Normal file
1056
assets/words/palabras_fr.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_hi.json
Normal file
1056
assets/words/palabras_hi.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_it.json
Normal file
1056
assets/words/palabras_it.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_ja.json
Normal file
1056
assets/words/palabras_ja.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_ko.json
Normal file
1056
assets/words/palabras_ko.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_nl.json
Normal file
1056
assets/words/palabras_nl.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_pl.json
Normal file
1056
assets/words/palabras_pl.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_pt.json
Normal file
1056
assets/words/palabras_pt.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_ru.json
Normal file
1056
assets/words/palabras_ru.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_tr.json
Normal file
1056
assets/words/palabras_tr.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_zh.json
Normal file
1056
assets/words/palabras_zh.json
Normal file
File diff suppressed because it is too large
Load Diff
1056
assets/words/palabras_zh_TW.json
Normal file
1056
assets/words/palabras_zh_TW.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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]!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/
|
||||
|
||||
216
tools/generate_word_banks.ps1
Normal file
216
tools/generate_word_banks.ps1
Normal 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"
|
||||
Reference in New Issue
Block a user