public/Build-SfGlobalValueSetTranslationMetadata.ps1

function Build-SfGlobalValueSetTranslationMetadata {

    <#
        .SYNOPSIS
        Builds the *.globalValueSetTranslation-meta.xml files from the metadata model.

        .DESCRIPTION
        Processes the global value set translations as given in table [_GlobalValueSetTranslationss] in the metadata model.
        The picklist label translations will be collected from table [_PicklistValues] in the metadata model.

        Manifest accepts bare GlobalValueSet names (e.g. 'MyValueSet'), fully qualified
        'GlobalValueSet:Name' entries, and fully qualified 'GlobalValueSetTranslation:Name-Locale'
        entries (e.g. 'GlobalValueSetTranslation:MyValueSet-de'), and can be piped in. Entries of
        any other Type (e.g. from a combined manifest list shared across multiple
        Build-Sf*Metadata functions) are silently ignored.

        Each entry is parsed via ConvertTo-SfMetadataComponent with DefaultComponentType
        'GlobalValueSet', so a bare name is treated as a GlobalValueSet, not a translation.
        A 'GlobalValueSet' entry whose Name does not exist in table [_GlobalValueSets] produces
        a warning and is skipped. A 'GlobalValueSetTranslation' entry's Name is split on the last
        '-' into <ParentName> and <Locale>; if <ParentName> does not exist in table
        [_GlobalValueSets], a warning is produced and the entry is skipped.

        If Manifest is left empty (no pipeline input and no explicit value), all global value
        sets in the metadata model are built.

        For every GlobalValueSet to be built (via manifest or 'build all'), one translation is
        generated per language code in LanguageCodes. A
        'GlobalValueSetTranslation:Name-Locale' manifest entry additionally requests that exact
        <ParentName>/<Locale> combination directly, regardless of LanguageCodes - this
        lets a translation be requested independently of its parent GlobalValueSet. The two
        sources are merged into a deduplicated set before building, so the same combination is
        never built twice. An empty LanguageCodes therefore builds nothing unless explicit
        'GlobalValueSetTranslation:Name-Locale' manifest entries are given.

        .EXAMPLE
        # Build all global value set translations, specific languages
        Build-SfGlobalValueSetTranslationMetadata -MetadataModel $Model -LanguageCodes @('de', 'fr')

        .EXAMPLE
        # Build translations for two value sets, specific languages
        Build-SfGlobalValueSetTranslationMetadata -MetadataModel $Model -Manifest @('MyValueSet', 'OtherValueSet') -LanguageCodes @('de', 'fr')

        .EXAMPLE
        # Request one specific translation directly, independent of LanguageCodes
        Build-SfGlobalValueSetTranslationMetadata -MetadataModel $Model -Manifest 'GlobalValueSetTranslation:MyValueSet-de'

        .OUTPUTS
        None. Writes *.globalValueSetTranslation-meta.xml files to disk.

        .LINK
        Build-SfGlobalValueSetMetadata

        .LINK
        Import-SfMetadataModel

        .LINK
        https://codeberg.org/uwe-braig/sf-metadata-toolkit
    #>


    [CmdletBinding()]
    param (
        # The metadata model as imported from *.csv and/or *.xlsx with Import-SfMetadataModel
        [Parameter(Mandatory, Position = 0)]
        [PSCustomObject] $MetadataModel,

        # Manifest entries in 'Type:Name' notation. Accepts 'GlobalValueSet:Name', bare names
        # (treated as GlobalValueSet), and 'GlobalValueSetTranslation:Name-Locale'. Other Types
        # are ignored. If left empty, all global value sets are built. Can be piped in.
        [Parameter(ValueFromPipeline, Position = 1)]
        [string[]] $Manifest,

        # List of language codes, e.g. ('en_US', 'de'). An empty LanguageCodes builds no
        # translations via the parent expansion below, unless explicit
        # 'GlobalValueSetTranslation:Name-Locale' manifest entries request specific ones directly.
        [Parameter()]
        [string[]] $LanguageCodes = @(),

        # Target path for the resulting *.xml files. Default is [./force-app/main/default].
        [Parameter()]
        [string] $TargetPath = './force-app/main/default'
    )

    begin {
        $ErrorActionPreference = 'Stop'
        [bool] $IsDebug   = $DebugPreference   -ne 'SilentlyContinue'
        [bool] $IsVerbose = $VerbosePreference -ne 'SilentlyContinue'

        [int] $ReceivedCount = 0
        [Collections.Generic.List[PSCustomObject]] $ParentsToBuild        = [Collections.Generic.List[PSCustomObject]]::new()
        [Collections.Generic.HashSet[string]]       $ExplicitTranslations = [Collections.Generic.HashSet[string]]::new()

        $GlobalValueSetTranslationPath = Join-Path $TargetPath 'globalValueSetTranslations'

        [hashtable] $GlobalValueSetsByName = @{}
        foreach ($ValueSet in $MetadataModel.Tables._GlobalValueSets) {
            $GlobalValueSetsByName[$ValueSet._Name] = $ValueSet
        }
    }

    process {
        foreach ($RawEntry in $Manifest) {
            $ReceivedCount++

            $Component = ConvertTo-SfMetadataComponent -ComponentName $RawEntry -DefaultComponentType 'GlobalValueSet'

            if ($null -eq $Component) {
                continue
            }

            if ($Component.Type -eq 'GlobalValueSet') {
                if (-not $GlobalValueSetsByName.ContainsKey($Component.Name)) {
                    Write-Warning "[$($Component.Key)] Not found in metadata model."
                    continue
                }
                $ParentsToBuild.Add($GlobalValueSetsByName[$Component.Name])
                continue
            }

            if ($Component.Type -eq 'GlobalValueSetTranslation') {
                [int] $SplitIndex = $Component.Name.LastIndexOf('-')
                if ($SplitIndex -lt 0) {
                    Write-Warning "[$($Component.Key)] Expected '<Name>-<Locale>' notation."
                    continue
                }

                [string] $ParentName = $Component.Name.Substring(0, $SplitIndex)
                [string] $Locale     = $Component.Name.Substring($SplitIndex + 1)

                if (-not $GlobalValueSetsByName.ContainsKey($ParentName)) {
                    Write-Warning "[$($Component.Key)] Parent GlobalValueSet '$ParentName' not found in metadata model."
                    continue
                }

                [void] $ExplicitTranslations.Add("$ParentName-$Locale")
                continue
            }

            # Any other Type belongs to a different Build-Sf*Metadata function - ignore silently.
        }
    }

    end {
        if ($ReceivedCount -eq 0) {
            foreach ($ValueSet in $MetadataModel.Tables._GlobalValueSets) {
                $ParentsToBuild.Add($ValueSet)
            }
        }

        # --- merge parent x language-code expansion with explicit manifest entries, deduplicated
        [Collections.Generic.HashSet[string]] $ComponentsToBuild = [Collections.Generic.HashSet[string]]::new()

        foreach ($ValueSet in $ParentsToBuild) {
            foreach ($LanguageCode in $LanguageCodes) {
                [void] $ComponentsToBuild.Add("$($ValueSet._Name)-$LanguageCode")
            }
        }

        foreach ($CompositeKey in $ExplicitTranslations) {
            [void] $ComponentsToBuild.Add($CompositeKey)
        }

        foreach ($CompositeKey in $ComponentsToBuild) {
            [int] $SplitIndex = $CompositeKey.LastIndexOf('-')
            [string] $ParentName    = $CompositeKey.Substring(0, $SplitIndex)
            [string] $LanguageCode  = $CompositeKey.Substring($SplitIndex + 1)
            $ValueSet = $GlobalValueSetsByName[$ParentName]

            $xmlString = ConvertTo-SfGlobalValueSetTranslationXml -ValueSet $ValueSet -LanguageCode $LanguageCode -PicklistValues $MetadataModel.Tables._PicklistValues
            Set-MetadataXmlFile 'GlobalValueSetTranslation' $CompositeKey '.globalValueSetTranslation-meta.xml' $GlobalValueSetTranslationPath $xmlString
        }
    }
}