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"