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

function Test-BcTranslation {
    <#
    .SYNOPSIS
        Checks AL XLIFF translation files for missing and "needs-work" translations.
 
    .DESCRIPTION
        A CI gate for translations. For each XLIFF file it reports units with a missing translation
        (no/empty target, the configured missing placeholder, or a needs-translation/-adaptation
        state) and units that fail the technical needs-work rules (placeholder mismatch, option
        member count, consecutive spaces, source=target for same-language files). Use -FailOnIssue
        to fail the build when any issue is found.
 
    .PARAMETER Path
        One or more XLIFF files, folders or wildcards. The generated base file (*.g.xlf) is skipped.
 
    .PARAMETER MissingPlaceholder
        Text that marks an untranslated target (e.g. '[NAB: NOT TRANSLATED]'). Default: none.
 
    .PARAMETER SkipNeedsWork
        Only check for missing translations, not the needs-work rules.
 
    .PARAMETER FailOnIssue
        Throw if any issue is found (for CI).
 
    .EXAMPLE
        Test-BcTranslation -Path ./app/Translations -FailOnIssue
 
    .OUTPUTS
        PSCustomObject per issue (File, UnitId, Type, Message).
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [string[]] $Path,
        [string] $MissingPlaceholder = '',
        [switch] $SkipNeedsWork,
        [switch] $FailOnIssue
    )

    begin { $allIssues = [System.Collections.Generic.List[object]]::new() }

    process {
        $files = foreach ($p in $Path) {
            if (Test-Path -LiteralPath $p -PathType Container) { Get-ChildItem -LiteralPath $p -Filter '*.xlf' -Recurse -File }
            elseif (Test-Path -LiteralPath $p) { Get-Item -LiteralPath $p }
            else { Get-ChildItem -Path $p -File -ErrorAction SilentlyContinue }
        }

        foreach ($file in ($files | Where-Object { $_.Name -notlike '*.g.xlf' })) {
            [xml] $doc = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8
            $fileNode = $doc.SelectSingleNode("//*[local-name()='file']")
            $sourceLang = ''
            $targetLang = ''
            if ($fileNode -and $fileNode.Attributes) {
                foreach ($a in $fileNode.Attributes) {
                    if ($a.Name -ieq 'source-language' -or $a.Name -ieq 'srcLang') { $sourceLang = $a.Value }
                    if ($a.Name -ieq 'target-language' -or $a.Name -ieq 'trgLang') { $targetLang = $a.Value }
                }
            }
            $sameLanguage = $sourceLang -and $targetLang -and ($sourceLang -ieq $targetLang)

            foreach ($unit in (Get-BcXliffUnit -Document $doc)) {
                $missingTarget = [string]::IsNullOrWhiteSpace($unit.Target)
                $isPlaceholder = $MissingPlaceholder -and ($unit.Target -eq $MissingPlaceholder)
                $needsState = $unit.TargetState -in @('needs-translation', 'needs-adaptation')
                $isMissing = $missingTarget -or $isPlaceholder -or $needsState
                if ($isMissing) {
                    $allIssues.Add([PSCustomObject]@{ File = $file.Name; UnitId = $unit.Id; Type = 'Missing'; Message = 'Missing or incomplete translation.' })
                    continue
                }
                if (-not $SkipNeedsWork) {
                    foreach ($problem in (Test-BcXliffNeedsWork -Source $unit.Source -Target $unit.Target -SameLanguage:$sameLanguage)) {
                        $allIssues.Add([PSCustomObject]@{ File = $file.Name; UnitId = $unit.Id; Type = 'NeedsWork'; Message = $problem })
                    }
                }
            }
        }
    }

    end {
        foreach ($issue in $allIssues) { $issue }
        if ($allIssues.Count -gt 0) {
            Write-ALbuildLog -Level Warning "Found $($allIssues.Count) translation issue(s)."
            if ($FailOnIssue) { throw "Translation check failed with $($allIssues.Count) issue(s)." }
        }
        else {
            Write-ALbuildLog -Level Success 'No translation issues found.'
        }
    }
}