functions/Set-SccLabelLocalization.ps1

function Set-SccLabelLocalization
{
<#
    .SYNOPSIS
        Updates localization of Labels.
     
    .DESCRIPTION
        Updates localization of Labels.
     
    .PARAMETER Name
        The (system) name of the label.
     
    .PARAMETER FriendlyName
        The friendly name of the label.
        This is the DisplayName in case of a top level Label.
        In case of a child label, it is <ParentDisplayName>\<DisplayName>
     
    .PARAMETER Identity
        The Identity - or FQLN - of a Label is similar to the FriendlyName, only using the Name properties instead.
        Thus it is the Name in case of a top level Label.
        In case of a child label, it is <ParentName>\<Name>
     
    .PARAMETER Language
        The language for which to update text.
     
    .PARAMETER Type
        The type of text to write:
        DisplayName or Tooltip.
     
    .PARAMETER Text
        The text to write.
     
    .PARAMETER Default
        By default, existing localization entries will be overwritten.
        With this parameter, already existing localization entries will be honored and only new strings added.
     
    .PARAMETER DelayWrite
        Defers updating labels until the end, collecting all changes and applies them in bulk.
        By default, each label text is written as it comes in.
        Caching all writes and executing them in bulk is a performance update, but risks changes to be lost in case of terminating errors.
     
    .PARAMETER NameMapping
        A hashtable mapping input Names to new names.
        Useful when importing into a tenant other than the source tenant where not all names match perfectly.
     
    .PARAMETER FriendlyNameMapping
        A hashtable mapping input FriendlyNames to new FriendlyNames.
        Useful when importing into a tenant other than the source tenant where not all names match perfectly.
     
    .PARAMETER IdentityMapping
        A hashtable mapping input Identities to new Identities.
        Useful when importing into a tenant other than the source tenant where not all names match perfectly.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Set-SccLabelLocalization -Name Confidential -Language 'de-DE' -Type DisplayName -Text 'Vertraulich'
     
        Adds a German localization to the "Confidential" label
     
    .EXAMPLE
        PS C:\> Import-Csv .\localizations.csv | Set-SccLabelLocalization
     
        Imports all localization data from the localizations.csv document.
        The document must contain some columns:
        - Language
        - Type
        - Text
        - At least one of: Name, FriendlyName, Identity, FQLN
     
    .EXAMPLE
        PS C:\> Import-SccLabelLocalizationXml -Path .\*.xml | Set-SccLabelLocalization
     
        Imports all localization XML in the current folder and applies the localization data to the online labels in SCC.
#>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,
        
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $FriendlyName,
        
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('FQLN')]
        [string]
        $Identity,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Language,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateSet('DisplayName', 'Tooltip')]
        [string]
        $Type,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Text,
        
        [switch]
        $Default,
        
        [switch]
        $DelayWrite,
        
        [Hashtable]
        $NameMapping,
        
        [hashtable]
        $FriendlyNameMapping,
        
        [hashtable]
        $IdentityMapping,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        Assert-SccConnection -Cmdlet $PSCmdlet
        
        #region Utility Functions
        function Write-Label
        {
            [CmdletBinding()]
            param (
                $LabelObject
            )

            # Due to a change in service processing, we must only upload if all languages in both sets are filled out
            if ($LabelObject.LS.DisplayName.Keys.Count -and $LabelObject.LS.Tooltip.Keys.Count)
            {
                foreach ($key in $LabelObject.LS.DisplayName.Keys) {
                    if ($key -in $LabelObject.LS.Tooltip.Keys) { continue }
                    $LabelObject.LS.Tooltip[$key] = $LabelObject.LS.Tooltip['default']
                }
                foreach ($key in $LabelObject.LS.Tooltip.Keys) {
                    if ($key -in $LabelObject.LS.DisplayName.Keys) { continue }
                    $LabelObject.LS.DisplayName[$key] = $LabelObject.LS.DisplayName['default']
                }
            }
            
            $dnHash = @{
                localeKey = "displayName"
                Settings  = @()
            }
            foreach ($key in $LabelObject.LS.DisplayName.Keys)
            {
                $dnHash.Settings += [pscustomobject]@{
                    Key   = $key
                    Value = $LabelObject.LS.DisplayName[$key]
                }
            }
            
            $ttHash = @{
                localeKey = "tooltip"
                Settings  = @()
            }
            foreach ($key in $LabelObject.LS.Tooltip.Keys)
            {
                $ttHash.Settings += [pscustomobject]@{
                    Key   = $key
                    Value = $LabelObject.LS.Tooltip[$key]
                }
            }
            
            try
            {
                if ($dnHash.Settings.Count -and $ttHash.Settings.Count) {
                    Set-Label -Identity $LabelObject.Guid -LocaleSettings ($dnHash | ConvertTo-Json), ($ttHash | ConvertTo-Json) -ErrorAction Stop -WarningAction SilentlyContinue
                }
                elseif ($dnHash.Settings.Count -gt 0) { Set-Label -Identity $LabelObject.Guid -LocaleSettings ($dnHash | ConvertTo-Json) -ErrorAction Stop -WarningAction SilentlyContinue }
                elseif ($ttHash.Settings.Count -gt 0) { Set-Label -Identity $LabelObject.Guid -LocaleSettings ($ttHash | ConvertTo-Json) -ErrorAction Stop -WarningAction SilentlyContinue }
            }
            catch { throw }
        }
        #endregion Utility Functions
        
        $allLabels = Get-EnrichedLabel
        $modifiedLabels = @{ }
    }
    process
    {
        :main foreach ($dummyVar in 1)
        {
            $targetItem = [PSCustomObject]($PSBoundParameters | ConvertTo-PSFHashtable)
            
            #region Find Target Label
            if (Test-PSFParameterBinding -ParameterName Name, FriendlyName, Identity -Not)
            {
                Stop-PSFFunction -String 'Set-SccLabelLocalization.Label.NoIdentity.Error' -EnableException $EnableException -Category InvalidArgument -Continue -Cmdlet $PSCmdlet -Target $targetItem
            }
            
            $targetLabel = $null
            if ($Name)
            {
                $resolvedName = $Name
                if ($NameMapping -and $NameMapping[$Name]) { $resolvedName = $NameMapping[$Name] }
                $targetLabel = $allLabels | Where-Object Name -EQ $resolvedName
            }
            if ($FriendlyName -and -not $targetLabel)
            {
                $resolvedFriendlyName = $FriendlyName
                if ($FriendlyNameMapping -and $FriendlyNameMapping[$FriendlyName]) { $resolvedFriendlyName = $FriendlyNameMapping[$FriendlyName] }
                $targetLabel = $allLabels | Where-Object FriendlyName -EQ $resolvedFriendlyName
            }
            if ($Identity -and -not $targetLabel)
            {
                $resolvedIdentity = $Identity
                if ($IdentityMapping -and $IdentityMapping[$Identity]) { $resolvedIdentity = $IdentityMapping[$Identity] }
                $targetLabel = $allLabels | Where-Object FQLN -EQ $resolvedIdentity
            }
            
            if (-not $targetLabel)
            {
                Stop-PSFFunction -String 'Set-SccLabelLocalization.Label.NotFound.Error' -StringValues $Name, $FriendlyName, $Identity -EnableException $EnableException -Category ObjectNotFound -Continue -Cmdlet $PSCmdlet -Target $targetItem
            }
            #endregion Find Target Label
            
            #region Validate Text
            switch ($Type)
            {
                'DisplayName' {
                    if ($Text.Length -gt 64)
                    {
                        Stop-PSFFunction -String 'Set-SccLabelLocalization.Text.DisplayName.TooLong' -StringValues $targetLabel.FriendlyName, $Language, $Text -EnableException $EnableException -Category InvalidArgument -Continue -ContinueLabel main -Cmdlet $PSCmdlet -Target $targetItem
                    }
                    if ($Text -match $script:PatternDisplayNameValidation)
                    {
                        $characters = $Text | Select-String "($script:PatternDisplayNameValidation)" -AllMatches | ForEach-Object {
                            @($_.Matches).ForEach{ $_.Groups[1].Value }
                        } | Select-Object -Unique | ForEach-Object { '"{0} (C: {1})"' -f $_, ([int][char]$_) }
                        Stop-PSFFunction -String 'Set-SccLabelLocalization.Text.DisplayName.BadCharacters' -StringValues $targetLabel.FriendlyName, $Language, ($characters -join ","), $Text -EnableException $EnableException -Category InvalidArgument -Continue -ContinueLabel main -Cmdlet $PSCmdlet -Target $targetItem
                    }
                }
                'Tooltip' {
                    if ($Text.Length -gt 1000)
                    {
                        Stop-PSFFunction -String 'Set-SccLabelLocalization.Text.Tooltip.TooLong' -StringValues $targetLabel.FriendlyName, $Language, $Text -EnableException $EnableException -Category InvalidArgument -Continue -ContinueLabel main -Cmdlet $PSCmdlet -Target $targetItem
                    }
                    if ($Text -match $script:PatternTooltipValidation)
                    {
                        $characters = $Text | Select-String "($script:PatternTooltipValidation)" -AllMatches | ForEach-Object {
                            @($_.Matches).ForEach{ $_.Groups[1].Value }
                        } | Select-Object -Unique | ForEach-Object { '"{0} (C: {1})"' -f $_, ([int][char]$_) }
                        Stop-PSFFunction -String 'Set-SccLabelLocalization.Text.Tooltip.BadCharacters' -StringValues $targetLabel.FriendlyName, $Language, ($characters -join ","), $Text -EnableException $EnableException -Category InvalidArgument -Continue -ContinueLabel main -Cmdlet $PSCmdlet -Target $targetItem
                    }
                }
            }
            
            Write-PSFMessage -String 'Set-SccLabelLocalization.Processing' -StringValues $targetLabel.FriendlyName -Target $targetItem
            #endregion Validate Text
            
            #region Process Updates
            if ($Default -and $targetLabel.LS."$Type".$Language)
            {
                Write-PSFMessage -String 'Set-SccLabelLocalization.Skipping.AlreadySet' -StringValues $targetLabel.FriendlyName, $Type, $Language -Target $targetItem
                continue
            }
            
            Invoke-PSFProtectedCommand -ActionString 'Set-SccLabelLocalization.Updating' -ActionStringValues $targetLabel.FriendlyName, $Type, $Language -ScriptBlock {
                $backupHash = $targetLabel.LS["$Type"].Clone()
                $targetLabel.LS["$Type"][$Language] = $Text
                if ($DelayWrite) { $modifiedLabels[$targetLabel.Name] = $targetLabel }
                else
                {
                    try { Write-Label -LabelObject $targetLabel -ErrorAction Stop }
                    catch
                    {
                        # Rollback the change that failed
                        $targetLabel.LS["$Type"] = $backupHash
                        throw
                    }
                }
            } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue -Target $targetItem
            #endregion Process Updates
        }
    }
    end
    {
        #region Execute delayed write
        if ($DelayWrite)
        {
            foreach ($labelObject in $modifiedLabels.Values)
            {
                try
                {
                    Write-PSFMessage -String 'Set-SccLabelLocalization.Updating.Bulk' -StringValues $labelObject.FriendlyName -Target $labelObject
                    Write-Label -LabelObject $labelObject -ErrorAction Stop
                }
                catch { Stop-PSFFunction -String 'Set-SccLabelLocalization.Updating.Bulk.Failed' -StringValues $labelObject.FriendlyName -Target $labelObject -ErrorRecord $_ -EnableException $EnableException -Continue }
            }
        }
        #endregion Execute delayed write
    }
}