LocalizedCompletion.psm1

$script:ModuleRoot = $PSScriptRoot

function ConvertTo-LocalizedCompletion {
    <#
    .SYNOPSIS
        Converts a completion result into a localized version of itself.
     
    .DESCRIPTION
        Converts a completion result into a localized version of itself.
        Use Register-LCLocalization to provide localization values.
 
        For internal use and troubleshooting.
     
    .PARAMETER Completion
        The completion result provided by the PowerShell engine.
     
    .PARAMETER Code
        The line of code that is being completed for.
     
    .PARAMETER Offset
        Where in the line of code the cursor is.
     
    .EXAMPLE
        PS C:\> ConvertTo-LocalizedCompletion -Completion $ompletion -Code $code -Offset $offset
 
        Converts a completion result into a localized version of itself.
    #>

    [OutputType([System.Management.Automation.CommandCompletion])]
    [CmdletBinding()]
    param (
        [System.Management.Automation.CommandCompletion]
        $Completion,

        [string]
        $Code,

        [int]
        $Offset
    )
    process {
        $type = Resolve-LCCompletionCommand -Code $Code -Position $Offset
        if ($type.Type -eq 'unknown') { return $Completion }

        $selector = [LocalizedCompletion.Selector]::new($script:language,$script:defaultLanguage)

        if ($type.Type -eq 'ParameterCompletion') {
            if (-not $type.IsParameterCompletion) { return $Completion }
            if (-not $script:localization[$type.CommandName]) { return $Completion }

            $parameterHash = $script:localization[$type.CommandName].Parameter
            if ($parameterHash.Count -lt 1) { return $Completion }

            $allItems = $($Completion.CompletionMatches)
            $Completion.CompletionMatches.Clear()

            foreach ($item in $allItems) {
                # Skip Irrelevant
                if ($item.ResultType -ne 'ParameterName') {
                    $Completion.CompletionMatches.Add($item)
                    continue
                }

                $itemName = $item.CompletionText.TrimStart('-')
                if ($parameterHash.Keys -notcontains $itemName) {
                    $Completion.CompletionMatches.Add($item)
                    continue
                }

                $Completion.CompletionMatches.Add(
                    [System.Management.Automation.CompletionResult]::new(
                        $selector.SelectParameter($item.CompletionText, $parameterHash.$itemName.Alias),
                        $selector.Select($item.ListItemText, $parameterHash.$itemName.ListItem),
                        'ParameterName',
                        $selector.Select($item.ToolTip, $parameterHash.$itemName.ToolTip)
                    )
                )
            }

            return $Completion
        }

        if ($type.Type -eq 'CommandCompletion') {
            $allItems = $($Completion.CompletionMatches)
            $Completion.CompletionMatches.Clear()
            foreach ($item in $allItems) {
                if ($item.CompletionText -notin $script:localization.Keys) {
                    $Completion.CompletionMatches.Add($item)
                    continue
                }

                $commandHash = $script:localization[$item.CompletionText]
                $Completion.CompletionMatches.Add(
                    [System.Management.Automation.CompletionResult]::new(
                        $selector.Select($item.CompletionText, $commandHash.Alias),
                        $selector.Select($item.ListItemText, $commandHash.ListItem),
                        'ParameterName',
                        $selector.Select($item.ToolTip, $commandHash.Tooltip)
                    )
                )
            }

            return $Completion
        }

        $Completion
    }
}

function Disable-LocalizedCompletion {
    <#
    .SYNOPSIS
        Disables the localized tab completion, restoring the default behavior.
     
    .DESCRIPTION
        Disables the localized tab completion, restoring the default behavior.
        Should end any completion-related bugs.
     
    .EXAMPLE
        PS C:\> Disable-LocalizedCompletion
 
        Disables the localized tab completion, restoring the default behavior.
    #>

    [CmdletBinding()]
    param ()
    process {
        & "$script:ModuleRoot\internal\expander\TabExpansion2.ps1"
    }
}

function Enable-LocalizedCompletion {
    <#
    .SYNOPSIS
        Enables the localized tab completion.
     
    .DESCRIPTION
        Enables the localized tab completion.
        Use Register-LCLocalization to provide localized completion.
        Use Set-LCLanguage to define the language used for completion.
     
    .EXAMPLE
        PS C:\> Enable-LocalizedCompletion
         
        Enables the localized tab completion.
    #>

    [CmdletBinding()]
    param ()
    process {
        & "$script:ModuleRoot\internal\expander\TabExpansion3.ps1"
    }
}

function Register-LCLocalization {
    <#
    .SYNOPSIS
        Registers localization data for tab completion.
     
    .DESCRIPTION
        Registers localization data for tab completion.
     
    .PARAMETER CommandName
        Name of the command to complete.
     
    .PARAMETER Tooltip
        Tooltip that should be shown when completing the command.
     
    .PARAMETER Alias
        Alternative command name that should be completed to.
        No alias will actually be created - be sure the new name actually exists.
     
    .PARAMETER ListItem
        Alternative command name to display in a completion menu.
     
    .PARAMETER ParameterName
        Name of the parameter to localized for completion.
     
    .PARAMETER ParameterAlias
        Alternative name of the parameter to complete to.
        This alias is not actually added to the parameter, so be sure it actually exists on the actual command before assigning it here.
     
    .PARAMETER ParameterListItem
        Alternative parameter name to show during a completion menu.
     
    .PARAMETER ParameterTooltip
        Tooltip for the parameter to show during completion.
     
    .PARAMETER ParameterHash
        A set of parameters to update in bulk.
        Each key is a parameter name, each value a hashtable with the keys Alias, ListItem and Tooltip.
        Each of these three should then contain a hashtable mapping language-code to text.
     
    .PARAMETER LoadHelp
        NOT YET IMPLEMENTED
        Whether the command help should be loaded and cached to the entry.
        This would then be used to provide automatic Tooltip content.
     
    .EXAMPLE
        PS C:\> Register-LCLocalization -CommandName Get-ChildItem -ListItem @{ 'de-de' = 'Lese-Kindobjekte' } -Alias @{ 'de-de' = 'Lese-KindObjekt' } -Tooltip @{ 'de-de' = 'Macht seltsame Dinge' }
 
        Provides localized completion for Get-ChildItem in German
    #>

    [CmdletBinding(DefaultParameterSetName = 'default')]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $CommandName,

        [hashtable]
        $Tooltip,

        [hashtable]
        $Alias,

        [hashtable]
        $ListItem,

        [Parameter(Mandatory = $true, ParameterSetName = 'Parameter')]
        [string]
        $ParameterName,

        [Parameter(ParameterSetName = 'Parameter')]
        [hashtable]
        $ParameterAlias,

        [Parameter(ParameterSetName = 'Parameter')]
        [hashtable]
        $ParameterListItem,

        [Parameter(ParameterSetName = 'Parameter')]
        [hashtable]
        $ParameterTooltip,

        [Parameter(Mandatory = $true, ParameterSetName = 'Hash')]
        [hashtable]
        $ParameterHash,

        [switch]
        $LoadHelp
    )
    begin {
        #region Functions
        function Update-Parameter {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                [hashtable]
                $CommandHash,

                [string]
                $ParameterName,

                [AllowNull()]
                $Alias,
                [AllowNull()]
                $ListItem,
                [AllowNull()]
                $Tooltip
            )

            if (-not $CommandHash.Parameter[$ParameterName]) {
                $CommandHash.Parameter[$ParameterName] = @{
                    Alias = @{}
                    ListItem = @{}
                    Tooltip = @{}
                }
            }

            $parameterHash = $CommandHash.Parameter[$ParameterName]

            $aspects = 'Alias', 'ListItem', 'Tooltip'
            foreach ($aspect in $aspects) {
                if (-not $PSBoundParameters.$aspect) { continue }
                if ($PSBoundParameters.$aspect -isnot [hashtable]) {
                    Write-Warning "Invalid $aspect datatype! Ensure to provide a hashtable for that."
                    continue
                }

                foreach ($pair in $PSBoundParameters.$aspect.GetEnumerator()) {
                    $parameterHash[$aspect][$pair.Key] = $pair.Value
                }
            }
        }
        #endregion Functions
    }
    process {
        #region Main Command
        if (-not $script:localization[$CommandName]) {
            $script:localization[$CommandName] = @{
                Name = $CommandName
                Help = $null
                Tooltip = @{ }
                Alias = @{ }
                ListItem = @{ }
                Parameter = @{ }
            }
        }

        $commandHash = $script:localization[$CommandName]

        $aspects = 'Tooltip', 'Alias', 'ListItem'
        foreach ($aspect in $aspects) {
            if (-not $PSBoundParameters.$aspect) { continue }

            foreach ($pair in $PSBoundParameters.$aspect.GetEnumerator()) {
                $commandHash[$aspect][$pair.Key] = $pair.Value
            }
        }

        if ($LoadHelp -and -not $commandHash.Help) { $commandHash.Help = Get-Help -Name $CommandName -ErrorAction Ignore }
        #endregion Main Command

        #region Single Parameter
        if ($ParameterName) {
            Update-Parameter -CommandHash $commandHash -ParameterName $ParameterName -Alias $ParameterAlias -ListItem $ParameterListItem -Tooltip $ParameterTooltip
        }
        #endregion Single Parameter

        #region Parameter Hash
        if ($ParameterHash) {
            foreach ($pair in $ParameterHash.GetEnumerator()) {
                Update-Parameter -CommandHash $commandHash -ParameterName $pair.Key -Alias $pair.Value.Alias -ListItem $pair.Value.ListItem -Tooltip $pair.Value.Tooltip
            }
        }
        #endregion Parameter Hash
    }
}

function Resolve-LCCompletionCommand {
    <#
    .SYNOPSIS
        Resolves the command being completed for.
     
    .DESCRIPTION
        Resolves the command being completed for.
        Used internally to aid in determining the matching localization to provide.
     
    .PARAMETER Code
        The line of code for which completion has been triggered.
     
    .PARAMETER Position
        The position within the code where the cursor is at.
     
    .EXAMPLE
        PS C:\> Resolve-LCCompletionCommand -Code $code -Position $index
         
        Resolves the command being completed for.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    param (
        [string]
        $Code,

        [int]
        $Position
    )
    process {
        $ast = [System.Management.Automation.Language.Parser]::ParseInput($Code, [ref]$null, [ref]$null)
        $item = $ast.FindAll({
                $args[0].Extent.StartOffSet -le $Position -and $args[0].Extent.EndOffset -ge $Position
            }, $true) | Sort-Object { $_.Extent.StartOffset } -Descending | Select-Object -First 1

        #region Parameter Completion
        if ($item.Parent -is [System.Management.Automation.Language.CommandAst]) {
            $commandName = $item.Parent.CommandElements[0].Value
            $command = $ExecutionContext.InvokeCommand.GetCommand($item.Parent.CommandElements[0].Value, 'Function,Cmdlet,Alias')

            # To ensure precedence is respected
            if ($commandObject = @($command).Where{ $_.CommandType -eq 'Alias' }) {
                $resolvedCommand = $commandObject.ResolvedCommand
                $commandName = $resolvedCommand.Name
            }
            elseif ($commandObject = @($command).Where{ $_.CommandType -eq 'Function' }) {
                $resolvedCommand = $commandObject
                $commandName = $resolvedCommand.Name
            }
            elseif ($commandObject = @($command).Where{ $_.CommandType -eq 'Cmdlet' }) {
                $resolvedCommand = $commandObject
                $commandName = $resolvedCommand.Name
            }

            $isParameterCompletion = (
                $item -is [System.Management.Automation.Language.CommandParameterAst] -or
                (
                    $item -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
                    $item.Extent.Text -match '^-'
                )
            )

            [PSCustomObject]@{
                Type                  = 'ParameterCompletion'
                CommandText           = $item.Parent.CommandElements[0].Value
                Command               = $resolvedCommand
                CommandName           = $commandName
                CompletionAst         = $item
                IsParameterCompletion = $isParameterCompletion
            }

            return
        }
        #endregion Parameter Completion

        #region Command Completion
        # Case: First Command
        if ($item -is [System.Management.Automation.Language.ScriptBlockAst] -and $item.Extent.Text -notmatch '^\$') {
            [PSCustomObject]@{
                Type                  = 'CommandCompletion'
                CommandText           = $item.Extent.Text
                Command               = $null
                CommandName           = $null
                CompletionAst         = $item
                IsParameterCompletion = $false
            }
            return
        }

        # Case: Subsequent command in pipeline
        if ($item -is [System.Management.Automation.Language.CommandAst]) {
            [PSCustomObject]@{
                Type                  = 'CommandCompletion'
                CommandText           = $item.CommandElements[0].Value
                Command               = $null
                CommandName           = $null
                CompletionAst         = $item
                IsParameterCompletion = $false
            }
            return
        }
        #endregion Command Completion

        [PSCustomObject]@{
            Type                  = 'Unknown'
            CommandText           = $item
            Command               = $null
            CommandName           = $null
            CompletionAst         = $item
            IsParameterCompletion = $false
        }
    }
}

function Set-LCLanguage {
    <#
    .SYNOPSIS
        Sets the language completed for.
     
    .DESCRIPTION
        Sets the language completed for.
     
    .PARAMETER Language
        The language to use for completion.
        Must be a language code such as "en-us" or "de-de".
     
    .PARAMETER DefaultLanugage
        What language should be used as the default language.
        Must be a language code such as "en-us" or "de-de".
     
    .EXAMPLE
        PS C:\> Set-LCLanguage -Language de-de
 
        Changes the current completion language to German.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [ValidateScript({
            if ($_ -in [System.Globalization.CultureInfo]::GetCultures('AllCultures').Name) { return $true }
            Write-Warning "Invalid language! Must be in a format similar to 'en-US' or 'de-DE'! $_"
            throw "Invalid language! Must be in a format similar to 'en-US' or 'de-DE'! $_"
        })]
        [string]
        $Language,

        [ValidateScript({
            if ($_ -in [System.Globalization.CultureInfo]::GetCultures('AllCultures').Name) { return $true }
            Write-Warning "Invalid language! Must be in a format similar to 'en-US' or 'de-DE'! $_"
            throw "Invalid language! Must be in a format similar to 'en-US' or 'de-DE'! $_"
        })]
        [string]
        $DefaultLanugage
    )
    process {
        if ($Language) { $script:Language = $Language }
        if ($DefaultLanugage) { $script:DefaultLanugage = $DefaultLanugage }
    }
}

$code = @'
using System;
using System.Collections;
 
namespace LocalizedCompletion
{
    public class Selector
    {
        public string Language;
        public string DefaultLanguage;
 
        public Selector(string Language, string DefaultLanguage)
        {
            this.Language = Language;
            this.DefaultLanguage = DefaultLanguage;
        }
 
        public string Select(string Original, Hashtable Localization)
        {
            if (null == Localization)
                return Original;
            if (null != Localization[Language] && !String.IsNullOrEmpty(Localization[Language].ToString()))
                return Localization[Language].ToString();
            if (null != Localization[DefaultLanguage] && !String.IsNullOrEmpty(Localization[DefaultLanguage].ToString()))
                return Localization[DefaultLanguage].ToString();
 
            return Original;
        }
 
        public string SelectParameter(string Original, Hashtable Localization)
        {
            if (null == Localization)
                return Original;
            if (null != Localization[Language] && !String.IsNullOrEmpty(Localization[Language].ToString()))
                return $"-{Localization[Language].ToString().TrimStart('-')}";
            if (null != Localization[DefaultLanguage] && !String.IsNullOrEmpty(Localization[DefaultLanguage].ToString()))
                return $"-{Localization[DefaultLanguage].ToString().TrimStart('-')}";
 
            return Original;
        }
    }
}
'@

try { Add-Type $code -ErrorAction Ignore }
catch { }

# Language to complete to
$script:language = $Host.CurrentUICulture.Name
if (-not $script:language) { $script:language = 'en-us' }
$script:defaultLanguage = 'en-us'

# Registered Localization
$script:localization = @{
<#
<CommandName> = @{
    Help = [help]
    Synopsis = @{ <language> = <override> }
    Alias = @{ <language> = <override> }
    ListItem = @{ <language> = <override> }
    Parameter = @{
        <name> = @{
            Alias = @{ <language> = <override> }
            ListItem = @{ <language> = <override> }
            Tooltip = @{ <language> = <override> }
        }
    }
}
#>

}