PSTranslate.psm1

function New-AzureTranslation
{
<#
    .SYNOPSIS
        Uses Azure Cognitive Services to translate text.
 
    .DESCRIPTION
        Uses Azure Cognitive Services to translate text.
 
    .PARAMETER Value
        The text to translate.
 
    .PARAMETER From
        The language to translate from.
 
    .PARAMETER To
        The language to translate to.
 
    .PARAMETER ApiVersion
        The API version to use.
 
    .EXAMPLE
        PS C:\> $text | New-AzureTranslation -From $From -To $To
 
        Converts the text stored in the $text from one language to another
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Value,

        [Parameter(Mandatory = $true)]
        [CultureInfo]
        $From,

        [Parameter(Mandatory = $true)]
        [CultureInfo]
        $To,

        [Parameter()]
        [String]
        $ApiVersion = '3.0'
    )

    begin
    {
        $azureUri = 'https://api.cognitive.microsofttranslator.com/translate?api-version={0}&from={1}&to={2}' -f $ApiVersion, $From.TwoLetterISOLanguageName, $To.TwoLetterISOLanguageName
        $providerOptions = Get-PSFConfigValue -FullName PSTranslate.Azure.Options
        $key = Get-TranslationApiKey -Provider Azure

        if (-not $key)
        {
            Stop-PSFFunction -String 'New-AzureTranslation.ApiKey.NotFound'
            return
        }

        $header = @{
            'Ocp-Apim-Subscription-Key' = $key
        }

        if ($providerOptions.ExtraHeader)
        {
            $header += $providerOptions.ExtraHeader
        }

        Write-PSFMessage -String 'New-AzureTranslation.Preparing' -StringValues $From.TwoLetterISOLanguageName, $To.TwoLetterISOLanguageName, $($(-join $key[0, 1]) + $('*' * 28) + $(-join $key[-2, -1])), $azureUri

        $requestCollector = New-Object -TypeName System.Collections.Specialized.OrderedDictionary
        $count = 0
        $requestCollector.Add($count, @())
        $currentLength = 0
        $charactersPerSecond = [Math]::Floor($providerOptions.CharacterLimit / $providerOptions.LimitWindow.TotalSeconds)
    }

    process
    {
        if ($requestCollector[$count].Count -gt 999 -or ($currentLength + $Value.Length) -ge $charactersPerSecond)
        {
            $count ++
            $requestCollector.Add($count, @())
            $currentLength = 0
        }

        Write-PSFMessage -String 'New-AzureTranslation.AddingInput' -StringValues $Value, $requestCollector[$count].Count
        $requestCollector[$count] += @{ Text = $Value }
        $currentLength += $Value.Length
    }

    end
    {
        Write-PSFMessage -String 'New-AzureTranslation.ExecutingBatches' -StringValues $requestCollector.Count, $charactersPerSecond
        foreach ($entry in $requestCollector.GetEnumerator())
        {
            $jsonBody = ConvertTo-Json -InputObject $entry.Value
            $(Invoke-RestMethod -Method Post -Uri $azureUri -Body $jsonBody -Headers $header -ContentType application/json).translations.text
            [Threading.Thread]::Sleep(1000) # Having calculated chars/second, we can just wait a second
        }
    }
}


function New-GoogleTranslation
{
<#
    .SYNOPSIS
        Uses Google Translation API to translate text.
 
    .DESCRIPTION
        Uses Google Translation API to translate text.
 
    .PARAMETER Value
        The text to translate.
 
    .PARAMETER From
        The language to translate from.
 
    .PARAMETER To
        The language to translate to.
 
    .PARAMETER ApiVersion
        The API version to use.
 
    .EXAMPLE
        PS C:\> $text | New-GoogleTranslation -From $From -To $To
 
        Converts the text stored in the $text from one language to another
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Value,

        [Parameter(Mandatory = $true)]
        [CultureInfo]
        $From,

        [Parameter(Mandatory = $true)]
        [CultureInfo]
        $To,

        [Parameter()]
        [String]
        $ApiVersion = '2'
    )

    begin
    {
        $googleUri = 'https://translation.googleapis.com/language/translate/v{0}' -f $ApiVersion
        $key = Get-TranslationApiKey -Provider Google

        if (-not $key)
        {
            Stop-PSFFunction -String 'New-GoogleTranslation.ApiKey.NotFound'
            return
        }

        $header = @{
            Authorization = "Bearer $key"
        }

        $body = @{
            source = $From.TwoLetterISOLanguageName
            target = $To.TwoLetterISOLanguageName
            q      = New-Object -TypeName 'System.Collections.Generic.List[string]'
        }

        Write-PSFMessage -String 'New-GoogleTranslation.Preparing' -StringValues $From.TwoLetterISOLanguageName, $To.TwoLetterISOLanguageName, $($(-join $key[0, 1]) + $('*' * 28) + $(-join $key[-2, -1])), $googleUri
    }

    process
    {
        Write-PSFMessage -String 'New-GoogleTranslation.AddingInput' -StringValues $Value
        $body.q.Add($Value)
    }

    end
    {
        Write-PSFMessage -String 'New-GoogleTranslation.ExecutingBatches' -StringValues $body.q.Count
        $jsonBody = ConvertTo-Json -InputObject $body
        (Invoke-RestMethod -Method Post -Uri $googleUri -Headers $header -Body $jsonBody -UseBasicParsing -ContentType 'application/json; charset=utf-8').Content
    }
}


function Add-TranslationApiKey
{
<#
    .SYNOPSIS
        Stores API Keys for the APIs used to translate text.
 
    .DESCRIPTION
        Stores API Keys for the APIs used to translate text.
 
    .PARAMETER ApiKey
        The API Key to store.
 
    .PARAMETER Provider
        The provider for which to store the API Key
 
    .PARAMETER Force
        Overwrite existing API Keys
 
    .PARAMETER Register
        Persist the API key, so it will be remembered in future sessions.
        On Windows, this is written to registry using windows encryption.
        On Linux this is stored in plaintext in file.
 
    .EXAMPLE
        PS C:\> Add-TranslationApiKey -ApiKey 'c01d33ba-5af9-4528-b6be-61cdb94820c1' -Provider 'azure'
 
        Adds an API key to connect to azure with.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $ApiKey,

        [Parameter(Mandatory = $true)]
        [PsfValidateSet(TabCompletion = 'PSTranslate.Provider')]
        [string]
        $Provider,

        [switch]
        $Force,

        [switch]
        $Register
    )

    process
    {
        switch ($Provider)
        {
            'Azure'
            {
                $cred = Get-PSFConfigValue -FullName 'PSTranslate.Azure.ApiKey'
                if ($cred -and -not $Force)
                {
                    Write-PSFMessage -Level Warning -String 'Add-TranslationApiKey.ApiKey.Present' -StringValues $Provider
                    return
                }
                if (Test-PSFPowerShell -OperatingSystem Windows)
                {
                    $secureKey = $ApiKey | ConvertTo-SecureString -AsPlainText -Force
                    $cred = New-Object PSCredential($Provider, $secureKey)
                    Set-PSFConfig -Module PSTranslate -Name Azure.ApiKey -Value $cred
                }
                else { Set-PSFConfig -Module PSTranslate -Name Azure.ApiKey -Value $ApiKey }

                if ($Register)
                {
                    Register-PSFConfig -Module PSTranslate -Name Azure.ApiKey
                }
                break
            }
            default
            {
                Write-PSFMessage -Level Warning -String 'Add-TranslationApiKey.BadProvider' -StringValues $Provider
            }
        }
    }
}


function Get-Translation
{
<#
    .SYNOPSIS
        Translates text from one language to another.
 
    .DESCRIPTION
        Translates text from one language to another.
 
    .PARAMETER Value
        The text to translate.
 
    .PARAMETER From
        THe language to translate the text from.
 
    .PARAMETER To
        The language to translate the text to.
 
    .PARAMETER Provider
        The translation service to use.
 
    .EXAMPLE
        PS C:\> Get-Content .\essay.md | Get-Translation -From en -To de
 
        Translates the content of essay.md from English to German.
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String[]]
        $Value,

        [Parameter(Mandatory = $true)]
        [CultureInfo]
        $From,

        [Parameter(Mandatory = $true)]
        [CultureInfo]
        $To,

        [Parameter()]
        [PsfValidateSet(TabCompletion = 'PSTranslate.Provider')]
        [string]
        $Provider = (Get-PSFConfigValue -FullName 'PSTranslate.DefaultProvider')
    )

    begin
    {
        $pipeCollection = New-Object -TypeName 'System.Collections.Generic.List[string]'
    }

    process
    {
        if ($Value.Count -eq 1)
        {
            $pipeCollection.Add($Value)
        }
        else
        {
            $pipeCollection.AddRange($Value)
        }
    }

    end
    {
        switch ($Provider)
        {
            'Azure' { $pipeCollection | New-AzureTranslation -From $From -To $To }
            default { Write-PSFMessage -Level Warning -String 'Get-Translation.BadProvider' -StringValues $Provider }
        }
    }
}


function Get-TranslationApiKey
{
<#
    .SYNOPSIS
        Returns an API key used for translation services.
     
    .DESCRIPTION
        Returns an API key used for translation services.
     
    .PARAMETER Provider
        The provider to return the API key for.
     
    .EXAMPLE
        PS C:\> Get-TranslationApiKey -Provider 'Azure'
     
        Returns the API key for connectign to azure translation services.
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [PsfValidateSet(TabCompletion = 'PSTranslate.Provider')]
        [string]
        $Provider
    )
    
    process
    {
        switch ($Provider)
        {
            'Azure'
            {
                $cred = Get-PSFConfigValue -FullName 'PSTranslate.Azure.ApiKey'
                if ($cred)
                {
                    if (Test-PSFPowerShell -OperatingSystem Windows) { $cred.GetNetworkCredential().Password }
                    else { $cred }
                }
                break
            }
            default
            {
                Write-PSFMessage -Level Warning -String 'Get-TranslationApiKey.BadProvider' -StringValues $Provider
            }
        }
    }
}


<#
.SYNOPSIS
    Set provider-specific options.
.DESCRIPTION
    Set provider-specific options to use with Invoke-RestMethod. Know options for Azure would
    be the regionality of the Cognitive Services that is passed in the header Ocp-Apim-Subscription-Region
.PARAMETER Provider
    The translation service to use.
.PARAMETER ExtraHeader
    The additional headers to add.
.PARAMETER ExtraQueryParameter
    The query parameters to add.
.PARAMETER ExtraPostBody
    The extra body content to add to a post if post is used.
.PARAMETER Register
    Persist the provider options, so they will be remembered in future sessions.
.PARAMETER CharacterLimit
    How many characters per defined Window will be sent to the API
.PARAMETER LimitWindow
    What Window is used?
.EXAMPLE
    Set-TranslationProviderOption -Provider Azure -ExtraHeader @{'Ocp-Apim-Subscription-Region' = 'westeurope'} -Register
 
    Adds regional header for Azure Cognitive Services
#>

function Set-TranslationProviderOption
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [PsfValidateSet(TabCompletion = 'PSTranslate.Provider')]
        [string]
        $Provider = (Get-PSFConfigValue -FullName 'PSTranslate.DefaultProvider'),

        [hashtable]
        $ExtraHeader,

        [hashtable]
        $ExtraQueryParameter,

        [hashtable]
        $ExtraPostBody,

        [uint64]
        $CharacterLimit = 33000, # Using Azure defaults...

        [timespan]
        $LimitWindow = '00:01:00',

        [switch]
        $Register
    )

    $config = Set-PSFConfig -Module PSTranslate -Name "$Provider.Options" -Value @{
        ExtraHeader         = $ExtraHeader
        ExtraQueryParameter = $ExtraQueryParameter
        ExtraPostBody       = $ExtraPostBody
        CharacterLimit      = $CharacterLimit
        LimitWindow         = $LimitWindow
    } -PassThru

    if (-not $Register.IsPresent) { return }

    $config | Register-PSFConfig
}

<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

$root = Get-Item -Path $PSScriptRoot
if ($root.BaseName -eq 'scripts') { $root = $root.Parent.Parent}
Import-PSFLocalizedString -Path (Join-Path -Path $root.FullName -ChildPath /en-us/*.psd1) -Module 'PSTranslate' -Language 'en-US'

Register-PSFTeppScriptblock -Name "PSTranslate.Provider" -ScriptBlock {
    'Azure', 'Google'
}

Register-PSFTeppScriptblock -Name 'PSTranslate.Culture' -ScriptBlock {
    [cultureinfo]::GetCultures('AllCultures').Name
}


Register-PSFTeppArgumentCompleter -Command Get-TranslationApiKey -Parameter Provider -Name PSTranslate.Provider
Register-PSFTeppArgumentCompleter -Command Add-TranslationApiKey -Parameter Provider -Name PSTranslate.Provider
Register-PSFTeppArgumentCompleter -Command Get-Translation -Parameter Provider -Name PSTranslate.Provider
Register-PSFTeppArgumentCompleter -Command Get-Translation -Parameter To,From -Name PSTranslate.Culture

# General
Set-PSFConfig -Module 'PSTranslate' -Name 'DefaultProvider' -Value 'Azure' -Initialize -Validation String -Description 'The default service to contact for translation services.'

# Azure Cognitive Services
if (Test-PSFPowerShell -OperatingSystem Windows)
{
    Set-PSFConfig -Module 'PSTranslate' -Name 'Azure.ApiKey' -Value $null -Initialize -Validation credential -Description 'The API key used to connect to the Azure Cognitive Translation Services'
}
else { Set-PSFConfig -Module 'PSTranslate' -Name 'Azure.ApiKey' -Value $null -Initialize -Validation string -Description 'The API key used to connect to the Azure Cognitive Translation Services' }

# Google Translation API
if (Test-PSFPowerShell -OperatingSystem Windows)
{
    Set-PSFConfig -Module 'PSTranslate' -Name 'Google.ApiKey' -Value $null -Initialize -Validation credential -Description 'The API key used to connect to the Azure Cognitive Translation Services'
}
else { Set-PSFConfig -Module 'PSTranslate' -Name 'Google.ApiKey' -Value $null -Initialize -Validation string -Description 'The API key used to connect to the Azure Cognitive Translation Services' }