Modules/businessdev.ALbuild.Apps/Public/Sync-BcTranslation.ps1

function Sync-BcTranslation {
    <#
    .SYNOPSIS
        Synchronises an AL XLIFF target file from a generated base file (.g.xlf).
 
    .DESCRIPTION
        Merges the translation units of the generated base file into the target file, preserving
        existing translations by matching units using a prioritised, multi-pass strategy:
          1. by id
          2. by Xliff-Generator note + source
          3. by Xliff-Generator note + Developer note
          4. by Xliff-Generator note
          5. by source + Developer note
          6. by source
        New units are added (untranslated); units removed from the base are dropped. When
        -DetectSourceChanges is set (default), a translation whose source text changed is marked
        needs-adaptation. The result is written back to the target file (XLIFF 1.2).
 
    .PARAMETER BaseFile
        The generated base XLIFF file (typically *.g.xlf).
 
    .PARAMETER TargetFile
        The target-language XLIFF file to update (created if missing).
 
    .PARAMETER DetectSourceChanges
        Mark translations needs-adaptation when the source text changed. Default: $true.
 
    .EXAMPLE
        Sync-BcTranslation -BaseFile .\App.g.xlf -TargetFile .\App.de-DE.xlf
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $BaseFile,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $TargetFile,
        [bool] $DetectSourceChanges = $true
    )

    if (-not (Test-Path -LiteralPath $BaseFile)) { throw "Base XLIFF file not found: '$BaseFile'." }

    [xml] $baseDoc = Get-Content -LiteralPath $BaseFile -Raw -Encoding UTF8

    # Build lookup maps from the existing target translations (if any).
    $byId = @{}; $byGenSource = @{}; $byGenDev = @{}; $byGen = @{}; $bySourceDev = @{}; $bySource = @{}
    $targetLanguage = ''
    if (Test-Path -LiteralPath $TargetFile) {
        [xml] $targetDoc = Get-Content -LiteralPath $TargetFile -Raw -Encoding UTF8
        $tFileNode = $targetDoc.SelectSingleNode("//*[local-name()='file']")
        if ($tFileNode -and $tFileNode.Attributes) {
            foreach ($a in $tFileNode.Attributes) { if ($a.Name -ieq 'target-language' -or $a.Name -ieq 'trgLang') { $targetLanguage = $a.Value } }
        }
        foreach ($u in (Get-BcXliffUnit -Document $targetDoc)) {
            if ([string]::IsNullOrEmpty($u.Target)) { continue }
            if ($u.Id) { $byId[$u.Id] = $u }
            if ($u.GenNote -and $u.Source) { $byGenSource["$($u.GenNote)|$($u.Source)"] = $u }
            if ($u.GenNote -and $u.DevNote) { $byGenDev["$($u.GenNote)|$($u.DevNote)"] = $u }
            if ($u.GenNote) { $byGen[$u.GenNote] = $u }
            if ($u.Source -and $u.DevNote) { $bySourceDev["$($u.Source)|$($u.DevNote)"] = $u }
            if ($u.Source) { $bySource[$u.Source] = $u }
        }
    }

    $findOld = {
        param($baseUnit)
        if ($baseUnit.Id -and $byId.ContainsKey($baseUnit.Id)) { return $byId[$baseUnit.Id] }
        if ($baseUnit.GenNote -and $baseUnit.Source -and $byGenSource.ContainsKey("$($baseUnit.GenNote)|$($baseUnit.Source)")) { return $byGenSource["$($baseUnit.GenNote)|$($baseUnit.Source)"] }
        if ($baseUnit.GenNote -and $baseUnit.DevNote -and $byGenDev.ContainsKey("$($baseUnit.GenNote)|$($baseUnit.DevNote)")) { return $byGenDev["$($baseUnit.GenNote)|$($baseUnit.DevNote)"] }
        if ($baseUnit.GenNote -and $byGen.ContainsKey($baseUnit.GenNote)) { return $byGen[$baseUnit.GenNote] }
        if ($baseUnit.Source -and $baseUnit.DevNote -and $bySourceDev.ContainsKey("$($baseUnit.Source)|$($baseUnit.DevNote)")) { return $bySourceDev["$($baseUnit.Source)|$($baseUnit.DevNote)"] }
        if ($baseUnit.Source -and $bySource.ContainsKey($baseUnit.Source)) { return $bySource[$baseUnit.Source] }
        return $null
    }

    # Start from a clone of the base, set the target language, and fill targets.
    $newDoc = $baseDoc.Clone()
    $newFileNode = $newDoc.SelectSingleNode("//*[local-name()='file']")
    if ($targetLanguage -and $newFileNode) {
        $attr = $newFileNode.Attributes | Where-Object { $_.Name -ieq 'target-language' } | Select-Object -First 1
        if ($attr) { $attr.Value = $targetLanguage }
    }

    $added = 0; $kept = 0; $adapted = 0
    foreach ($unit in (Get-BcXliffUnit -Document $newDoc)) {
        $old = & $findOld $unit
        $sourceNode = $unit.SourceNode
        if (-not $sourceNode) { continue }
        $ns = $sourceNode.NamespaceURI

        # Ensure a <target> element exists right after <source>.
        $targetNode = $unit.TargetNode
        if (-not $targetNode) {
            $targetNode = if ($ns) { $newDoc.CreateElement('target', $ns) } else { $newDoc.CreateElement('target') }
            $null = $sourceNode.ParentNode.InsertAfter($targetNode, $sourceNode)
        }

        if ($old) {
            $targetNode.InnerText = $old.Target
            $kept++
            if ($DetectSourceChanges -and $old.Source -and ($old.Source -ne $unit.Source)) {
                $stateAttr = $targetNode.Attributes | Where-Object { $_.Name -ieq 'state' } | Select-Object -First 1
                if (-not $stateAttr) { $stateAttr = $targetNode.SetAttributeNode($newDoc.CreateAttribute('state')) }
                $stateAttr.Value = 'needs-adaptation'
                $adapted++
            }
        }
        else {
            $stateAttr = $targetNode.Attributes | Where-Object { $_.Name -ieq 'state' } | Select-Object -First 1
            if (-not $stateAttr) { $stateAttr = $targetNode.SetAttributeNode($newDoc.CreateAttribute('state')) }
            $stateAttr.Value = 'needs-translation'
            $added++
        }
    }

    if ($PSCmdlet.ShouldProcess($TargetFile, 'Write synchronised XLIFF')) {
        $newDoc.Save($TargetFile)
        Write-ALbuildLog -Level Success "Synchronised '$TargetFile': $kept kept, $added new, $adapted needs-adaptation."
    }
}