Public/Invoke-DeeplTranslateFile.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
<#PSScriptInfo
 
.VERSION 1.1.0
 
.GUID 93cf5a1d-2793-4645-8937-36f24b2de164
 
.AUTHOR diko@admins-little-helper.de
 
.COMPANYNAME
 
.COPYRIGHT (c) 2022 All rights reserved.
 
.TAGS DeepL Translate Translation
 
.LICENSEURI https://github.com/admins-little-helper/DeeplTranslate/blob/main/LICENSE
 
.PROJECTURI https://github.com/admins-little-helper/DeeplTranslate
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
    1.0.0
    Initial release
 
    1.1.0
    Updated way to get DeepL Api Uri and Http Status codes.
 
#>



<#
 
.DESCRIPTION
    Contains a function to translate a file using the DeepL API.
    More information about the DeepL API can be found here: https://www.deepl.com/de/docs-api/introduction/.
 
    To use this PowerShell function, a DeepL ApiKey is needed which requires an account. To register for an account, go to www.deepl.com.
 
.LINK
    https://github.com/admins-little-helper/DeeplTranslate
 
.LINK
    https://www.deepl.com
 
.LINK
    https://www.deepl.com/de/docs-api/introduction/
 
#>



function Invoke-DeeplTranslateFile {
    <#
    .SYNOPSIS
        Translates a file using the DeepL API.
 
    .DESCRIPTION
        The 'Invoke-DeeplTranslateFile' function allows to translate a file using the DeepL API.
        Supported file types are .docx, .pptx, .pdf, .htm, .html, .txt.
 
    .PARAMETER InputFile
        The file path to the document to translate. Only these types are supported: .docx, .pptx, .pdf, .htm, .html, .txt.
 
    .PARAMETER OutputPath
        The folder path in which the output file should be saved. The output filename will be set to '<InputFilename>_<TargetLanguage>.<FileExtension>'.
 
    .PARAMETER TargetLanguage
        The language the text should be translated to.
 
    .PARAMETER OpenTranslatedFile
        If specified, the output file will be opened after download.
        In case there are more than 5 input files from pipeline ipnut, only the first 5 files will be opened.
        In case there are more than 5 input files specified via parameter, no file will be opened automatically.
 
    .PARAMETER Force
        If specified, existing output files will be overwritten.
 
    .PARAMETER SourceLanguage
        Language of the text to be translated. If no source language is specified, the DeepL Api tries to automatically detect the language.
 
    .PARAMETER Formality
        Sets whether the translated text should lean towards formal or informal language.
 
    .PARAMETER GlossaryId
        Specify the glossary to use for the translation.
        Important: This requires the SourceLanguage parameter to be set and the language pair of the glossary has to match the language pair of the request.
 
    .PARAMETER ApiKey
        API authentication key. You need an authentication key to access the DeepL API. Refer to the DeepL API documentation for more information.
 
    .EXAMPLE
        Invoke-DeeplTranslateFile -InputFile C:\Temp\SourceDoc.docx -OutputPath C:\Temp\Output -SourceLanguage "EN" -TargetLanguage "DE" -ApiKey "<MyApiKey>"
 
        This example shows how to translate a DOCX document.
 
    .EXAMPLE
        Invoke-DeeplTranslateFile -InputFile C:\Temp\SourceDoc.docx -OutputPath C:\Temp\Output -SourceLanguage "EN" -TargetLanguage "DE" -ApiKey "<MyApiKey>" -OpenTranslatedFile
 
        This example shows how to translate a DOCX document and directly opens the output file once it has been successfully downloaded.
 
    .EXAMPLE
        Invoke-DeeplTranslateFile InputFile C:\Temp\SourceDoc.docx -OutputPath C:\Temp\Output -SourceLanguage "EN" -TargetLanguage "DE" -ApiKey "<MyApiKey>"
 
        This example shows how to translate multiple files.
 
    .EXAMPLE
        Invoke-DeeplTranslateFile -InputFile C:\Temp\SourceDoc.docx -OutputPath C:\Temp\Output -SourceLanguage "EN" -TargetLanguage "DE" -ApiKey "<MyApiKey>" -Force
 
        This example shows how to translate multiple files, overwriting any existing files in the output path.
 
    .INPUTS
        System.IO.FileInfo for parameter 'InputFile'
 
    .OUTPUTS
        System.IO.FileInfo for each output file generated.
 
    .NOTES
        Author: Dieter Koch
        Email: diko@admins-little-helper.de
 
    .LINK
        https://github.com/admins-little-helper/DeeplTranslate/blob/main/Help/Invoke-DeeplTranslateFile.txt
 
    #>


    [CmdletBinding()]
    param (
        [ValidateScript({
                # Check if the given path is valid.
                if (-not (Test-Path -Path $_) ) {
                    throw "File or folder does not exist"
                }
                # Check if the given path is a file (not a folder).
                if (-not (Test-Path -Path $_ -PathType Leaf) ) {
                    throw "The Path argument must be a file. Folder paths are not allowed."
                }
                # Check if the extension of the given file matches the supported file extensions.
                if ($_ -notmatch "(\.docx|\.pptx|\.pdf|\.htm|\.html|\.txt)") {
                    throw "The file specified in the InputFile parameter must be one of these types: .docx, .pptx, .pdf, .htm, .html, .txt"
                }
                return $true
            })]
        [Parameter(Mandatory = $true, ValueFromPipeline)]
        [string[]]
        $InputFile,

        [ValidateScript({
                # Check if the given path is valid.
                if (-not (Test-Path -Path $_) ) {
                    throw "Folder does not exist"
                }
                # Check if the given path is a directory.
                if (-not (Test-Path -Path $_ -PathType Container) ) {
                    throw "The Path argument must be a folder. File paths are not allowed."
                }
                return $true
            })]
        [Parameter(Mandatory = $true)]
        [System.IO.FileInfo]
        $OutputPath,

        [Parameter(Mandatory = $false)]
        [switch]
        $Force,

        [Parameter(Mandatory = $false)]
        [switch]
        $OpenTranslatedFile,

        [ValidateSet("default", "more", "less", "prefer_more", "prefer_less")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Formality,

        [Parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.Guid[]]
        $GlossaryId,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ApiKey
    )

    # Use dynamic parameters for source and target language to either choose the language from a predefined static list
    # or retrieve the list of supported languages from the DeepL Api.
    DynamicParam {
        # Set the dynamic parameters name.
        $ParamSourceLang = 'SourceLanguage'
        $ParamTargetLang = 'TargetLanguage'

        # Create the dictionary.
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

        # Create the collection of attributes.
        $ParamSourceAttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $ParamTargetAttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

        # Create and set the parameters attributes.
        $ParamSourceAttribute = New-Object System.Management.Automation.ParameterAttribute
        $ParamSourceAttribute.Mandatory = $false
        $ParamTargetAttribute = New-Object System.Management.Automation.ParameterAttribute
        $ParamTargetAttribute.Mandatory = $true

        # Add the attributes to the attributes collections.
        $ParamSourceAttributeCollection.Add($ParamSourceAttribute)
        $ParamTargetAttributeCollection.Add($ParamTargetAttribute)

        # Generate and set the ValidateSet.
        if ([string]::IsNullOrEmpty($ApiKey)) {
            # Try to retrieve the list of supported source languages.
            $SourceLangList = Get-DeeplSupportedLanguage
            # If something was returned, we set the valid parameter values to the list of languages retrieved.
            if ($null -ne $SourceLangList) { $SourceLangArrSet = $SourceLangList.language }

            # Try to retrieve the list of supported target languages
            $TargetLangList = Get-DeeplSupportedLanguage -TargetLanguage
            # If something was returned, we set the valid parameter values to the list of languages retrieved.
            if ($null -ne $TargetLangList) { $TargetLangArrSet = $TargetLangList.language }
        }
        else {
            # Try to retrieve the list of supported source languages.
            $SourceLangList = Get-DeeplSupportedLanguage -ApiKey $ApiKey
            # If something was returned, we set the valid parameter values to the list of languages retrieved.
            if ($null -ne $SourceLangList) { $SourceLangArrSet = $SourceLangList.language }

            # Try to retrieve the list of supported target languages.
            $TargetLangList = Get-DeeplSupportedLanguage -ApiKey $ApiKey -TargetLanguage
            # If something was returned, we set the valid parameter values to the list of languages retrieved.
            if ($null -ne $TargetLangList) { $TargetLangArrSet = $TargetLangList.language }
        }

        $ValidateSetSourceLangAttribute = New-Object System.Management.Automation.ValidateSetAttribute($SourceLangArrSet)
        $ValidateSetTargetLangAttribute = New-Object System.Management.Automation.ValidateSetAttribute($TargetLangArrSet)

        # Add the ValidateSet to the attributes collection.
        $ParamSourceAttributeCollection.Add($ValidateSetSourceLangAttribute)
        $ParamTargetAttributeCollection.Add($ValidateSetTargetLangAttribute)

        # Create and return the dynamic parameter.
        $SourceLangRuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamSourceLang, [string[]], $ParamSourceAttributeCollection)
        $TargetLangRuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamTargetLang, [string[]], $ParamTargetAttributeCollection)
        $RuntimeParameterDictionary.Add($ParamSourceLang, $SourceLangRuntimeParameter)
        $RuntimeParameterDictionary.Add($ParamTargetLang, $TargetLangRuntimeParameter)
        return $RuntimeParameterDictionary
    }

    begin {
        # Bind the parameter to a friendly variable.
        if ($null -ne $PsBoundParameters[$ParamSourceLang]) {
            $SourceLanguage = $PsBoundParameters[$ParamSourceLang][0]
            Write-Verbose -Message "Setting source language to $SourceLanguage"
        }
        $TargetLanguage = $PsBoundParameters[$ParamTargetLang][0]
        Write-Verbose -Message "Setting source language to $TargetLanguage"

        # Build the body parameters as hashtable. Other properties are added later depending on the parameter values specified when the function is called.
        $Body = @{
            auth_key    = $ApiKey
            target_lang = $TargetLanguage
        }

        # Set optional API parameters in case parameter values have been specified in the function call.
        if ($SourceLanguage) { $Body.source_lang = $SourceLanguage }
        if ($Formality) { $Body.formality = $Formality }
        if ($GlossaryId) { $Body.glossary_id = $GlossaryId.Guid }

        # Set the content type to multipart to support the file content upload.
        $ContentType = "multipart/form-data; charset=utf-8"

        # Set the output encoding to UTF8 so that this function shows the same results for special characters in PowerShell 5.1 and in PowerShell 6.x or later.
        [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()

        $BaseUri = Get-DeeplApiUri -ApiKey $ApiKey

        $ProcessCount = 0
    }

    process {
        $ProcessCount++
        Write-Verbose -Message "ProcessCount: $ProcessCount"

        if ($OpenTranslatedFile.IsPresent -and $InputFile.Count -gt 5) {
            Write-Warning -Message "Number of input files exceeds the limit of 5. No file will be opened."
        }

        foreach ($File in $InputFile) {
            # Generate the filename for the output file.
            $InputFileObj = Get-Item -Path $File
            [System.IO.FileInfo]$OutputFile = "$($OutputPath.FullName)\$($InputFileObj.BaseName)_$TargetLanguage$($InputFileObj.Extension)"

            Write-Verbose -Message $OutputFile.FullName

            # Check if the output file already exists.
            if (Test-Path -Path $OutputFile -PathType Leaf) {
                Write-Warning -Message "OutputFile file already exists!"

                if ($Force.IsPresent) {
                    Write-Warning -Message "Parameter 'Force' was specified. Will overwrite existing file!"
                }
                else {
                    Write-Error -Message "OutputFile file already exist! Specify a different filename/path or use the -Force parameter to overwrite the existing file."
                    break
                }
            }
            else {
                Write-Verbose -Message "OutputFile file does not yet exist. Will continue."
            }

            # Set the file parameter to the content of the InputDoc.
            $Body.file = Get-Item -Path $File
            # Set the filename parameter.
            $Body.filename = $File

            # Upload the document for translation.
            try {
                # Set the uthorization header.
                $Headers = @{ Authorization = "DeepL-Auth-Key $ApiKey" }

                # Set parameters for the Invoke-RestMethod cmdlet.
                $UploadParams = @{
                    Method      = 'POST'
                    Uri         = "$BaseUri/document"
                    Headers     = $Headers
                    Form        = $Body
                    ContentType = $ContentType
                }

                Write-Verbose -Message "Calling Uri: $($UploadParams.Uri)"
                $UploadResponse = Invoke-RestMethod @UploadParams

                Write-Verbose -Message "Document ID: $($UploadResponse.document_id)"
                Write-Verbose -Message "Document Key: $($UploadResponse.document_key)"

                # Prepare a new body to check the status of the document translation.
                $BodyDocStatus = @{
                    auth_key     = $Body.auth_key
                    document_key = $UploadResponse.document_key
                }

                # Set a timeout to have a escape condition.
                $TimeOutOccured = $false
                $TimeOutAfterSeconds = 60
                $Timer = [System.Diagnostics.Stopwatch]::new()
                $Timer.Start()

                do {
                    # Check if the request already timed out.
                    if ($Timer.ElapsedMilliseconds / 1000 -gt $TimeOutAfterSeconds) {
                        $TimeOutOccured = $true
                    }
                    else {
                        # Set parameters for the Invoke-RestMethod cmdlet.
                        $StatusParams = @{
                            Method  = 'POST'
                            Uri     = "$BaseUri/document/$($UploadResponse.document_id)"
                            Headers = $Headers
                            Body    = $BodyDocStatus
                        }

                        Write-Verbose -Message "Calling Uri: $($StatusParams.uri)"
                        $StatusResponse = Invoke-RestMethod @StatusParams

                        # Check if the response contains a 'status' property.
                        if (($StatusResponse | Get-Member -MemberType NoteProperty | Select-Object -Property Name).Name -contains "status") {
                            Write-Verbose -Message "Job status: $($StatusResponse.status)"

                            switch ($StatusResponse.status) {
                                'translating' {
                                    Write-Verbose -Message "Estimated seconds remaining for document translation: $($StatusResponse.seconds_remaining)"
                                    Start-Sleep -Seconds 1
                                }
                                'error' {
                                    if (($StatusResponse | Get-Member -MemberType NoteProperty | Select-Object -Property Name).Name -contains "message") {
                                        Write-Error -Message "Job status: $($StatusResponse.message)"
                                        break
                                    }
                                }
                                'done' {
                                    Write-Verbose -Message "Translation completed."
                                    #$StatusResponse
                                }
                            }
                        }
                    }
                } while ($StatusResponse.status -eq 'translating' -or $TimeOutOccured)

                # when the document translation is done, try to download the resulting document.
                if ($StatusResponse.status -eq 'done') {
                    # Set parameters for the Invoke-RestMethod cmdlet.
                    $DocResponseParams = @{
                        Method  = 'POST'
                        Uri     = "$BaseUri/document/$($UploadResponse.document_id)/result"
                        Headers = $Headers
                        Body    = $BodyDocStatus
                        OutFile = $OutputFile
                    }

                    Write-Verbose -Message "Calling Uri: $($DocResponseParams.Uri))"
                    $DocResponse = Invoke-RestMethod @DocResponseParams
                    Write-Verbose -Message "Number of billed characters: $($StatusResponse.billed_characters)"

                    # In case the parameter 'OpenTranslatedFile' was specified, the resulting file will be openend.
                    # However, this will only be done in case the total number of files is less than 5.
                    # In case the input file was handed over via the 'InputFile' parameter, we can use the count of that array variable.
                    # But in case the input file comes from the pipeline, the count will always be 1. So we then we use the $ProcessCount variable.
                    # However, that will allow to open the first 5 files coming from the pipeline. But still better then possibly opening hundreds of files.
                    if ($OpenTranslatedFile.IsPresent -and $InputFile.Count -le 5 -and $ProcessCount -le 5) {
                        Start-Process -FilePath $OutputFile
                    }

                    if ($OpenTranslatedFile.IsPresent -and $ProcessCount -gt 5) {
                        Write-Warning -Message "Number of input files exceeds the limit of 5. No more files will be opened."
                    }
                }

                if ($DocResponse) {
                    $DocResponse
                }
                else {
                    Get-Item -Path $OutputFile
                }
            }
            catch [Microsoft.PowerShell.Commands.HttpResponseException] {
                $ErrorMessage = Get-DeeplStatusCode -StatusCode $_.Exception.Response.StatusCode
                if ($null -ne $ErrorMessage) {
                    Write-Error -Message $ErrorMessage
                }
                else {
                    Write-Error -Message "Http Status Code: $_"
                }
            }
            catch {
                Write-Verbose -Message "An unknown error occured."
                $_
            }
        }
    }
}


#region EndOfScript
<#
################################################################################
################################################################################
#
# ______ _ __ _____ _ _
# | ____| | | / _| / ____| (_) | |
# | |__ _ __ __| | ___ | |_ | (___ ___ _ __ _ _ __ | |_
# | __| | '_ \ / _` | / _ \| _| \___ \ / __| '__| | '_ \| __|
# | |____| | | | (_| | | (_) | | ____) | (__| | | | |_) | |_
# |______|_| |_|\__,_| \___/|_| |_____/ \___|_| |_| .__/ \__|
# | |
# |_|
################################################################################
################################################################################
# created with help of http://patorjk.com/software/taag/
#>

#endregion