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 '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
@@ -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/
|
||||||
|
|||||||
@@ -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