private/localization/Convert-CtResultText.ps1
|
<#
.SYNOPSIS Tradutor pt-BR do texto de RESULTADO dos testes, por dicionário de padrões (offline). .DESCRIPTION O texto de resultado é gerado dinamicamente pela lógica de cada teste, a partir de templates fixos com valores do cliente interpolados. Traduzir esses literais na fonte é inseguro: os mesmos tokens (ex.: 'Enabled', 'Pass') também são usados em comparações lógicas contra dados da API. Por isso a tradução do resultado é feita aqui, em tempo de execução, sobre o texto JÁ RENDERIZADO — depois que toda a lógica de comparação rodou. Estratégia (baseada no dicionário assets/result-dictionary.json): 1. Entradas exatas (sem placeholder) -> hashtable, busca O(1). 2. Entradas com placeholders {0},{1}... -> regex com grupos de captura, indexadas pelo prefixo literal para não testar milhares de regex por linha. 3. Traduz linha a linha preservando os VALORES do cliente capturados. Fallback seguro: linha sem correspondência permanece em inglês. Adaptado do ResultTranslator.ps1 (Cloud Target). Inicialização preguiçosa: a primeira chamada a Convert-CtResultText carrega o dicionário dos assets. #> $script:CtExact = @{} # texto EN exato -> PT $script:CtByPrefix = @{} # prefixo (8 chars, minúsculo) -> lista de padrões $script:CtNoPrefix = @() # padrões que começam com placeholder $script:CtCell = @{} # termos de célula de tabela (lista fechada) $script:CtLoaded = $false function Initialize-CtResultTranslator { [CmdletBinding()] param([string]$DictionaryPath) if (-not $DictionaryPath) { $DictionaryPath = Join-Path $script:ModuleRoot 'assets/result-dictionary.json' } $script:CtExact = @{}; $script:CtByPrefix = @{}; $script:CtNoPrefix = @(); $script:CtLoaded = $false # Termos de célula de tabela. Lista fechada e curta: só entra o que NUNCA é # dado do cliente (status, sim/não, estados de política). $script:CtCell = @{ 'Pass' = 'Aprovado'; '✅ Pass' = '✅ Aprovado'; '✅ Passed' = '✅ Aprovado' 'Fail' = 'Reprovado'; '❌ Fail' = '❌ Reprovado'; '❌ Failed' = '❌ Reprovado' 'Enabled' = 'Habilitado'; '✅ Enabled' = '✅ Habilitado'; '❌ Enabled' = '❌ Habilitado' 'Disabled' = 'Desabilitado'; '❌ Disabled' = '❌ Desabilitado'; '✅ Disabled' = '✅ Desabilitado' 'Yes' = 'Sim'; '✅ Yes' = '✅ Sim'; '❌ Yes' = '❌ Sim' 'No' = 'Não'; '✅ No' = '✅ Não'; '❌ No' = '❌ Não' 'None' = 'Nenhum'; 'Unknown' = 'Desconhecido' 'Not configured' = 'Não configurado'; '❌ Not configured' = '❌ Não configurado' 'Not applicable' = 'Não aplicável'; 'Not found' = 'Não encontrado' 'Investigate' = 'Investigar'; '⚠️ Investigate' = '⚠️ Investigar' 'Valid' = 'Válido'; 'Expired' = 'Vencido'; 'Skipped' = 'Ignorado' 'Allowed' = 'Permitido'; '✅ Allowed' = '✅ Permitido' 'Blocked' = 'Bloqueado'; '❌ Blocked' = '❌ Bloqueado' 'Compliant' = 'Conforme'; 'Non-compliant' = 'Não conforme' 'Covered' = 'Coberto'; '✅ Covered' = '✅ Coberto' 'Member' = 'Membro'; 'Guest' = 'Convidado'; 'User' = 'Usuário'; 'Group' = 'Grupo' 'Report-only' = 'Somente relatório'; 'Prevention' = 'Prevention' 'No data available' = 'Nenhum dado disponível' 'Permanent' = 'Permanente'; 'Eligible' = 'Elegível'; 'Active' = 'Ativo' } if (-not (Test-Path $DictionaryPath)) { $script:CtLoaded = $true; return 0 } $dic = Get-Content $DictionaryPath -Raw -Encoding UTF8 | ConvertFrom-Json $n = 0 foreach ($p in $dic.entries.PSObject.Properties) { $en = $p.Name; $pt = $p.Value if ([string]::IsNullOrWhiteSpace($pt)) { continue } $n++ if ($en -notmatch '\{\d+\}') { $script:CtExact[$en] = $pt # o vendor usa '%TestResult%' como marcador de concatenação; a linha # renderizada pode chegar sem ele. Registra a variante enxuta também. if ($en.EndsWith('%TestResult%')) { $bare = $en.Substring(0, $en.Length - 12).TrimEnd() if ($bare -and -not $script:CtExact.ContainsKey($bare)) { $script:CtExact[$bare] = $pt.Replace('%TestResult%', '').TrimEnd() } } continue } # padrão com placeholders -> regex ancorado, placeholders viram capturas. # Atenção: [regex]::Escape escapa '{' mas NÃO escapa '}'. $rx = [regex]::Escape($en) -replace '\\\{(\d+)\}', '(.*?)' $entry = [pscustomobject]@{ Regex = [regex]::new('^' + $rx + '$', 'Compiled') Pt = $pt Len = $en.Length } $idx = $en.IndexOf('{') $prefix = if ($idx -ge 1) { $en.Substring(0, [Math]::Min(8, $idx)).ToLowerInvariant() } else { '' } if ($prefix) { if (-not $script:CtByPrefix.ContainsKey($prefix)) { $script:CtByPrefix[$prefix] = @() } $script:CtByPrefix[$prefix] += $entry } else { $script:CtNoPrefix += $entry } } # mais específicos (mais longos) primeiro foreach ($k in @($script:CtByPrefix.Keys)) { $script:CtByPrefix[$k] = @($script:CtByPrefix[$k] | Sort-Object { -$_.Len }) } $script:CtNoPrefix = @($script:CtNoPrefix | Sort-Object { -$_.Len }) $script:CtLoaded = $true return $n } function Convert-CtResultLine { param([string]$Line) $trimmed = $Line.Trim() if ($trimmed.Length -lt 3) { return $Line } $indent = $Line.Substring(0, $Line.Length - $Line.TrimStart().Length) # 1) correspondência exata if ($script:CtExact.ContainsKey($trimmed)) { return $indent + $script:CtExact[$trimmed] } # 2) padrões, filtrados por prefixo $cands = @() if ($trimmed.Length -ge 1) { $pfx = $trimmed.Substring(0, [Math]::Min(8, $trimmed.Length)).ToLowerInvariant() for ($l = $pfx.Length; $l -ge 1; $l--) { $key = $pfx.Substring(0, $l) if ($script:CtByPrefix.ContainsKey($key)) { $cands += $script:CtByPrefix[$key] } } } $cands += $script:CtNoPrefix foreach ($c in $cands) { $m = $c.Regex.Match($trimmed) if ($m.Success) { $pt = $c.Pt for ($i = 1; $i -lt $m.Groups.Count; $i++) { $pt = $pt.Replace("{$($i-1)}", $m.Groups[$i].Value) } return $indent + $pt } } # 3) afixos markdown: tenta traduzir o miolo (## Título, - item, **Título**) $m3 = [regex]::Match($trimmed, '^(#{1,6}\s+|[-*]\s+|\*\*)(.+?)(\*\*)?$') if ($m3.Success) { $core = $m3.Groups[2].Value $ptCore = Convert-CtResultLine -Line $core if ($ptCore -ne $core) { return $indent + $m3.Groups[1].Value + $ptCore + $m3.Groups[3].Value } } # 4) título dentro de link markdown: [Texto](url) -> traduz só o texto, # preservando a URL intacta. $m4 = [regex]::Match($trimmed, '^\[([^\]]+)\]\(([^)]+)\)$') if ($m4.Success) { $core = $m4.Groups[1].Value if ($script:CtExact.ContainsKey($core)) { return $indent + '[' + $script:CtExact[$core] + '](' + $m4.Groups[2].Value + ')' } } # 5) linhas de tabela: traduz apenas células que batem exatamente com um # termo conhecido (Pass/Fail/Enabled/...). Dado do cliente nunca casa. if ($trimmed.StartsWith('|') -and $trimmed.Contains('|')) { $cells = $trimmed -split '\|' $changed = $false for ($i = 0; $i -lt $cells.Count; $i++) { $cell = $cells[$i].Trim() if ($cell -and $script:CtCell.ContainsKey($cell)) { $cells[$i] = ' ' + $script:CtCell[$cell] + ' ' $changed = $true } } if ($changed) { return $indent + ($cells -join '|') } } return $Line } function Convert-CtResultText { [CmdletBinding()] param([string]$Text) if (-not $script:CtLoaded) { $null = Initialize-CtResultTranslator } if ([string]::IsNullOrEmpty($Text)) { return $Text } $lines = $Text -split "`n" $outLines = foreach ($ln in $lines) { Convert-CtResultLine -Line ($ln.TrimEnd("`r")) } return ($outLines -join "`n") } |