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> } } } } #> } |