internal/Export/Get-GuerrillaReportLocale.ps1
|
# Guerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/Guerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution # # Report localization. Two catalog kinds, both under Data/Locales: # # report.<code>.json the report SHELL: section titles, table headers, # labels, executive-summary prose, footer notes. # English (report.en.json) is the source; other # locales carry { value, status } entries, the same # provenance convention the GUI and website use. # # checks/<code>/<family>.json the report CONTENT: per-check translations keyed # by CheckId, each { name, description, # recommendedValue, remediationSteps } with a # status. One file per check family so a reviewer # can review one family at a time. # # The honest boundary: only STATIC check-definition text is translated. Live # collected evidence (a finding's CurrentValue, affected accounts, attack-path # strings) is rendered as collected, because it is data the scan observed, not # authored prose. A missing translation (whole language, one check, or one # field) falls back to English, so a partial catalog degrades to mixed-language, # never to a blank report. function Get-GuerrillaReportLocaleRoot { [CmdletBinding()] param() $base = $null try { $base = $ExecutionContext.SessionState.Module.ModuleBase } catch { } if (-not $base) { $base = Join-Path $PSScriptRoot '..' '..' } return (Join-Path $base 'Data' 'Locales') } function Get-GuerrillaReportLanguages { # Languages that have a report SHELL catalog (report.<code>.json). English # first, the rest alphabetical. A report language must have at least the # shell; check content can lag and fall back per key. [CmdletBinding()] param() $root = Get-GuerrillaReportLocaleRoot $langs = [System.Collections.Generic.List[object]]::new() foreach ($f in (Get-ChildItem -Path $root -Filter 'report.*.json' -ErrorAction SilentlyContinue | Sort-Object Name)) { try { $doc = Get-Content $f.FullName -Raw | ConvertFrom-Json -AsHashtable $meta = $doc['_language'] if ($meta -and $meta.code -and $meta.name) { $dir = if ("$($meta.direction)" -eq 'rtl') { 'rtl' } else { 'ltr' } $langs.Add([PSCustomObject]@{ Code = [string]$meta.code; Name = [string]$meta.name; Direction = $dir }) } } catch { } } return @(@($langs | Where-Object Code -eq 'en') + @($langs | Where-Object Code -ne 'en' | Sort-Object Code)) } function Resolve-GuerrillaReportLanguage { # Report language: explicit choice if that shell catalog exists, else the # GUI language if it has a report catalog, else the OS UI culture, else en. [CmdletBinding()] param([string]$Configured) $available = @((Get-GuerrillaReportLanguages).Code) if ($Configured -and $Configured -in $available) { return $Configured } if ($script:GuerrillaGuiLanguage -and $script:GuerrillaGuiLanguage -in $available) { return $script:GuerrillaGuiLanguage } try { $os = [System.Globalization.CultureInfo]::CurrentUICulture.TwoLetterISOLanguageName if ($os -in $available) { return $os } } catch { } return 'en' } # Flattened dot-key -> string table for the report shell, English as the # fallback for any key a language does not carry. Cached per language. $script:GuerrillaReportStringCache = @{} function Get-GuerrillaReportStringTable { [CmdletBinding()] param([string]$Language = 'en') if ($script:GuerrillaReportStringCache.ContainsKey($Language)) { return $script:GuerrillaReportStringCache[$Language] } $root = Get-GuerrillaReportLocaleRoot $flatten = { param($Node, $Prefix, $Table) foreach ($k in $Node.Keys) { if ($k -like '_*') { continue } $key = if ($Prefix) { "$Prefix.$k" } else { $k } $v = $Node[$k] if ($v -is [System.Collections.IDictionary]) { if ($v.Contains('value')) { $Table[$key] = [string]$v['value'] } else { & $flatten $v $key $Table } } else { $Table[$key] = [string]$v } } } $load = { param($Code) $path = Join-Path $root "report.$Code.json" if (-not (Test-Path $path)) { return $null } try { Get-Content $path -Raw | ConvertFrom-Json -AsHashtable } catch { $null } } $table = @{} $en = & $load 'en' if ($en) { & $flatten $en '' $table } if ($Language -and $Language -ne 'en') { $loc = & $load $Language if ($loc) { & $flatten $loc '' $table } } $script:GuerrillaReportStringCache[$Language] = $table return $table } # Convenience: a scriptblock that looks up a report-shell key and HTML-encodes # the result. Exporters do: $t = Get-GuerrillaReportStringResolver -Language es # then $(& $t 'ad.title') in place of the old literal, and # $(& $t 'ioe.affected' $count) for format strings. function Get-GuerrillaReportStringResolver { [CmdletBinding()] param([string]$Language = 'en', [switch]$Raw) $table = Get-GuerrillaReportStringTable -Language $Language if ($Raw) { return { param([string]$Key) $s = $table[$Key] if ($null -eq $s) { return $Key } if ($args.Count -gt 0) { return ($s -f $args) } return $s }.GetNewClosure() } return { param([string]$Key) $s = $table[$Key] if ($null -eq $s) { $s = $Key } elseif ($args.Count -gt 0) { $s = $s -f $args } return [System.Web.HttpUtility]::HtmlEncode($s) }.GetNewClosure() } # Load the per-check translation table for a language: CheckId -> hashtable of # translated fields. Missing file/field falls back to English at apply time. # Cached per language. $script:GuerrillaCheckContentCache = @{} function Get-GuerrillaCheckContentTable { [CmdletBinding()] param([string]$Language) if (-not $Language -or $Language -eq 'en') { return @{} } if ($script:GuerrillaCheckContentCache.ContainsKey($Language)) { return $script:GuerrillaCheckContentCache[$Language] } $dir = Join-Path (Get-GuerrillaReportLocaleRoot) 'checks' $Language $table = @{} if (Test-Path $dir) { foreach ($f in (Get-ChildItem -Path $dir -Filter '*.json' -ErrorAction SilentlyContinue)) { try { $doc = Get-Content $f.FullName -Raw | ConvertFrom-Json -AsHashtable foreach ($id in $doc.Keys) { if ($id -like '_*') { continue } $table[[string]$id] = $doc[$id] } } catch { } } } $script:GuerrillaCheckContentCache[$Language] = $table return $table } # Return a copy of the findings with their STATIC definition text swapped to the # target language (CheckName, Description, RecommendedValue, RemediationSteps, # Category), leaving live evidence (CurrentValue, Details, Severity, Status, # Compliance, CheckId) untouched. English passes through unchanged. Each # translated field falls back to the original when the catalog lacks it. function Get-GuerrillaLocalizedFindings { [CmdletBinding()] param( [Parameter(Mandatory)][AllowEmptyCollection()][AllowNull()][PSCustomObject[]]$Findings, [string]$Language = 'en' ) if (-not $Findings -or $Findings.Count -eq 0) { return @($Findings) } if (-not $Language -or $Language -eq 'en') { return @($Findings) } $content = Get-GuerrillaCheckContentTable -Language $Language if ($content.Count -eq 0) { return @($Findings) } # Map catalog field name -> finding property name. Category is deliberately # NOT here: findings group by Category, so the grouping key stays English # (stable even with a partial catalog) and only the printed category LABEL is # localized, via Get-GuerrillaLocalizedCategoryName at the render site. $fieldMap = [ordered]@{ name = 'CheckName' description = 'Description' recommendedValue = 'RecommendedValue' remediationSteps = 'RemediationSteps' } $pick = { param($Entry, $Key) if (-not $Entry) { return $null } $v = $Entry[$Key] if ($null -eq $v) { return $null } if ($v -is [System.Collections.IDictionary]) { $v = $v['value'] } $s = [string]$v if ([string]::IsNullOrWhiteSpace($s)) { return $null } return $s } $out = [System.Collections.Generic.List[object]]::new() foreach ($f in $Findings) { $entry = $content["$($f.CheckId)"] if (-not $entry) { $out.Add($f); continue } $clone = $f.PSObject.Copy() foreach ($ck in $fieldMap.Keys) { $t = & $pick $entry $ck if ($null -ne $t) { $clone.($fieldMap[$ck]) = $t } } $out.Add($clone) } return @($out) } # Localize a category display name via the shell catalog's `category` namespace, # keyed by the English category name lowercased with non-alphanumerics stripped # (e.g. "AD ACL & Delegation" -> category.adacldelegation). Returns the English # name unchanged when no translation exists, so grouping keys stay English and # only the label localizes. function Get-GuerrillaLocalizedCategoryName { [CmdletBinding()] param([string]$Name, [string]$Language = 'en') if (-not $Name -or -not $Language -or $Language -eq 'en') { return $Name } $table = Get-GuerrillaReportStringTable -Language $Language $slug = 'category.' + ([regex]::Replace($Name.ToLower(), '[^a-z0-9]', '')) $v = $table[$slug] if ([string]::IsNullOrWhiteSpace($v)) { return $Name } return $v } |