Modules/businessdev.ALbuild.Apps/Public/Import-BcTranslationMemory.ps1
|
function Import-BcTranslationMemory { <# .SYNOPSIS Builds (and caches) a deterministic translation-memory index from prioritised reference XLIFFs. .DESCRIPTION Reads a prioritised, ordered list of reference XLIFF files and builds an in-memory index of known source->target translations, for Merge-BcTranslationMemory to apply to new units. Two keys per unit: a context-sensitive "GenNote|source" (e.g. "Table Vendor - Field No.|No.") and a plain "source" fallback. On a key collision the higher-priority reference wins (equal priority: the earlier reference wins), so an app's own / sibling-app terms override the Microsoft BC base. The index is cached under LOCALAPPDATA/ALbuild/tm (or ~/.local/share/ALbuild/tm) keyed by the reference files' paths + last-write-times + sizes, so the large BC base XLF (>100k units) is only re-parsed when a reference actually changed. Deterministic and offline: no network, no LLM. .PARAMETER Reference Ordered reference descriptors (highest priority first is NOT required - Priority decides). Each: [pscustomobject]@{ Files = <string[] paths or globs>; Origin = '<self|app:Name|bc-base>'; Priority = <int> } .PARAMETER ContextAware Also index the context-sensitive "GenNote|source" key. Default: $true. .PARAMETER CacheDir Override the cache directory (tests pass a temp dir). .PARAMETER NoCache Build fresh and do not read/write the cache. .OUTPUTS PSCustomObject { ContextAware, Count, Entries (hashtable key -> { Target, Origin, Priority }) }. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [object[]] $Reference, [bool] $ContextAware = $true, [string] $CacheDir, [switch] $NoCache ) $prop = { param($obj, [string] $name, $default) if ($null -eq $obj) { return $default } $p = $obj.PSObject.Properties | Where-Object { $_.Name -ieq $name } | Select-Object -First 1 if ($p -and $null -ne $p.Value) { return $p.Value } return $default } # Expand each descriptor's Files (paths/globs) to concrete, existing files, tagged with origin+priority. $refFiles = [System.Collections.Generic.List[object]]::new() foreach ($r in $Reference) { $origin = [string](& $prop $r 'Origin' 'unknown') $priority = [int](& $prop $r 'Priority' 0) foreach ($spec in @(& $prop $r 'Files' @())) { if ([string]::IsNullOrWhiteSpace($spec)) { continue } $resolved = @() if (Test-Path -LiteralPath $spec) { $resolved = @(Get-Item -LiteralPath $spec) } else { $resolved = @(Get-ChildItem -Path $spec -File -ErrorAction SilentlyContinue) } foreach ($f in $resolved) { if ($f.Name -like '*.g.xlf') { continue } $refFiles.Add([PSCustomObject]@{ Path = $f.FullName; Origin = $origin; Priority = $priority; Stamp = "$($f.LastWriteTimeUtc.Ticks):$($f.Length)" }) } } } # Cache key from the resolved reference set (path+stamp) and the context-aware flag. $keyMaterial = ($refFiles | Sort-Object Path | ForEach-Object { "$($_.Path)|$($_.Stamp)|$($_.Priority)" }) -join ';' $keyMaterial += "|ctx=$ContextAware" $sha = [System.Security.Cryptography.SHA256]::Create() $hash = [BitConverter]::ToString($sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($keyMaterial))).Replace('-', '').Substring(0, 32).ToLowerInvariant() $sha.Dispose() if (-not $CacheDir) { $base = if ($env:LOCALAPPDATA) { $env:LOCALAPPDATA } elseif ($env:XDG_DATA_HOME) { $env:XDG_DATA_HOME } else { Join-Path $HOME '.local/share' } $CacheDir = Join-Path $base 'ALbuild/tm' } $cacheFile = Join-Path $CacheDir "$hash.json" if (-not $NoCache -and (Test-Path -LiteralPath $cacheFile)) { try { $cached = Get-Content -LiteralPath $cacheFile -Raw -Encoding UTF8 | ConvertFrom-Json $entries = @{} foreach ($e in $cached.Entries.PSObject.Properties) { $entries[$e.Name] = [PSCustomObject]@{ Target = $e.Value.Target; Origin = $e.Value.Origin; Priority = [int]$e.Value.Priority } } Write-ALbuildLog -Level Verbose "Translation memory: loaded $($entries.Count) entries from cache." return [PSCustomObject]@{ ContextAware = [bool]$cached.ContextAware; Count = $entries.Count; Entries = $entries } } catch { Write-ALbuildLog -Level Verbose "Translation memory: cache read failed ($($_.Exception.Message)); rebuilding." } } # Build: add each reference unit under its plain (and, if enabled, context) key; higher priority wins. $entries = @{} $addKey = { param([string] $key, [string] $target, [string] $origin, [int] $priority) if ([string]::IsNullOrEmpty($key) -or [string]::IsNullOrEmpty($target)) { return } if ($entries.ContainsKey($key)) { if ($priority -le $entries[$key].Priority) { return } # keep the higher (earlier on tie) priority } $entries[$key] = [PSCustomObject]@{ Target = $target; Origin = $origin; Priority = $priority } } foreach ($rf in $refFiles) { [xml] $doc = $null try { $doc = Get-Content -LiteralPath $rf.Path -Raw -Encoding UTF8 } catch { Write-ALbuildLog -Level Warning "Translation memory: cannot read '$($rf.Path)': $($_.Exception.Message)."; continue } foreach ($u in (Get-BcXliffUnit -Document $doc)) { if ([string]::IsNullOrEmpty($u.Source) -or [string]::IsNullOrEmpty($u.Target)) { continue } if ($u.TargetState -in @('needs-translation', 'needs-adaptation')) { continue } # only completed translations if ($ContextAware -and $u.GenNote) { & $addKey "$($u.GenNote)|$($u.Source)" $u.Target $rf.Origin $rf.Priority } & $addKey "$($u.Source)" $u.Target $rf.Origin $rf.Priority } } if (-not $NoCache) { try { if (-not (Test-Path -LiteralPath $CacheDir)) { New-Item -ItemType Directory -Force -Path $CacheDir | Out-Null } $dump = [ordered]@{ ContextAware = $ContextAware; Entries = [ordered]@{} } foreach ($k in $entries.Keys) { $dump.Entries[$k] = $entries[$k] } ($dump | ConvertTo-Json -Depth 5 -Compress) | Set-Content -LiteralPath $cacheFile -Encoding UTF8 } catch { Write-ALbuildLog -Level Verbose "Translation memory: cache write failed ($($_.Exception.Message))." } } Write-ALbuildLog -Level Verbose "Translation memory: built $($entries.Count) entries from $($refFiles.Count) reference file(s)." return [PSCustomObject]@{ ContextAware = [bool]$ContextAware; Count = $entries.Count; Entries = $entries } } |