Modules/businessdev.ALbuild.Apps/Public/Merge-BcTranslationMemory.ps1

function Merge-BcTranslationMemory {
    <#
    .SYNOPSIS
        Fills untranslated units in a target XLIFF from a translation-memory index (deterministic, exact).
 
    .DESCRIPTION
        For each unit still in a needs-translation / needs-adaptation state (or with an empty target),
        looks up the index built by Import-BcTranslationMemory: first the context-sensitive
        "GenNote|source" key, then the plain "source" key. On an exact match it writes the <target>, sets
        state="translated", and records the origin in an auditable note (<note from-tool='albuild-tm'>
        origin=<self|app:...|bc-base></note>). Units without a match are left untouched (needs-translation)
        so a downstream consumer can translate the true new strings. No fuzzy matching, no LLM.
 
    .PARAMETER Path
        The target-language XLIFF file to fill (written in place unless -WhatIf).
 
    .PARAMETER Index
        The index object returned by Import-BcTranslationMemory.
 
    .OUTPUTS
        PSCustomObject { filled, skipped, byOrigin (hashtable origin -> count), file }.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path,
        [Parameter(Mandatory)] [PSCustomObject] $Index
    )

    if (-not (Test-Path -LiteralPath $Path)) { throw "XLIFF file not found: '$Path'." }
    $entries = $Index.Entries
    $contextAware = [bool]$Index.ContextAware

    [xml] $doc = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
    $filled = 0; $skipped = 0
    $byOrigin = @{}

    foreach ($u in (Get-BcXliffUnit -Document $doc)) {
        $needs = [string]::IsNullOrWhiteSpace($u.Target) -or ($u.TargetState -in @('needs-translation', 'needs-adaptation'))
        if (-not $needs) { continue }
        if ([string]::IsNullOrEmpty($u.Source)) { $skipped++; continue }

        $hit = $null
        if ($contextAware -and $u.GenNote -and $entries.ContainsKey("$($u.GenNote)|$($u.Source)")) {
            $hit = $entries["$($u.GenNote)|$($u.Source)"]
        }
        elseif ($entries.ContainsKey("$($u.Source)")) {
            $hit = $entries["$($u.Source)"]
        }
        if (-not $hit) { $skipped++; continue }

        # Write target + state="translated" on <target>.
        $targetNode = $u.TargetNode
        if (-not $targetNode) {
            $ns = $u.SourceNode.NamespaceURI
            $targetNode = if ($ns) { $doc.CreateElement('target', $ns) } else { $doc.CreateElement('target') }
            $null = $u.SourceNode.ParentNode.InsertAfter($targetNode, $u.SourceNode)
        }
        $targetNode.InnerText = $hit.Target
        $stateAttr = $targetNode.Attributes | Where-Object { $_.Name -ieq 'state' } | Select-Object -First 1
        if (-not $stateAttr) { $stateAttr = $targetNode.SetAttributeNode($doc.CreateAttribute('state')) }
        $stateAttr.Value = 'translated'

        # Auditable origin note (replace any prior albuild-tm note on this unit).
        $ns = $u.UnitNode.NamespaceURI
        foreach ($n in @($u.UnitNode.SelectNodes(".//*[local-name()='note']"))) {
            $fromTool = $null
            if ($n.Attributes) { foreach ($a in $n.Attributes) { if ($a.Name -ieq 'from-tool') { $fromTool = $a.Value } } }
            if ($fromTool -eq 'albuild-tm') { $null = $n.ParentNode.RemoveChild($n) }
        }
        $note = if ($ns) { $doc.CreateElement('note', $ns) } else { $doc.CreateElement('note') }
        $fa = $note.SetAttributeNode($doc.CreateAttribute('from-tool')); $fa.Value = 'albuild-tm'
        $note.InnerText = "origin=$($hit.Origin)"
        $null = $u.UnitNode.AppendChild($note)

        $filled++
        $o = [string]$hit.Origin
        if ($byOrigin.ContainsKey($o)) { $byOrigin[$o]++ } else { $byOrigin[$o] = 1 }
    }

    if ($filled -gt 0 -and $PSCmdlet.ShouldProcess($Path, "Fill $filled unit(s) from translation memory")) {
        $doc.Save($Path)
    }
    Write-ALbuildLog -Level Success "Translation memory: filled $filled, $skipped still need translation in '$(Split-Path -Leaf $Path)'."
    return [PSCustomObject]@{ filled = $filled; skipped = $skipped; byOrigin = $byOrigin; file = (Split-Path -Leaf $Path) }
}