Translation/Translate-XlfFile.ps1

function Translate-App {
    Param(
        [Parameter(Mandatory=$false)]
        [string]$TranslationSource,        
        [Parameter(Mandatory=$false)]
        [string]$SourceDir = (Get-Location)
    )

    $EnvJson = ConvertFrom-Json (Get-Content (Join-Path $SourceDir 'environment.json') -Raw)
    if ($TranslationSource -eq '' -or $null -eq $TranslationSource) {
        $TranslationSource = Join-Path $SourceDir $EnvJson.translationSource
    }

    if (!(Test-Path $TranslationSource)) {
        throw 'Could not find translation source file.'
    }

    Write-Host "Using translation source file $TranslationSource"

    foreach ($Translation in $EnvJson.translations) {
        Write-Host ('Translating to {0}-{1}' -f $Translation.language, $Translation.country)
        Translate-XlfFile -SourcePath $TranslationSource -TargetCountry $Translation.country -TargetLanguage $Translation.language
    }
}

function Translate-XlfFile {
    Param(
        [Parameter(Mandatory=$true)]
        [string]$SourcePath,
        [Parameter(Mandatory=$false)]
        [string]$OutputPath,
        [Parameter(Mandatory=$false)]
        [string]$TargetLanguage,
        [Parameter(Mandatory=$false)]
        [string]$TargetCountry
    )

    if ($OutputPath -eq '' -or $null -eq $OutputPath) {
        $OutputPath = (Join-Path (Split-Path $SourcePath -Parent) ($TargetLanguage.ToLower() + "-" + $TargetCountry.ToUpper())) + ".xlf"     
    }

    #create xlf file if it doesn't already exist
    if (!(Test-Path $OutputPath)) {
        Copy-Item $SourcePath $OutputPath
        [xml]$OutputXml = Get-Content $OutputPath
        $TargetLanguageAtt = $OutputXml.CreateAttribute('target-language')
        $TargetLanguageAtt.Value = '{0}-{1}' -f $TargetLanguage.ToLower(), $TargetCountry.ToUpper()
        $OutputXml.xliff.file.Attributes.SetNamedItem($TargetLanguageAtt)
        $OutputXml.Save($OutputPath)
    }

    #add any translation units that are present in the source but not in the output
    Sync-TranslationUnits $SourcePath $OutputPath

    $StringsToTranslate = Get-StringsToTranslate -SourcePath $OutputPath

    if ($null -eq $StringsToTranslate) {
        Write-Host 'Already up to date'
        return
    }
    
    $TranslatedStrings = Translate-Strings -Strings $StringsToTranslate -TargetLanguage $TargetLanguage
    if ($null -ne $TranslatedStrings) {
        Write-TranslatedStrings -OutputPath $OutputPath -StringsToTranslate $TranslatedStrings
    }
}

function Sync-TranslationUnits {
    Param(
        [Parameter(Mandatory=$true)]
        $SourcePath,
        [Parameter(Mandatory=$true)]
        $OutputPath
    )

    [bool]$SaveFile = $false

    [xml]$SourceXml = Get-Content $SourcePath
    [xml]$OutputXml = Get-Content $OutputPath
    [System.Xml.XmlNamespaceManager]$NSMgr = [System.Xml.XmlNameSpaceManager]::new($OutputXml.NameTable)
    $NSMgr.AddNamespace('x',$SourceXml.DocumentElement.NamespaceURI)
    
    #add missing sources to the output file
    foreach ($SourceTUnit in $SourceXml.SelectNodes('/x:xliff/x:file/x:body/x:group/x:trans-unit',$NSMgr)) {
        $OutputTUnit = $OutputXml.SelectSingleNode(("/x:xliff/x:file/x:body/x:group/x:trans-unit[@id='{0}']" -f $SourceTUnit.Attributes.GetNamedItem('id')."#text"),$NSMgr)
        if ($null -eq $OutputTUnit) {
            $OutputXml.xliff.file.body.group.AppendChild($OutputXml.ImportNode($SourceTUnit,$true))
            $SaveFile = $true
        }
        elseif ($OutputTUnit.source -ne $SourceTUnit.source) {
            if(($null -eq $OutputTUnit.source.InnerText) -or (($null -ne $OutputTUnit.source.InnerText) -and ($OutputTUnit.source.InnerText -ne $SourceTUnit.source.InnerText))){
                $OutputTUnit.source = $SourceTUnit.source
                $OutputTUnit.RemoveChild($OutputTUnit.SelectSingleNode('./x:target',$NSMgr))
                $SaveFile = $true
            }
        }
    }

    #remove orphaned sources from the output
    foreach ($OutputTUnit in $OutputXml.SelectNodes('/x:xliff/x:file/x:body/x:group/x:trans-unit',$NSMgr)) {
        $SourceTUnit = $SourceXml.SelectSingleNode(("/x:xliff/x:file/x:body/x:group/x:trans-unit[@id='{0}']" -f $OutputTUnit.Attributes.GetNamedItem('id')."#text"),$NSMgr)
        if ($null -eq $SourceTUnit) {
            $OutputXml.xliff.file.body.group.RemoveChild($OutputTUnit)
            $SaveFile = $true
        }
    }

    if ($SaveFile) {
        $OutputXml.Save($OutputPath)
    }
}

function Get-StringsToTranslate {
    Param(
        [Parameter(Mandatory=$true)]
        [string]$SourcePath
    )

    $StringsToTranslate = @()

    [xml]$SourceXml = Get-Content $SourcePath
    foreach ($TUnit in $SourceXml.xliff.file.body.group.'trans-unit') {
        if ($null -eq $TUnit.target)  {
                $StringToTranslate = New-Object System.Object
                $StringToTranslate | Add-Member -MemberType NoteProperty -Name ID -Value $TUnit.id
                $SourceText = $TUnit.source.InnerText
                if ($null -eq $SourceText){
                    $SourceText = $TUnit.source
                }
                $StringToTranslate | Add-Member -MemberType NoteProperty -Name Source -Value $SourceText
                $StringToTranslate | Add-Member -MemberType NoteProperty -Name Target -Value ''
                $StringsToTranslate += $StringToTranslate
        }
    }

    $StringsToTranslate
}

function Translate-Strings {
    Param(
        [Parameter(Mandatory=$true)]
        $Strings,
        [Parameter(Mandatory=$true)]
        $TargetLanguage
    )

    #don't send English strings for translation
    if ($TargetLanguage -eq 'en') {
        $Translations = $Strings
        $Translations | ForEach-Object {$_.Target = $_.Source}
        return $Translations
    }

    $Strings = Translate-StringsWithDictionary -Strings $Strings -TargetLanguage $TargetLanguage
    $Translations = @()
    $Translations += $Strings | Where-Object Target -ne ''
    $StringsToTranslate = @()
    $StringsToTranslate += ($Strings | Where-Object Target -eq '')
    if ($null -ne $StringsToTranslate) {
        $AzureTranslations = @()
        $AzureTranslations += Translate-StringsWithAzure -StringsToTranslate $StringsToTranslate -TargetLanguage $TargetLanguage
        $Translations += $AzureTranslations
    }

    $Translations
}

function Translate-StringsWithDictionary {
    Param(
        [Parameter(Mandatory=$true)]
        $Strings,
        [Parameter(Mandatory=$true)]
        $TargetLanguage
    )
    
    if (($null -eq (Get-TFSConfigKeyValue 'translationDictionaryPath')) -or ('' -eq (Get-TFSConfigKeyValue 'translationDictionaryPath'))) {
        return $Strings
    }

    $LanguageID = Get-LanguageIDFromCode $TargetLanguage
    if ($null -eq $LanguageID) {
        return $Strings
    }

    Write-Host ("Searching for language $LanguageID captions in dictionary {0}" -f (Get-TFSConfigKeyValue -KeyName 'translationDictionaryPath'))

    $TranslatedStrings = @()
    if($null -eq $Script:Dictionary){
        [xml]$Script:Dictionary = Get-Content (Get-TFSConfigKeyValue 'translationDictionaryPath') -Encoding UTF8
    }
    
    foreach ($String in $Strings) {
        $PhraseNode = $Script:Dictionary.SelectSingleNode(('/dictionary/phrase[A1033 = "{0}"]' -f $String.Source))
        if ($null -ne $PhraseNode) {
            $LanguageNode = $PhraseNode.SelectSingleNode("./$LanguageID")
            if ($null -ne $LanguageNode) {
                $String.Target = $LanguageNode.InnerText
            }
        }
        $TranslatedStrings += $String
    }

    $TranslatedStrings
}

function Translate-StringsWithAzure {
    Param(
        [Parameter(Mandatory=$true)]
        $StringsToTranslate,
        [Parameter(Mandatory=$true)]
        $TargetLanguage,
        [Parameter(Mandatory=$false)]
        [int]$RequestSize = 100
    )

    $Key = Get-TFSConfigKeyValue 'translationkey'
    if (($null -eq $Key) -or ('' -eq $Key)) {
        return
    }

    $Headers = @{'Ocp-Apim-Subscription-Key'=$Key}
    $Headers.Add('Content-Type','application/json')
    Write-Host "Sending strings for translation to Azure"

    #split into subsets of request size
    $Counter = @{ Value = 0 }
    $StringsToTranslateSet = $StringsToTranslate | Group-Object -Property { [math]::Floor($Counter.Value++ / $RequestSize) }

    $TranslatedStrings = @()
    foreach($StringsToTranslateSubset in $StringsToTranslateSet){
        #convert input object into json request
        $Request = '['

        $StringsToTranslate = $StringsToTranslateSubset.Group
        foreach ($StringToTranslate in $StringsToTranslate) {
            $String = $StringToTranslate.Source
            $String = $String.Replace('\','\\')
            $String = $String.Replace('"','\"')
            $Request += '{Text:"' + $String + '"},'
        }

        $Request = $Request.Substring(0,$Request.Length - 1)
        $Request += ']'

        $Response = Invoke-WebRequest ('https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to={0}' -f $TargetLanguage) -Headers $Headers -Method Post -Body $Request

        $Translations = @()

        if ($null -ne $Response) {
            (ConvertFrom-Json $Response.Content).translations | % {
                $Text = $_.text
                $Text = $Text.Replace('% 10',' %10')
                $Text = $Text.Replace('% 1',' %1')
                $Text = $Text.Replace('% 2',' %2')
                $Text = $Text.Replace('% 3',' %3')
                $Text = $Text.Replace('% 4',' %4')
                $Text = $Text.Replace('% 5',' %5')
                $Text = $Text.Replace('% 6',' %6')
                $Text = $Text.Replace('% 7',' %7')
                $Text = $Text.Replace('% 8',' %8')
                $Text = $Text.Replace('% 9',' %9')
                $Translations += $Text
            }
        }

        if ($StringsToTranslate.Count -eq 1) {
            $TranslatedString += $StringsToTranslate.Item(0)
            $TranslatedString.Target = $Translations
            $TranslatedStrings += $TranslatedString
        }
        else {
            $Element = 0  
            foreach ($Translation in $Translations) {
                $TranslatedString = $StringsToTranslate.Item($Element)
                $TranslatedString.Target = $Translation
                $TranslatedStrings += $TranslatedString
                $Element += 1
            }
        }
    }

    $TranslatedStrings
}

function Get-LanguageIDFromCode {
    Param(
        # the langugae code
        [Parameter(Mandatory)]
        [string]
        $LanguageCode
    )

    switch ($LanguageCode) {
        'en' {return 'A1033'} #(US) English
        'de' {return 'A1031'} #German
        'fr' {return 'A1036'} #French
        'nl' {return 'A1043'} #Dutch
        'es' {return 'A1034'} #Spanish
        'it' {return 'A1040'} #Italian
        'da' {return 'A1030'} #Danish
        'se' {return 'A1053'} #Swedish
        'fi' {return 'A1035'} #Finish
        'is' {return 'A1039'} #Icelandic
        'no' {return 'A1044'} #Norwegian
    }
}

function Write-TranslatedStrings {
    Param(
        [Parameter(Mandatory=$true)]
        [string]$OutputPath,
        [Parameter(Mandatory=$true)]
        $StringsToTranslate        
    )

    Write-Host ('Updating {0}' -f [System.IO.Path]::GetFileName($OutputPath))

    [xml]$OutputXml = Get-Content $OutputPath -Encoding UTF8
    foreach ($StringToTranslate in $StringsToTranslate) {
        $TUnit = $OutputXml.xliff.file.body.group.'trans-unit' | ? ID -eq $StringToTranslate.ID
        $TargetNode = $OutputXml.CreateElement('target',$OutputXml.DocumentElement.NamespaceURI)
        $TargetNode.InnerText = $StringToTranslate.Target
        $SourceNode = $TUnit.FirstChild.NextSibling
        $TUnit.InsertAfter($TargetNode,$SourceNode) | Out-Null
        $ElementNo++
    }

    $OutputXml.Save($OutputPath)
}

Export-ModuleMember -Function Translate-XlfFile
Export-ModuleMember -Function Translate-App
Export-ModuleMember -Function Get-StringsToTranslate
Export-ModuleMember -Function Sync-TranslationUnits