Modules/businessdev.ALbuild.Apps/Public/Invoke-BcTranslationSync.ps1
|
function Invoke-BcTranslationSync { <# .SYNOPSIS Syncs a project's target-language XLFs from its generated .g.xlf and fills them from the translation memory (the orchestration behind `albuild translation sync`). .DESCRIPTION For each target language: locates the generated base file (<App>.g.xlf, from the AL compiler's TranslationFile feature) and the target XLF under the project's translations folder, runs Sync-BcTranslation (structural merge), then - unless -NoMemory - builds a prioritised translation memory (Import-BcTranslationMemory) from the configured sources and applies it (Merge-BcTranslationMemory). Deterministic and offline: only exact TM matches are filled; the rest stay needs-translation for a downstream consumer. Sources (priority: self > sibling apps > BC base): - self: the app's own *.<lang>.xlf (highest) - xlf: a path/glob to other apps' XLFs (from albuild.json translation.memory.sources) - bc-base: Microsoft Base/System Application <lang> XLFs, resolved from the BC artifact cache of the project's target application version (best-effort: skipped with a warning when not present in the cache - never triggers a download). With -Check nothing is written: each language is synced into a temp copy and compared to the committed XLF; a difference is reported as drift (for the CI 'check' pipeline mode). .PARAMETER ProjectFolder The AL app folder (contains app.json and a Translations subfolder). .PARAMETER WorkspaceRoot The repo root holding albuild.json (for translation config + memory sources). Defaults to ProjectFolder. .PARAMETER Language Target language(s) (e.g. de-DE). Default: albuild.json translation.targetLanguages, else the languages of the existing *.??-??.xlf files. .PARAMETER BaseFile Override the generated base .g.xlf (default: the single *.g.xlf under the translations folder). .PARAMETER DetectSourceChanges Passed to Sync-BcTranslation (mark changed sources needs-adaptation). Default: $true. .PARAMETER NoMemory Skip the translation-memory fill (structural sync only). .PARAMETER Check Do not write: report whether the committed XLF is out of sync with the .g.xlf (drift). .OUTPUTS PSCustomObject { skipped?, translationsFolder, baseFile, languages: [ { language, targetFile, added, kept, adapted, tm { filled, skipped, byOrigin }, drift? } ] }. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ProjectFolder, [string] $WorkspaceRoot, [string[]] $Language, [string] $BaseFile, [bool] $DetectSourceChanges = $true, [switch] $NoMemory, [switch] $Check ) if (-not (Test-Path -LiteralPath $ProjectFolder)) { throw "Project folder '$ProjectFolder' does not exist." } if (-not $WorkspaceRoot) { $WorkspaceRoot = $ProjectFolder } $cfg = $null try { $cfg = Get-ALbuildProjectConfig -AppFolder $ProjectFolder -WorkspaceRoot $WorkspaceRoot } catch { Write-ALbuildLog -Level Verbose "No project config: $($_.Exception.Message)" } $trans = if ($cfg) { $cfg.Translation } else { $null } $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 } # Translations folder + base .g.xlf. No feature / no generated file => nothing to do (clean skip). $tf = Join-Path $ProjectFolder 'Translations' if (-not (Test-Path -LiteralPath $tf)) { $tf = $ProjectFolder } # NB: wrap the whole if-expression in @() - a bare `$x = if (...) { @(one) }` unwraps a single-element # array back to a scalar, and $x.Count then throws under Set-StrictMode -Version Latest. $gxlf = @(if ($BaseFile) { Get-Item -LiteralPath $BaseFile -ErrorAction SilentlyContinue } else { Get-ChildItem -LiteralPath $tf -Filter '*.g.xlf' -File -ErrorAction SilentlyContinue }) if ($gxlf.Count -eq 0) { Write-ALbuildLog "No generated .g.xlf found under '$tf' - skipping translation sync (no TranslationFile feature or not built yet)." return [PSCustomObject]@{ skipped = 'no-g-xlf'; translationsFolder = $tf; baseFile = $null; languages = @() } } $baseXlf = $gxlf[0].FullName $appName = [regex]::Replace($gxlf[0].Name, '\.g\.xlf$', '') # Target languages: explicit -> config -> discovered from existing *.??-??.xlf. $languages = @($Language) if (-not $languages -or $languages.Count -eq 0) { $languages = @(& $prop $trans 'targetLanguages' @()) } if (-not $languages -or $languages.Count -eq 0) { $languages = @(Get-ChildItem -LiteralPath $tf -Filter '*.xlf' -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '*.g.xlf' -and $_.BaseName -match '\.([a-z]{2}-[A-Z]{2})$' } | ForEach-Object { $matches[1] } | Select-Object -Unique) } if (-not $languages -or $languages.Count -eq 0) { Write-ALbuildLog "No target languages configured or found for '$appName' - skipping translation sync." return [PSCustomObject]@{ skipped = 'no-target-languages'; translationsFolder = $tf; baseFile = $baseXlf; languages = @() } } $memEnabled = -not $NoMemory -and [bool](& $prop (& $prop $trans 'memory' $null) 'enabled' $true) $contextAware = [bool](& $prop (& $prop $trans 'memory' $null) 'contextAware' $true) $results = [System.Collections.Generic.List[object]]::new() foreach ($lang in $languages) { $targetFile = Join-Path $tf "$appName.$lang.xlf" if ($Check) { # Drift = the committed XLF's unit set (id -> source) differs from the .g.xlf's, i.e. strings were # added, removed, or their source changed. Compared semantically (not by raw text) so XML # re-serialisation / target / formatting differences never count as drift. [xml] $baseDoc = Get-Content -LiteralPath $baseXlf -Raw -Encoding UTF8 $baseMap = @{} foreach ($bu in (Get-BcXliffUnit -Document $baseDoc)) { if ($bu.Id) { $baseMap[$bu.Id] = "$($bu.Source)" } } $tgtMap = @{} if (Test-Path -LiteralPath $targetFile) { [xml] $tgtDoc = Get-Content -LiteralPath $targetFile -Raw -Encoding UTF8 foreach ($tu in (Get-BcXliffUnit -Document $tgtDoc)) { if ($tu.Id) { $tgtMap[$tu.Id] = "$($tu.Source)" } } } $drift = $false if ($baseMap.Count -ne $tgtMap.Count) { $drift = $true } else { foreach ($k in $baseMap.Keys) { if (-not $tgtMap.ContainsKey($k) -or $tgtMap[$k] -ne $baseMap[$k]) { $drift = $true; break } } } $results.Add([PSCustomObject]@{ language = $lang; targetFile = $targetFile; drift = $drift }) continue } Sync-BcTranslation -BaseFile $baseXlf -TargetFile $targetFile -DetectSourceChanges $DetectSourceChanges | Out-Null $tm = $null if ($memEnabled) { $refs = @(Resolve-BcTranslationMemorySource -WorkspaceRoot $WorkspaceRoot -Language $lang -TargetFile $targetFile -Config $trans) if ($refs.Count -gt 0) { $idx = Import-BcTranslationMemory -Reference $refs -ContextAware $contextAware $tm = Merge-BcTranslationMemory -Path $targetFile -Index $idx } } $results.Add([PSCustomObject]@{ language = $lang; targetFile = $targetFile; tm = $tm }) } return [PSCustomObject]@{ translationsFolder = $tf; baseFile = $baseXlf; app = $appName; languages = @($results) } } |