├── .gitattributes
├── .gitignore
├── .gitmodules
├── DocumentTranslation.CLI
├── DocumentTranslatorIcon_100.png
├── Program.cs
├── Properties
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ └── launchSettings.json
├── doctr.csproj
├── testDocTr.cmd
└── testDocTrSet.cmd
├── DocumentTranslation.GUI
├── App.xaml
├── App.xaml.cs
├── AssemblyInfo.cs
├── Categories.cs
├── DocumentTranslation.GUI.csproj
├── DocumentTranslatorIcon_100.ico
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── Properties
│ ├── Resources.Designer.cs
│ ├── Resources.de-de.resx
│ ├── Resources.es-es.resx
│ ├── Resources.fr-fr.resx
│ ├── Resources.it-it.resx
│ └── Resources.resx
├── Resources
│ └── Version.txt
├── ShowErrors.xaml
├── ShowErrors.xaml.cs
├── UISettingsSetter.cs
└── ViewModel.cs
├── DocumentTranslation.Setup
├── DocumentTranslation.Setup.wixproj
├── LICENSE.rtf
├── Product.wxs
└── ZipItUp.cmd
├── DocumentTranslation.sln
├── DocumentTranslationService
├── AppSettingsSetter.cs
├── CredentialsException.cs
├── DocTransAppSettings.cs
├── DocumentTranslationBusiness.cs
├── DocumentTranslationService.cs
├── DocumentTranslationService.csproj
├── FlightPolicy.cs
├── GetCapabilities.cs
├── Glossary.cs
├── HttpClientFactory.cs
├── InvalidCategoryException.cs
├── KeyVaultAccess.cs
├── KeyVaultAccessException.cs
├── Language.cs
├── LocalFormats
│ ├── LocalDocumentTranslationFileFormat.cs
│ ├── LocalFormats.cs
│ ├── SRTCaptionInfo.cs
│ ├── SRTMarkdownConverter.cs
│ └── StringCompression.cs
├── Logger.cs
├── StatusResponse.cs
├── TestCredentials.cs
└── TextTranslationService.cs
├── LICENSE.md
├── README.md
└── docs
├── images
├── AppPrivateEndpoint.png
├── AzureKeyVault.png
├── AzureKeyVaultOverview.png
├── AzurePrivateEndpoint.png
├── Glossary.png
├── Logo.png
├── Running.png
├── SettingsDialog.png
├── TextTranslate.png
├── TranslateDocuments.png
├── connectionstring.png
├── storageaccount1.png
├── translatoraccount.png
└── translatorkey.png
└── index.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
364 | /DocumentTranslation.GUI/Properties/BuildDate.txt
365 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/.gitmodules
--------------------------------------------------------------------------------
/DocumentTranslation.CLI/DocumentTranslatorIcon_100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/DocumentTranslation.CLI/DocumentTranslatorIcon_100.png
--------------------------------------------------------------------------------
/DocumentTranslation.CLI/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace DocumentTranslation.CLI.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DocumentTranslation.CLI.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 |
63 | ///
64 | /// Looks up a localized string similar to ERROR: Missing or invalid credentials. Use the 'config set' command to set values..
65 | ///
66 | internal static string msg_MissingCredentials {
67 | get {
68 | return ResourceManager.GetString("msg_MissingCredentials", resourceCulture);
69 | }
70 | }
71 |
72 | ///
73 | /// Looks up a localized string similar to ERROR: Translator service: .
74 | ///
75 | internal static string msg_ServerMessage {
76 | get {
77 | return ResourceManager.GetString("msg_ServerMessage", resourceCulture);
78 | }
79 | }
80 |
81 | ///
82 | /// Looks up a localized string similar to ERROR: Missing or invalid resource name. Use the 'config set name' command to set value..
83 | ///
84 | internal static string msg_WrongResourceName {
85 | get {
86 | return ResourceManager.GetString("msg_WrongResourceName", resourceCulture);
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/DocumentTranslation.CLI/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | ERROR: Missing or invalid credentials. Use the 'config set' command to set values.
122 |
123 |
124 | ERROR: Translator service:
125 |
126 |
127 | ERROR: Missing or invalid resource name. Use the 'config set name' command to set value.
128 |
129 |
--------------------------------------------------------------------------------
/DocumentTranslation.CLI/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "TestDocumentTranslation": {
4 | "commandName": "Project",
5 | "commandLineArgs": "config test"
6 | },
7 | "TranslateWGlossary": {
8 | "commandName": "Project",
9 | "commandLineArgs": "translate d:\\TestDocsSmall --to de --glossary d:\\testdocs\\glossary.tsv"
10 | },
11 | "TranslateOneDrive": {
12 | "commandName": "Project",
13 | "commandLineArgs": "translate C:\\Users\\wendt\\OneDrive\\Test --to de "
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/DocumentTranslation.CLI/doctr.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | DocumentTranslation.CLI
7 | 0.0.5
8 | Chris Wendt
9 | Document Translation
10 | Translate documents using the Azure Translator service
11 | MIT license
12 |
13 | https://github.com/MicrosoftTranslator/DocumentTranslation
14 | DocumentTranslatorIcon_100.png
15 | https://github.com/MicrosoftTranslator/DocumentTranslation
16 | Github
17 | Initial pre-release.
18 | en
19 | 0.0.8.0
20 | 0.0.8.0
21 | true
22 | false
23 | OnBuildSuccess
24 | DocumentTranslation.CLI.Program
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | True
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | True
45 | True
46 | Resources.resx
47 |
48 |
49 |
50 |
51 |
52 | ResXFileCodeGenerator
53 | Resources.Designer.cs
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/DocumentTranslation.CLI/testDocTr.cmd:
--------------------------------------------------------------------------------
1 | cd bin\Debug\net6.0
2 | doctr languages
3 | pause
4 | doctr formats
5 | pause
6 | doctr clear
7 | pause
8 | doctr config test
9 | pause
10 | doctr config list
11 | pause
12 | doctr glossary
13 | pause
14 | doctr translate d:\TestDocsSmall --to de --glossary d:\glossary.tsv
--------------------------------------------------------------------------------
/DocumentTranslation.CLI/testDocTrSet.cmd:
--------------------------------------------------------------------------------
1 | REM fill all the with the values from your Azure portal
2 | cd bin\debug\net6.0
3 | doctr config set --key
4 | doctr config set --storage .cognitiveservices.azure.com/
6 | doctr config set --region eastus2
7 | doctr config set --category sometestcategory
8 | pause
9 | doctr config list
10 | pause
11 | doctr config test
12 | cd ..\..\..
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace DocumentTranslation.GUI
4 | {
5 | ///
6 | /// Interaction logic for App.xaml
7 | ///
8 | public partial class App : Application
9 | {
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | [assembly: ThemeInfo(
4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
5 | //(used if a resource is not found in the page,
6 | // or application resource dictionaries)
7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
8 | //(used if a resource is not found in the page,
9 | // app, or any theme specific resource dictionaries)
10 | )]
11 |
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/Categories.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.IO;
4 | using System.Text.Json;
5 |
6 | namespace DocumentTranslation.GUI
7 | {
8 | public class Categories
9 | {
10 | public BindingList MyCategoryList { get; set; } = new();
11 |
12 | const string AppName = "Document Translation";
13 | const string AppSettingsFileName = "CustomCategories.json";
14 |
15 | public Categories()
16 | {
17 | Read();
18 | }
19 |
20 | private void Read()
21 | {
22 | string categoriesJson;
23 | try
24 | {
25 | string filename = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + AppName + Path.DirectorySeparatorChar + AppSettingsFileName;
26 | categoriesJson = File.ReadAllText(filename);
27 | }
28 | catch (Exception ex)
29 | {
30 | if (ex is FileNotFoundException || ex is DirectoryNotFoundException)
31 | {
32 | MyCategoryList.Add(new MyCategory("Category 1", ""));
33 | MyCategoryList.Add(new MyCategory("Category 2", ""));
34 | return;
35 | }
36 | throw;
37 | }
38 | try
39 | {
40 | MyCategoryList = JsonSerializer.Deserialize>(categoriesJson, new JsonSerializerOptions { IncludeFields = true });
41 | }
42 | catch
43 | {
44 | MyCategoryList.Add(new MyCategory("Category 1", ""));
45 | MyCategoryList.Add(new MyCategory("Category 2", ""));
46 | }
47 | }
48 |
49 | public void Write(string filename = null)
50 | {
51 | if (string.IsNullOrEmpty(filename))
52 | {
53 | Directory.CreateDirectory(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + AppName);
54 | filename = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + AppName + Path.DirectorySeparatorChar + AppSettingsFileName;
55 | }
56 | File.WriteAllText(filename, JsonSerializer.Serialize(this.MyCategoryList, new JsonSerializerOptions { IncludeFields = true, WriteIndented = true }));
57 | }
58 | }
59 |
60 | public class MyCategory
61 | {
62 | public string Name { get; set; }
63 | public string ID { get; set; }
64 |
65 | public MyCategory(string name, string iD)
66 | {
67 | Name = name;
68 | ID = iD;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/DocumentTranslation.GUI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net8.0-windows7.0
6 | true
7 | true
8 | DocumentTranslatorIcon_100.ico
9 | 1.0.0.0
10 | 1.0.0.0
11 | en-US
12 |
13 |
14 |
15 |
16 | True
17 | True
18 | Resources.resx
19 |
20 |
21 | Resources.resx
22 | True
23 | True
24 |
25 |
26 |
27 |
28 |
29 | PublicResXFileCodeGenerator
30 | Resources.Designer.cs
31 |
32 |
33 | Designer
34 | Resources.Designer.cs
35 | PublicResXFileCodeGenerator
36 |
37 |
38 |
39 |
40 |
41 | Always
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/DocumentTranslatorIcon_100.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/DocumentTranslation.GUI/DocumentTranslatorIcon_100.ico
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/Properties/Resources.es-es.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 |
122 | BuildDate.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252
123 |
124 |
125 | ..\Resources\Version.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252
126 |
127 |
128 | Agregar
129 |
130 |
131 | Hojear
132 |
133 |
134 | Cancelar
135 |
136 |
137 | Claro
138 |
139 |
140 | Cerrar
141 |
142 |
143 | Borrar
144 |
145 |
146 | Salvar
147 |
148 |
149 | Escoger
150 |
151 |
152 | Mostrar errores
153 |
154 |
155 | Prueba
156 |
157 |
158 | Traducir
159 |
160 |
161 | Traducir documentos
162 |
163 |
164 | Detección automática
165 |
166 |
167 | Región de Azure:
168 |
169 |
170 | Categoría:
171 |
172 |
173 | ID de categoría de traductor personalizado
174 |
175 |
176 | Nombre
177 |
178 |
179 | Punto de enlace de traducción de documentos:
180 |
181 |
182 | Documentos a traducir:
183 |
184 |
185 | Traducción de documentos |
186 |
187 |
188 | Experimental
189 |
190 |
191 | Vuelo:
192 |
193 |
194 | De:
195 |
196 |
197 | Glosarios |
198 |
199 |
200 | Glosario a utilizar (opcional):
201 |
202 |
203 | Entrada:
204 |
205 |
206 | URI del Almacén de claves de Azure:
207 |
208 |
209 | Nuevo ID de categoría
210 |
211 |
212 | Nuevo nombre de categoría
213 |
214 |
215 | Mostrar lenguajes experimentales:
216 |
217 |
218 | Cadena de conexión de almacenamiento:
219 |
220 |
221 | Clave de recurso:
222 |
223 |
224 | Carpeta de destino:
225 |
226 |
227 | Ensayo...
228 |
229 |
230 | Extremo de traducción de texto:
231 |
232 |
233 | Para:
234 |
235 |
236 | Traducción:
237 |
238 |
239 | Bytes
240 |
241 |
242 | Cancelado
243 |
244 |
245 | Cancelar...
246 |
247 |
248 | Categorías guardadas
249 |
250 |
251 | Personajes cargados:
252 |
253 |
254 | Completado:
255 |
256 |
257 | documento(s) traducido(s)
258 |
259 |
260 | Documentos cargados.
261 |
262 |
263 | Carga de documentos ...
264 |
265 |
266 | Hecho
267 |
268 |
269 | Error
270 |
271 |
272 | Fracasado:
273 |
274 |
275 | Archivos
276 |
277 |
278 | En curso:
279 |
280 |
281 | Credenciales no válidas
282 |
283 |
284 | Obtenga credenciales válidas e ingrese en Configuración
285 |
286 |
287 | idioma seleccionado
288 |
289 |
290 | idiomas seleccionados
291 |
292 |
293 | Inicia sesión con tu cuenta profesional o educativa.
294 |
295 |
296 | Nombre del recurso incorrecto o no es una suscripción de pago
297 |
298 |
299 | Se requiere una suscripción de nivel S1 o superior.
300 |
301 |
302 | Configuración guardada
303 |
304 |
305 | Haber iniciado sesión
306 |
307 |
308 | Iniciar sesión ...
309 |
310 |
311 | Error del contenedor de almacenamiento
312 |
313 |
314 | Prueba fallida
315 |
316 |
317 | Prueba superada
318 |
319 |
320 | caracteres traducidos
321 |
322 |
323 | Error
324 |
325 |
326 | Categoría no válida
327 |
328 |
329 | Espera:
330 |
331 |
332 | Acerca de
333 |
334 |
335 | Autenticación
336 |
337 |
338 | Categorías
339 |
340 |
341 | Documentación
342 |
343 |
344 | Idiomas
345 |
346 |
347 | Configuración
348 |
349 |
350 | Traducir documentos
351 |
352 |
353 | Traducir texto
354 |
355 |
356 | Fecha de construcción:
357 |
358 |
359 | Esta lista de nombres de categoría proporciona un fácil acceso a un nombre descriptivo para los sistemas de traducción personalizados que ha creado con Custom Translator. Más información en http://customtranslator.ai. No es necesario usar esto a menos que haya creado un sistema de traducción personalizado.
360 |
361 |
362 | Cómo obtener las credenciales de servicio.
363 |
364 |
365 | La traducción de documentos utiliza códigos de idioma ISO para referirse a un idioma. Esta tabla ayuda a identificar dónde coloca Document Translation un archivo cuando se utiliza un '*' en el nombre de la carpeta de destino.
366 |
367 |
368 | Abrir en el navegador
369 |
370 |
371 | Obtenga la clave de recursos, la región de Azure, los puntos de conexión y la cadena de conexión de almacenamiento del Portal de Azure o del administrador de Azure de su organización. Si tiene un URI del Almacén de claves de Azure, no es necesario rellenar los demás campos.
372 |
373 |
374 | Versión:
375 |
376 |
377 | Errores de traducción de documentos
378 |
379 |
380 | Traducción de documentos
381 |
382 |
383 | La región de Azure en la que reside el recurso de Translator.
384 |
385 |
386 | Utilice la pestaña "Configuración", "Categorías" para definir un conjunto de categorías de traductor personalizado que desea utilizar. Deje la selección clara si no está utilizando Custom Translator.
387 |
388 |
389 | Borra la selección de una categoría. No elimina la categoría de su conjunto de categorías disponibles.
390 |
391 |
392 | El extremo del servicio de traducción de documentos. Para Azure public https://<resource name>.cognitiveservices.azure.com.
393 |
394 |
395 | Si recibió un código de Microsoft para la funcionalidad experimental, escríbalo aquí
396 |
397 |
398 | Obtenga el URI del Almacén de claves de Azure del administrador de Azure profesional o educativo. O 'Borrar' para proporcionar la configuración usted mismo.
399 |
400 |
401 | Guarde la configuración anterior en su dispositivo.
402 |
403 |
404 | Si mostrar los idiomas que el servicio Translator considera experimentales. Los idiomas experimentales pueden no estar disponibles en ambas direcciones y la traducción puede fallar. Déjelo sin marcar a menos que necesite experimentar con idiomas no totalmente compatibles.
405 |
406 |
407 | Copie la "Cadena de conexión" de la página Propiedades del recurso Almacenamiento de la suscripción de Azure.
408 |
409 |
410 | Copie la "Clave 1" o la "Clave 2" del recurso Traductor en su suscripción de Azure.
411 |
412 |
413 | Indique la carpeta de destino. '*' muestra dónde debe ir el ID de idioma.
414 |
415 |
416 | Invoque una prueba de la configuración anterior. Mostrará un resultado aprobado o no aprobado.
417 |
418 |
419 | Punto de conexión de traducción de texto de Azure Translator
420 |
421 |
422 | Establezca esta propiedad en true cuando las solicitudes deban autenticarse en un servidor proxy utilizando las credenciales del usuario que ha iniciado sesión actualmente.
423 |
424 |
425 | Usar credenciales predeterminadas (proxy)
426 |
427 |
428 | Dirección proxy
429 |
430 |
431 | Dirección que se utilizará para las solicitudes http proxy.
432 |
433 |
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | Add
122 |
123 |
124 | Browse
125 |
126 |
127 | Cancel
128 |
129 |
130 | Clear
131 |
132 |
133 | Delete
134 |
135 |
136 | Save
137 |
138 |
139 | Select
140 |
141 |
142 | Test
143 |
144 |
145 | Translate
146 |
147 |
148 | Translate Documents
149 |
150 |
151 | Azure Region:
152 |
153 |
154 | Category:
155 |
156 |
157 | Custom Translator Category ID
158 |
159 |
160 | Name
161 |
162 |
163 | Documents to translate:
164 |
165 |
166 | From:
167 |
168 |
169 | Glossary to use (optional):
170 |
171 |
172 | Input:
173 |
174 |
175 | Document Translation Endpoint:
176 |
177 |
178 | Show experimental languages:
179 |
180 |
181 | Storage Connection String:
182 |
183 |
184 | Resource Key:
185 |
186 |
187 | Target folder:
188 |
189 |
190 | To:
191 |
192 |
193 | Translation:
194 |
195 |
196 | bytes
197 |
198 |
199 | Canceled
200 |
201 |
202 | Canceling...
203 |
204 |
205 | Categories saved
206 |
207 |
208 | Characters Charged:
209 |
210 |
211 | Completed:
212 |
213 |
214 | document(s) translated
215 |
216 |
217 | Documents uploaded.
218 |
219 |
220 | Done
221 |
222 |
223 | Failed:
224 |
225 |
226 | In progress:
227 |
228 |
229 | Invalid credentials
230 |
231 |
232 | Please obtain valid credentials and enter in Settings
233 |
234 |
235 | Settings saved
236 |
237 |
238 | Test failed
239 |
240 |
241 | Test passed
242 |
243 |
244 | Error
245 |
246 |
247 | Invalid Category
248 |
249 |
250 | Waiting:
251 |
252 |
253 | Authentication
254 |
255 |
256 | Categories
257 |
258 |
259 | Settings
260 |
261 |
262 | Translate Documents
263 |
264 |
265 | Translate Text
266 |
267 |
268 | This list of category names provides easy access to a friendly name for custom translation systems you have built with Custom Translator. More information at http://customtranslator.ai. No need to use this unless you have built a custom translation system.
269 |
270 |
271 | Obtain the resource key, the Azure region, the endpoints and the storage connection string from the Azure portal or from your organization's Azure administrator. If you have an Azure Key Vault URI, no need to fill the other fields.
272 |
273 |
274 | Document Translation
275 | App title
276 |
277 |
278 | The Azure region your Translator resource resides in.
279 |
280 |
281 | Use the "Settings" tab, "Categories" to define a set of Custom Translator categories you want to use. Leave the selection clear if you are not using Custom Translator.
282 |
283 |
284 | Clears the selection of a category. Does not remove the category fron your set of available categories.
285 |
286 |
287 | The endpoint of the document Translation Service. For Azure public https://<resource name>.cognitiveservices.azure.com.
288 |
289 |
290 | Save the above settings on your device.
291 |
292 |
293 | Whether to show the languages the Translator service considers experimental. Experimental languages may not be available in both directions and the translation can fail. Leave unchecked unless you need to experiment with not fully supported languages.
294 |
295 |
296 | Copy the "Connection String" from the Properties page of the Storage resource in your Azure subscription.
297 |
298 |
299 | Copy the "Key 1" or "Key 2" of the Translator resource in your Azure subscription.
300 |
301 |
302 | Invoke a test of the above settings. Will show a pass or fail result.
303 |
304 |
305 | characters translated
306 |
307 |
308 | How to obtain the service credentials.
309 |
310 |
311 | Open in browser
312 |
313 |
314 |
315 | BuildDate.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252
316 |
317 |
318 | Build date:
319 |
320 |
321 | Testing...
322 |
323 |
324 | Storage container error
325 |
326 |
327 | S1 or higher tier subscription is required.
328 |
329 |
330 | Error
331 |
332 |
333 | Resource name incorrect or not a paid subscription
334 |
335 |
336 | New category name
337 |
338 |
339 | New category ID
340 |
341 |
342 | Auto-Detect
343 |
344 |
345 | Experimental
346 |
347 |
348 | Azure Key Vault URI:
349 |
350 |
351 | Please sign in with your work or school account.
352 |
353 |
354 | Obtain the URI of the Azure Key Vault from your work or school Azure administrator. Or 'Clear' to supply the settings yourself.
355 |
356 |
357 | Document upload ...
358 |
359 |
360 | files
361 |
362 |
363 | Show Errors
364 |
365 |
366 | Document translation errors
367 |
368 |
369 | Close
370 |
371 |
372 | Document Translation|
373 | Used for the file filter dialog.
374 |
375 |
376 | Glossaries|
377 | Used in the file filtering dialog.
378 |
379 |
380 | Signing in ...
381 |
382 |
383 | Signed in
384 |
385 |
386 | languages selected
387 |
388 |
389 | language selected
390 |
391 |
392 | Indicate the target folder. '*' shows where the language ID should go.
393 |
394 |
395 | Text Translation Endpoint:
396 |
397 |
398 | Azure Translator Text Translation Endpoint
399 |
400 |
401 | Flight:
402 |
403 |
404 | If you received a code from Microsoft for experimental functionality, enter it here
405 |
406 |
407 | Document Translation uses ISO language codes to refer to a language. This table helps identify where Document Translation places a file when you use a '*' in the target folder name.
408 |
409 |
410 | About
411 |
412 |
413 | Documentation
414 |
415 |
416 | Languages
417 |
418 |
419 | Version:
420 |
421 |
422 | ..\Resources\Version.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252
423 |
424 |
425 | Set this property to true when requests should be authenticated against a proxy server using the credentials of the currently logged on user.
426 |
427 |
428 | Use Default Credentials (Proxy):
429 |
430 |
431 | Proxy Address:
432 |
433 |
434 | Address to be used for proxying http requests.
435 |
436 |
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/Resources/Version.txt:
--------------------------------------------------------------------------------
1 | 1.0.3.0
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/ShowErrors.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/ShowErrors.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace DocumentTranslation.GUI
4 | {
5 | ///
6 | /// Interaction logic for ShowErrors.xaml
7 | ///
8 | public partial class ShowErrors : Window
9 | {
10 | public ShowErrors()
11 | {
12 | InitializeComponent();
13 | }
14 |
15 | private void CloseButton_Click(object sender, RoutedEventArgs e)
16 | {
17 | Close();
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/UISettingsSetter.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Holds the configuration information for the document translation CLI
3 | */
4 |
5 | using System;
6 | using System.Collections.Generic;
7 | using System.IO;
8 | using System.Text.Json;
9 |
10 | namespace DocumentTranslation.GUI
11 | {
12 | ///
13 | /// Manage the storage of the application settings
14 | ///
15 | public static class UISettingsSetter
16 | {
17 | const string AppName = "Document Translation";
18 | const string AppSettingsFileName = "uisettings.json";
19 |
20 | public static UISettings Read(string filename = null)
21 | {
22 | string appsettingsJson;
23 | try
24 | {
25 | if (string.IsNullOrEmpty(filename)) filename = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + AppName + Path.DirectorySeparatorChar + AppSettingsFileName;
26 | appsettingsJson = File.ReadAllText(filename);
27 | }
28 |
29 | catch (FileNotFoundException)
30 | {
31 | return new UISettings();
32 | }
33 | catch (DirectoryNotFoundException)
34 | {
35 | return new UISettings();
36 | }
37 |
38 | return JsonSerializer.Deserialize(appsettingsJson, new JsonSerializerOptions { IncludeFields = true });
39 | }
40 |
41 | public static void Write(string filename, UISettings settings)
42 | {
43 | if (string.IsNullOrEmpty(filename))
44 | {
45 | Directory.CreateDirectory(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + AppName);
46 | filename = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + AppName + Path.DirectorySeparatorChar + AppSettingsFileName;
47 | }
48 | File.WriteAllText(filename, JsonSerializer.Serialize(settings, new JsonSerializerOptions { IncludeFields = true, WriteIndented = true }));
49 | }
50 | }
51 |
52 | public class UISettings
53 | {
54 | public string lastFromLanguage;
55 | public string lastToLanguage;
56 | public string lastFromLanguageDocuments;
57 | public List lastToLanguagesDocuments;
58 | public string lastCategoryText;
59 | public string lastCategoryDocuments;
60 | public string lastDocumentsFolder;
61 | public Dictionary PerLanguageFolders;
62 | }
63 | public class PerLanguageData
64 | {
65 | public string lastGlossariesFolder;
66 | public string lastGlossary;
67 | public string lastTargetFolder;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/DocumentTranslation.GUI/ViewModel.cs:
--------------------------------------------------------------------------------
1 | using DocumentTranslationService.Core;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.ComponentModel;
5 | using System.Diagnostics;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using System.Windows.Forms;
10 |
11 | namespace DocumentTranslation.GUI
12 | {
13 | internal class ViewModel
14 | {
15 | public BindingList ToLanguageList { get; private set; } = new();
16 | public BindingList FromLanguageList { get; private set; } = new();
17 | internal static UISettings UISettings;
18 | public BindingList ToLanguageListForDocuments { get; private set; } = new();
19 | public BindingList FromLanguageListForDocuments { get; private set; } = new();
20 |
21 | public static DocTransAppSettings Settings
22 | {
23 | get => localSettings.UsingKeyVault ? keyVaultSettings : localSettings;
24 | set => localSettings = value;
25 | }
26 | internal static DocTransAppSettings localSettings;
27 | private static DocTransAppSettings keyVaultSettings;
28 |
29 | public Language FromLanguage { get; set; }
30 | public Language ToLanguage { get; init; }
31 | public BindingList FilesToTranslate { get; private set; } = new();
32 | public string TargetFolder { get; set; }
33 | public string ErrorsText { get; set; }
34 |
35 | public BindingList GlossariesToUse { get; set; }
36 | public event EventHandler OnLanguagesUpdate;
37 | public event EventHandler OnKeyVaultAuthenticationStart;
38 | public event EventHandler OnKeyVaultAuthenticationComplete;
39 | public event EventHandler OnLanguagesFailed;
40 |
41 | internal DocumentTranslationService.Core.DocumentTranslationService documentTranslationService = new();
42 | public readonly Categories categories = new();
43 |
44 | public ViewModel()
45 | {
46 | localSettings = AppSettingsSetter.Read();
47 | UISettings = UISettingsSetter.Read();
48 | UISettings.PerLanguageFolders ??= new Dictionary();
49 | UISettings.lastToLanguagesDocuments ??= new List();
50 | }
51 |
52 | ///
53 | /// Initializes the document translation service. Call once per instance.
54 | ///
55 | ///
56 | ///
57 | ///
58 | public async Task InitializeAsync(bool IsTest = false)
59 | {
60 | documentTranslationService.OnLanguagesUpdate += DocumentTranslationService_OnLanguagesUpdate;
61 | documentTranslationService.ShowExperimental = localSettings.ShowExperimental;
62 | if (string.IsNullOrEmpty(documentTranslationService.TextTransUri)) documentTranslationService.TextTransUri = "https://api.cognitive.microsofttranslator.com/";
63 | try
64 | {
65 | _ = documentTranslationService.GetLanguagesAsync(); //this method can be called without credentials, and before the document translation service is initialized with credentials.
66 | }
67 | catch (Exception ex)
68 | {
69 | OnLanguagesFailed?.Invoke(this, ex.Message);
70 | }
71 | if (localSettings.UsingKeyVault)
72 | {
73 | Debug.WriteLine($"Start authenticating Key Vault {localSettings.AzureKeyVaultName}");
74 | if (!IsTest) OnKeyVaultAuthenticationStart?.Invoke(this, EventArgs.Empty);
75 | KeyVaultAccess kv = new(localSettings.AzureKeyVaultName);
76 | try
77 | {
78 | keyVaultSettings = await kv.GetKVCredentialsAsync();
79 | }
80 | catch (Exception ex)
81 | {
82 | throw new KeyVaultAccessException("Key Vault: " + ex.Message);
83 | }
84 | Debug.WriteLine($"Authentication Complete {localSettings.AzureKeyVaultName}");
85 | if (!IsTest) OnKeyVaultAuthenticationComplete?.Invoke(this, EventArgs.Empty);
86 | }
87 | else
88 | {
89 | try
90 | {
91 | AppSettingsSetter.CheckSettings(Settings);
92 | }
93 | catch (ArgumentException e)
94 | {
95 | throw new ArgumentNullException(e.ParamName);
96 | }
97 | }
98 |
99 | documentTranslationService.SubscriptionKey = Settings.SubscriptionKey;
100 | documentTranslationService.AzureRegion = Settings.AzureRegion;
101 | documentTranslationService.AzureResourceName = Settings.AzureResourceName;
102 | documentTranslationService.StorageConnectionString = Settings.ConnectionStrings.StorageConnectionString;
103 | documentTranslationService.TextTransUri = Settings.TextTransEndpoint;
104 | documentTranslationService.FlightString = localSettings.FlightString; //read the flight only from local settings.
105 | try
106 | {
107 | _ = this.documentTranslationService.InitializeAsync();
108 | }
109 | catch (DocumentTranslationService.Core.DocumentTranslationService.CredentialsException ex)
110 | {
111 | throw new DocumentTranslationService.Core.DocumentTranslationService.CredentialsException(ex.Message, ex);
112 | }
113 | return;
114 | }
115 |
116 | public static void SaveUISettings()
117 | {
118 | UISettingsSetter.Write(null, UISettings);
119 | }
120 |
121 | public static void SaveAppSettings()
122 | {
123 | AppSettingsSetter.Write(null, localSettings);
124 | }
125 |
126 | private void DocumentTranslationService_OnLanguagesUpdate(object sender, EventArgs e)
127 | {
128 | //Document translation does not support experimental languages. Maintain two separate language lists between document and text translation
129 | ToLanguageList.Clear();
130 | FromLanguageList.Clear();
131 | ToLanguageListForDocuments.Clear();
132 | FromLanguageListForDocuments.Clear();
133 | FromLanguageList.Add(new Language("auto", Properties.Resources.label_AutoDetect));
134 | FromLanguageListForDocuments.Add(new Language("auto", Properties.Resources.label_AutoDetect));
135 | var list = documentTranslationService.Languages.OrderBy((x) => x.Value.Name);
136 | foreach (var lang in list)
137 | {
138 | Language newLang = (Language)lang.Value.Clone();
139 | if (lang.Value.Experimental)
140 | newLang.Name = lang.Value.Name + " -" + Properties.Resources.label_Experimental + "-";
141 | ToLanguageList.Add(newLang);
142 | FromLanguageList.Add(newLang);
143 | if (!lang.Value.Experimental)
144 | {
145 | ToLanguageListForDocuments.Add(lang.Value);
146 | FromLanguageListForDocuments.Add(lang.Value);
147 | }
148 | }
149 | OnLanguagesUpdate?.Invoke(this, EventArgs.Empty);
150 | }
151 |
152 | internal async Task TranslateTextAsync(string text, string fromLanguageCode, string toLanguageCode)
153 | {
154 | if (fromLanguageCode == "auto") fromLanguageCode = null;
155 | documentTranslationService.AzureRegion = Settings.AzureRegion;
156 | string result = await documentTranslationService.TranslateStringAsync(text, fromLanguageCode, toLanguageCode);
157 | Debug.WriteLine($"Translate {text.Length} characters from {fromLanguageCode} to {toLanguageCode}");
158 | return result;
159 | }
160 |
161 | #region Generate Filters
162 | internal async Task GetDocumentExtensionsFilter()
163 | {
164 | StringBuilder filterBuilder = new();
165 | filterBuilder.Append(Properties.Resources.label_DocumentTranslation);
166 | await documentTranslationService.GetDocumentFormatsAsync();
167 | foreach (var format in documentTranslationService.FileFormats)
168 | {
169 | foreach (var ext in format.FileExtensions)
170 | {
171 | filterBuilder.Append("*" + ext + ";");
172 | }
173 | }
174 | filterBuilder.Remove(filterBuilder.Length - 1, 1);
175 | filterBuilder.Append('|');
176 |
177 | foreach (var format in documentTranslationService.FileFormats)
178 | {
179 | filterBuilder.Append(format.Format + "|");
180 | foreach (var ext in format.FileExtensions)
181 | {
182 | filterBuilder.Append("*" + ext + ";");
183 | }
184 | filterBuilder.Remove(filterBuilder.Length - 1, 1);
185 | filterBuilder.Append('|');
186 | }
187 | filterBuilder.Remove(filterBuilder.Length - 1, 1);
188 | return filterBuilder.ToString();
189 | }
190 |
191 | internal async Task GetGlossaryExtensionsFilter()
192 | {
193 | StringBuilder filterBuilder = new();
194 | filterBuilder.Append(Properties.Resources.label_Glossaries);
195 | await documentTranslationService.GetGlossaryFormatsAsync();
196 | foreach (var format in documentTranslationService.GlossaryFormats)
197 | {
198 | foreach (var ext in format.FileExtensions)
199 | {
200 | filterBuilder.Append("*" + ext + ";");
201 | }
202 | }
203 | filterBuilder.Remove(filterBuilder.Length - 1, 1);
204 | return filterBuilder.ToString();
205 | }
206 | #endregion
207 | #region Settings.Categories
208 |
209 | internal void AddCategory(DataGridViewSelectedCellCollection selectedCells)
210 | {
211 | foreach (DataGridViewCell cell in selectedCells)
212 | categories.MyCategoryList.Insert(cell.RowIndex, new MyCategory(Properties.Resources.label_NewCategorySample, Properties.Resources.label_NewCategoryIDSample));
213 | }
214 |
215 | internal void DeleteCategory(DataGridViewSelectedCellCollection selectedCells)
216 | {
217 | foreach (DataGridViewCell cell in selectedCells)
218 | categories.MyCategoryList.RemoveAt(cell.RowIndex);
219 | }
220 |
221 | internal void SaveCategories()
222 | {
223 | categories.Write();
224 | }
225 |
226 | #endregion
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/DocumentTranslation.Setup/DocumentTranslation.Setup.wixproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | x86
6 | 3.10
7 | ed9deeb6-3e2d-47ac-979d-39294652c3f2
8 | 2.0
9 | DocumentTranslation.Setup
10 | Package
11 |
12 |
13 | bin\$(Configuration)\
14 | obj\$(Configuration)\
15 | Debug
16 |
17 |
18 | bin\$(Configuration)\
19 | obj\$(Configuration)\
20 |
21 |
22 |
23 |
24 |
25 |
26 | doctr
27 | {fee35a76-74a5-4321-afc2-961aa1b29636}
28 | True
29 | True
30 | Binaries;Content;Satellites
31 | INSTALLFOLDER
32 |
33 |
34 | DocumentTranslation.GUI
35 | {23830242-c648-49b3-97d4-233775c218c5}
36 | True
37 | True
38 | Binaries;Content;Satellites
39 | INSTALLFOLDER
40 |
41 |
42 | DocumentTranslationService
43 | {aabfd3bb-cc1e-4aa7-b792-72a1bc83a155}
44 | True
45 | True
46 | Binaries;Content;Satellites
47 | INSTALLFOLDER
48 |
49 |
50 |
51 |
52 | C:\Users\wendt\.nuget\packages\wix\3.11.2\tools\WixUtilExtension.dll
53 | WixUtilExtension
54 |
55 |
56 | C:\Users\wendt\.nuget\packages\wix\3.11.2\tools\WixUIExtension.dll
57 | WixUIExtension
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | if $(ConfigurationName) == Relskip "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\signtool.exe" sign /fd SHA256 /i Sectigo !(TargetPath)
71 |
72 |
80 |
--------------------------------------------------------------------------------
/DocumentTranslation.Setup/LICENSE.rtf:
--------------------------------------------------------------------------------
1 | {\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033\deflangfe1033{\fonttbl{\f0\fmodern\fprq1\fcharset0 Courier New;}}
2 | {\*\generator Riched20 6.3.9600}{\*\mmathPr\mnaryLim0\mdispDef1\mwrapIndent1440 }\viewkind4\uc1
3 | MIT License\par
4 | -----------\par
5 | \par
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\par
7 | \par
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\par
9 | \par
10 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\par
11 | }
12 |
--------------------------------------------------------------------------------
/DocumentTranslation.Setup/Product.wxs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
27 | WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
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 |
--------------------------------------------------------------------------------
/DocumentTranslation.Setup/ZipItUp.cmd:
--------------------------------------------------------------------------------
1 | del bin\release\DocumentTranslation.zip
2 | md temp
3 | del temp\*.* /s /f /q
4 |
5 | xcopy /q ..\DocumentTranslation.GUI\bin\Release\net6.0-windows\*.* temp
6 | xcopy /q /y ..\DocumentTranslation.CLI\bin\Release\net6.0\*.* temp
7 |
8 | tar.exe -a -c -p -f bin\release\DocumentTranslation.zip -C temp *.*
9 | pause
10 |
11 | del temp\*.* /s /f /q
12 | rd temp
--------------------------------------------------------------------------------
/DocumentTranslation.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.32014.148
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "doctr", "DocumentTranslation.CLI\doctr.csproj", "{FEE35A76-74A5-4321-AFC2-961AA1B29636}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D9A35F10-42E5-4DA4-B4DF-AF2A0FF5652E}"
9 | ProjectSection(SolutionItems) = preProject
10 | .editorconfig = .editorconfig
11 | LICENSE.md = LICENSE.md
12 | README.md = README.md
13 | EndProjectSection
14 | EndProject
15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocumentTranslation.GUI", "DocumentTranslation.GUI\DocumentTranslation.GUI.csproj", "{23830242-C648-49B3-97D4-233775C218C5}"
16 | EndProject
17 | Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "DocumentTranslation.Setup", "DocumentTranslation.Setup\DocumentTranslation.Setup.wixproj", "{ED9DEEB6-3E2D-47AC-979D-39294652C3F2}"
18 | EndProject
19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{204E769F-E6F0-441E-B8BA-80BB5B575CD1}"
20 | ProjectSection(SolutionItems) = preProject
21 | docs\index.md = docs\index.md
22 | EndProjectSection
23 | EndProject
24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{C0EB30FE-48F7-443E-AFC8-76EB24108B82}"
25 | ProjectSection(SolutionItems) = preProject
26 | docs\images\AppPrivateEndpoint.png = docs\images\AppPrivateEndpoint.png
27 | docs\images\AzureKeyVault.png = docs\images\AzureKeyVault.png
28 | docs\images\AzureKeyVaultOverview.png = docs\images\AzureKeyVaultOverview.png
29 | docs\images\AzurePrivateEndpoint.png = docs\images\AzurePrivateEndpoint.png
30 | docs\images\connectionstring.png = docs\images\connectionstring.png
31 | docs\images\Glossary.png = docs\images\Glossary.png
32 | docs\images\Running.png = docs\images\Running.png
33 | docs\images\SettingsDialog.png = docs\images\SettingsDialog.png
34 | docs\images\storageaccount1.png = docs\images\storageaccount1.png
35 | docs\images\TextTranslate.png = docs\images\TextTranslate.png
36 | docs\images\TranslateDocuments.png = docs\images\TranslateDocuments.png
37 | docs\images\translatoraccount.png = docs\images\translatoraccount.png
38 | docs\images\translatorkey.png = docs\images\translatorkey.png
39 | EndProjectSection
40 | EndProject
41 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocumentTranslationService", "DocumentTranslationService\DocumentTranslationService.csproj", "{AABFD3BB-CC1E-4AA7-B792-72A1BC83A155}"
42 | EndProject
43 | Global
44 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
45 | Debug|Any CPU = Debug|Any CPU
46 | Debug|x86 = Debug|x86
47 | Release|Any CPU = Release|Any CPU
48 | Release|x86 = Release|x86
49 | EndGlobalSection
50 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
51 | {FEE35A76-74A5-4321-AFC2-961AA1B29636}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
52 | {FEE35A76-74A5-4321-AFC2-961AA1B29636}.Debug|Any CPU.Build.0 = Debug|Any CPU
53 | {FEE35A76-74A5-4321-AFC2-961AA1B29636}.Debug|x86.ActiveCfg = Debug|Any CPU
54 | {FEE35A76-74A5-4321-AFC2-961AA1B29636}.Debug|x86.Build.0 = Debug|Any CPU
55 | {FEE35A76-74A5-4321-AFC2-961AA1B29636}.Release|Any CPU.ActiveCfg = Release|Any CPU
56 | {FEE35A76-74A5-4321-AFC2-961AA1B29636}.Release|Any CPU.Build.0 = Release|Any CPU
57 | {FEE35A76-74A5-4321-AFC2-961AA1B29636}.Release|x86.ActiveCfg = Release|Any CPU
58 | {FEE35A76-74A5-4321-AFC2-961AA1B29636}.Release|x86.Build.0 = Release|Any CPU
59 | {23830242-C648-49B3-97D4-233775C218C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
60 | {23830242-C648-49B3-97D4-233775C218C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
61 | {23830242-C648-49B3-97D4-233775C218C5}.Debug|x86.ActiveCfg = Debug|Any CPU
62 | {23830242-C648-49B3-97D4-233775C218C5}.Debug|x86.Build.0 = Debug|Any CPU
63 | {23830242-C648-49B3-97D4-233775C218C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
64 | {23830242-C648-49B3-97D4-233775C218C5}.Release|Any CPU.Build.0 = Release|Any CPU
65 | {23830242-C648-49B3-97D4-233775C218C5}.Release|x86.ActiveCfg = Release|Any CPU
66 | {23830242-C648-49B3-97D4-233775C218C5}.Release|x86.Build.0 = Release|Any CPU
67 | {ED9DEEB6-3E2D-47AC-979D-39294652C3F2}.Debug|Any CPU.ActiveCfg = Debug|x86
68 | {ED9DEEB6-3E2D-47AC-979D-39294652C3F2}.Debug|x86.ActiveCfg = Debug|x86
69 | {ED9DEEB6-3E2D-47AC-979D-39294652C3F2}.Debug|x86.Build.0 = Debug|x86
70 | {ED9DEEB6-3E2D-47AC-979D-39294652C3F2}.Release|Any CPU.ActiveCfg = Release|x86
71 | {ED9DEEB6-3E2D-47AC-979D-39294652C3F2}.Release|x86.ActiveCfg = Release|x86
72 | {ED9DEEB6-3E2D-47AC-979D-39294652C3F2}.Release|x86.Build.0 = Release|x86
73 | {AABFD3BB-CC1E-4AA7-B792-72A1BC83A155}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
74 | {AABFD3BB-CC1E-4AA7-B792-72A1BC83A155}.Debug|Any CPU.Build.0 = Debug|Any CPU
75 | {AABFD3BB-CC1E-4AA7-B792-72A1BC83A155}.Debug|x86.ActiveCfg = Debug|Any CPU
76 | {AABFD3BB-CC1E-4AA7-B792-72A1BC83A155}.Debug|x86.Build.0 = Debug|Any CPU
77 | {AABFD3BB-CC1E-4AA7-B792-72A1BC83A155}.Release|Any CPU.ActiveCfg = Release|Any CPU
78 | {AABFD3BB-CC1E-4AA7-B792-72A1BC83A155}.Release|Any CPU.Build.0 = Release|Any CPU
79 | {AABFD3BB-CC1E-4AA7-B792-72A1BC83A155}.Release|x86.ActiveCfg = Release|Any CPU
80 | {AABFD3BB-CC1E-4AA7-B792-72A1BC83A155}.Release|x86.Build.0 = Release|Any CPU
81 | EndGlobalSection
82 | GlobalSection(SolutionProperties) = preSolution
83 | HideSolutionNode = FALSE
84 | EndGlobalSection
85 | GlobalSection(NestedProjects) = preSolution
86 | {C0EB30FE-48F7-443E-AFC8-76EB24108B82} = {204E769F-E6F0-441E-B8BA-80BB5B575CD1}
87 | EndGlobalSection
88 | GlobalSection(ExtensibilityGlobals) = postSolution
89 | SolutionGuid = {27CBFB4A-74CF-4B6D-AFC3-A99B8989BE17}
90 | EndGlobalSection
91 | EndGlobal
92 |
--------------------------------------------------------------------------------
/DocumentTranslationService/AppSettingsSetter.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Holds the configuration information for the document translation CLI
3 | */
4 |
5 | using System;
6 | using System.Diagnostics;
7 | using System.IO;
8 | using System.Text.Json;
9 |
10 | namespace DocumentTranslationService.Core
11 | {
12 | ///
13 | /// Manage the storage of the application settings
14 | ///
15 | public static class AppSettingsSetter
16 | {
17 | public static event EventHandler SettingsReadComplete;
18 | const string AppName = "Document Translation";
19 | const string AppSettingsFileName = "appsettings.json";
20 |
21 | ///
22 | /// Create JSON string for app settings.
23 | ///
24 | /// JSON for appsettings
25 | public static string GetJson(DocTransAppSettings settings)
26 | {
27 | return JsonSerializer.Serialize(settings, new JsonSerializerOptions { IncludeFields = true, WriteIndented = true });
28 | }
29 |
30 | ///
31 | /// Reads settings from file and from Azure KeyVault, if file settings indicate a key vault
32 | ///
33 | /// File name to read settings from
34 | /// Task
35 | ///
36 | public static DocTransAppSettings Read(string filename = null)
37 | {
38 | string appsettingsJson;
39 | try
40 | {
41 | if (string.IsNullOrEmpty(filename)) filename = GetSettingsFilename();
42 | appsettingsJson = File.ReadAllText(filename);
43 | }
44 | catch (Exception ex)
45 | {
46 | if (ex is FileNotFoundException || ex is DirectoryNotFoundException)
47 | {
48 | DocTransAppSettings settings = new()
49 | {
50 | ConnectionStrings = new Connectionstrings(),
51 | AzureRegion = "global",
52 | TextTransEndpoint = "https://api.cognitive.microsofttranslator.com/",
53 | AzureResourceName = "https://*.cognitiveservices.azure.com/"
54 | };
55 | settings.ConnectionStrings.StorageConnectionString = "DefaultEndpointsProtocol=https;AccountName=*";
56 | return settings;
57 | }
58 | throw;
59 | }
60 | DocTransAppSettings result = JsonSerializer.Deserialize(appsettingsJson, new JsonSerializerOptions { IncludeFields = true });
61 | if (!string.IsNullOrEmpty(result.AzureKeyVaultName))
62 | {
63 | Debug.WriteLine($"Authentication: Using Azure Key Vault {result.AzureKeyVaultName} to read credentials.");
64 | }
65 | else
66 | {
67 | Debug.WriteLine("Authentication: Using appsettings.json file to read credentials.");
68 | SettingsReadComplete?.Invoke(null, EventArgs.Empty);
69 | }
70 | if (string.IsNullOrEmpty(result.AzureRegion)) result.AzureRegion = "global";
71 | return result;
72 | }
73 |
74 | public static void Write(string filename, DocTransAppSettings settings)
75 | {
76 | if (string.IsNullOrEmpty(filename))
77 | {
78 | filename = GetSettingsFilename();
79 | }
80 | try
81 | {
82 | File.WriteAllText(filename, GetJson(settings));
83 | }
84 | catch (IOException)
85 | {
86 | Console.WriteLine("ERROR: Failed writing settings to " + filename);
87 | }
88 | return;
89 | }
90 |
91 | private static string GetSettingsFilename()
92 | {
93 | string filename;
94 | if (OperatingSystem.IsWindows())
95 | {
96 | Directory.CreateDirectory(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + AppName);
97 | filename = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + AppName + Path.DirectorySeparatorChar + AppSettingsFileName;
98 | }
99 | else
100 | {
101 | filename = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + Path.DirectorySeparatorChar + AppName + "_" + AppSettingsFileName;
102 | }
103 |
104 | return filename;
105 | }
106 |
107 |
108 | ///
109 | /// Throws an exception to indicate the missing settings value;
110 | ///
111 | /// The settings object to check on
112 | ///
113 | public static void CheckSettings(DocTransAppSettings settings, bool textOnly = false)
114 | {
115 | if (string.IsNullOrEmpty(settings.SubscriptionKey)) throw new ArgumentException("SubscriptionKey");
116 | if (string.IsNullOrEmpty(settings.AzureRegion)) throw new ArgumentException("AzureRegion");
117 | if (!textOnly)
118 | {
119 | if (string.IsNullOrEmpty(settings.ConnectionStrings.StorageConnectionString)) throw new ArgumentException("StorageConnectionString");
120 | if (string.IsNullOrEmpty(settings.AzureResourceName)) throw new ArgumentException("AzureResourceName");
121 | }
122 | return;
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/DocumentTranslationService/CredentialsException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace DocumentTranslationService.Core
4 | {
5 | public partial class DocumentTranslationService
6 | {
7 | [Serializable]
8 | public class CredentialsException : Exception
9 | {
10 | public CredentialsException() : base() { }
11 | public CredentialsException(string reason) : base(reason) { }
12 | public CredentialsException(string reason, Exception innerException) : base(reason, innerException) { }
13 | }
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/DocumentTranslationService/DocTransAppSettings.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Holds the configuration information for the document translation settings
3 | */
4 |
5 | namespace DocumentTranslationService.Core
6 | {
7 | public class DocTransAppSettings
8 | {
9 | ///
10 | /// Azure Translator resource URI
11 | ///
12 | public string AzureResourceName { get; set; }
13 | ///
14 | /// Hold sthe connection strings.
15 | ///
16 | public Connectionstrings ConnectionStrings { get; set; }
17 | ///
18 | /// The resource key to use.
19 | ///
20 | public string SubscriptionKey { get; set; }
21 | ///
22 | /// Whether to show experimental languages
23 | ///
24 | public bool ShowExperimental { get; set; }
25 | ///
26 | /// The Custom Translator category ID to use.
27 | ///
28 | public string Category { get; set; }
29 | ///
30 | /// Hold the Azure region. Important only for text translation. This is the region ID, not the region friendly name.
31 | ///
32 | public string AzureRegion { get; set; }
33 | ///
34 | /// Holds the URI of the Azure key vault to use instead of local settings.
35 | /// If not null or empty, other secrets and region will be ignored.
36 | ///
37 | public string AzureKeyVaultName { get; set; }
38 | ///
39 | /// Holds the Text Translation Endpoint
40 | ///
41 | public string TextTransEndpoint { get; set; }
42 | ///
43 | /// Holds the string for experimental flights
44 | ///
45 | public string FlightString { get; set; }
46 | public bool UsingKeyVault
47 | {
48 | get
49 | {
50 | if (string.IsNullOrEmpty(AzureKeyVaultName?.Trim())) return false;
51 | else return true;
52 | }
53 | }
54 | public bool UsingProxy
55 | {
56 | get
57 | {
58 | if (string.IsNullOrEmpty(ProxyAddress?.Trim())) return false;
59 | else if (ProxyUseDefaultCredentials) return true;
60 | else return true;
61 | }
62 | }
63 | ///
64 | /// Whether to use user credentials when using a proxy
65 | ///
66 | public bool ProxyUseDefaultCredentials { get; set; }
67 |
68 | ///
69 | /// Proxy server address
70 | ///
71 | public string ProxyAddress { get; set; }
72 | }
73 |
74 | public class Connectionstrings
75 | {
76 | ///
77 | /// Azure storage connection string, copied from the portal.
78 | ///
79 | public string StorageConnectionString { get; set; }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/DocumentTranslationService/DocumentTranslationService.cs:
--------------------------------------------------------------------------------
1 | using Azure.AI.Translation.Document;
2 | using Azure.Storage.Blobs;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | namespace DocumentTranslationService.Core
10 | {
11 | public partial class DocumentTranslationService
12 | {
13 | #region Properties
14 | ///
15 | /// The "Connection String" of the Azure blob storage resource. Get from properties of Azure storage.
16 | ///
17 | public string StorageConnectionString { get; set; } = string.Empty;
18 |
19 | ///
20 | /// Holds the Custom Translator category.
21 | ///
22 | public string Category { get; set; }
23 |
24 | ///
25 | /// Your Azure Translator resource key. Get from properties of the Translator resource
26 | ///
27 | public string SubscriptionKey { get; set; } = string.Empty;
28 |
29 | ///
30 | /// The region of your Translator subscription.
31 | /// Needed only for text translation; can remain empty for document translation.
32 | ///
33 | public string AzureRegion { get; set; }
34 |
35 | ///
36 | /// The Uri of the Azure Document Translation endpoint
37 | ///
38 | public string AzureResourceName { get; set; } = string.Empty;
39 |
40 | ///
41 | /// The URI of the Azure Text Translation endpoint
42 | ///
43 | public string TextTransUri { get; set; } = string.Empty;
44 |
45 | ///
46 | /// Sets the string to be used as a flight
47 | ///
48 | public string FlightString { get; set; } = string.Empty;
49 |
50 | internal BlobContainerClient ContainerClientSource { get; set; }
51 | internal Dictionary ContainerClientTargets { get; set; } = new();
52 |
53 | ///
54 | /// Holds the Azure Http Status to check during the run of the translation
55 | ///
56 | internal Azure.Response AzureHttpStatus;
57 |
58 | public DocumentTranslationOperation DocumentTranslationOperation { get => documentTranslationOperation; set => documentTranslationOperation = value; }
59 |
60 | private DocumentTranslationClient documentTranslationClient;
61 |
62 | private DocumentTranslationOperation documentTranslationOperation;
63 |
64 | private CancellationToken cancellationToken;
65 |
66 | private CancellationTokenSource cancellationTokenSource;
67 |
68 |
69 | #endregion Properties
70 | #region Constants
71 | ///
72 | /// The base URL template for making translation requests.
73 | ///
74 | private const string baseUriTemplate = ".cognitiveservices.azure.com/";
75 | #endregion Constants
76 | #region Methods
77 |
78 | ///
79 | /// Constructor
80 | ///
81 | public DocumentTranslationService(string SubscriptionKey, string AzureResourceName, string StorageConnectionString)
82 | {
83 | this.SubscriptionKey = SubscriptionKey;
84 | this.AzureResourceName = AzureResourceName;
85 | this.StorageConnectionString = StorageConnectionString;
86 | }
87 |
88 | public DocumentTranslationService()
89 | {
90 |
91 | }
92 |
93 | ///
94 | /// Fires when initialization is complete.
95 | ///
96 | public event EventHandler OnInitializeComplete;
97 |
98 | ///
99 | /// Fills the properties with values from the service.
100 | ///
101 | ///
102 | public async Task InitializeAsync()
103 | {
104 | if (string.IsNullOrEmpty(AzureResourceName)) throw new CredentialsException("name");
105 | if (string.IsNullOrEmpty(SubscriptionKey)) throw new CredentialsException("key");
106 | if (string.IsNullOrEmpty(TextTransUri)) TextTransUri = "https://api.cognitive.microsofttranslator.com/";
107 | string DocTransEndpoint;
108 | if (!AzureResourceName.Contains('.')) DocTransEndpoint = "https://" + AzureResourceName + baseUriTemplate;
109 | else DocTransEndpoint = AzureResourceName;
110 | var options = new DocumentTranslationClientOptions();
111 | if (!string.IsNullOrEmpty(FlightString)) options.AddPolicy(new FlightPolicy(FlightString.Trim()), Azure.Core.HttpPipelinePosition.PerCall);
112 | documentTranslationClient = new(new Uri(DocTransEndpoint), new Azure.AzureKeyCredential(SubscriptionKey), options);
113 | List tasks = new()
114 | {
115 | GetDocumentFormatsAsync(),
116 | GetGlossaryFormatsAsync()
117 | };
118 | try
119 | {
120 | await Task.WhenAll(tasks);
121 | await GetLanguagesAsync();
122 | }
123 | catch (CredentialsException ex)
124 | {
125 | throw new CredentialsException(ex.Message, ex);
126 | }
127 | OnInitializeComplete?.Invoke(this, EventArgs.Empty);
128 | }
129 |
130 | ///
131 | /// Retrieve the status of the translation progress.
132 | ///
133 | ///
134 | public async Task CheckStatusAsync()
135 | {
136 | for (int i = 0; i < 3; i++)
137 | {
138 | AzureHttpStatus = await documentTranslationOperation.UpdateStatusAsync(cancellationToken);
139 | if (AzureHttpStatus.IsError)
140 | {
141 | await Task.Delay(300);
142 | continue;
143 | }
144 | return documentTranslationOperation;
145 | }
146 | return null;
147 | }
148 |
149 | ///
150 | /// Cancels an ongoing translation run.
151 | ///
152 | ///
153 | public async Task CancelRunAsync()
154 | {
155 | cancellationTokenSource.Cancel();
156 | await documentTranslationOperation.CancelAsync(cancellationToken);
157 | Azure.Response response = await documentTranslationOperation.UpdateStatusAsync(cancellationToken);
158 | Debug.WriteLine($"Cancellation: {response.Status} {response.ReasonPhrase}");
159 | return response;
160 | }
161 |
162 |
163 | ///
164 | /// Submit the translation request to the Document Translation Service.
165 | ///
166 | /// An object defining the input of what to translate
167 | /// The status ID
168 | public async Task SubmitTranslationRequestAsync(DocumentTranslationInput input)
169 | {
170 | if (String.IsNullOrEmpty(AzureResourceName)) throw new CredentialsException("name");
171 | if (String.IsNullOrEmpty(SubscriptionKey)) throw new CredentialsException("key");
172 | if (String.IsNullOrEmpty(StorageConnectionString)) throw new CredentialsException("storage");
173 | cancellationTokenSource = new();
174 | cancellationToken = cancellationTokenSource.Token;
175 | try
176 | {
177 | documentTranslationOperation = await documentTranslationClient.StartTranslationAsync(input, cancellationToken);
178 | }
179 | catch (Azure.RequestFailedException ex)
180 | {
181 | Debug.WriteLine("Request failed: " + ex.Source + ": " + ex.Message);
182 | throw new Exception(ex.Message);
183 | }
184 | catch (System.InvalidOperationException ex)
185 | {
186 | Debug.WriteLine("Request failed: " + ex.Source + ": " + ex.Message);
187 | throw new Exception(ex.Message);
188 | }
189 | await documentTranslationOperation.UpdateStatusAsync();
190 | Debug.WriteLine("Translation Request submitted. Status: " + documentTranslationOperation.Status);
191 | return documentTranslationOperation.Id;
192 | }
193 |
194 | public async Task> GetFinalResultsAsync()
195 | {
196 | List documentStatuses = new();
197 | Debug.WriteLine("Final results:");
198 | await foreach (DocumentStatusResult document in documentTranslationOperation.GetValuesAsync(cancellationToken))
199 | {
200 | documentStatuses.Add(document);
201 | Debug.WriteLine($"{document.SourceDocumentUri}\t{document.Error}\t{document.Status}\t{document.TranslatedToLanguageCode}");
202 | }
203 | return documentStatuses;
204 | }
205 |
206 | #endregion Methods
207 | }
208 | }
209 |
210 |
--------------------------------------------------------------------------------
/DocumentTranslationService/DocumentTranslationService.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | MIT
6 | 0.0.6
7 | https://github.com/microsofttranslator/documenttranslation
8 | en
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/DocumentTranslationService/FlightPolicy.cs:
--------------------------------------------------------------------------------
1 | using Azure.Core;
2 | using Azure.Core.Pipeline;
3 |
4 | namespace DocumentTranslationService
5 | {
6 | internal class FlightPolicy : HttpPipelineSynchronousPolicy
7 | {
8 | private readonly string[] flightStrings;
9 | ///
10 | /// Sets the string to be used in a flight
11 | ///
12 | /// the string to be used in a flight
13 | public FlightPolicy(string flightString)
14 | {
15 | char[] splitchars = { ',', ';', ' ' };
16 | this.flightStrings = flightString.Split(splitchars);
17 | }
18 |
19 | public override void OnSendingRequest(HttpMessage message)
20 | {
21 | if (message.Request.Method.Method == "POST")
22 | {
23 | foreach (string s in flightStrings)
24 | {
25 | string st = s.Trim();
26 | if (!string.IsNullOrEmpty(st))
27 | message.Request.Uri.AppendQuery("flight", st);
28 | }
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/DocumentTranslationService/GetCapabilities.cs:
--------------------------------------------------------------------------------
1 | using Azure.AI.Translation.Document;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Text.Json;
6 | using System.Threading.Tasks;
7 |
8 | namespace DocumentTranslationService.Core
9 | {
10 | public partial class DocumentTranslationService
11 | {
12 | ///
13 | /// Holds the list of file formats after initial retrieval from Service
14 | ///
15 | public List FileFormats { get; private set; } = new();
16 |
17 | public HashSet Extensions { get; private set; } = new();
18 |
19 | public HashSet GlossaryExtensions { get; private set; } = new();
20 |
21 | public event EventHandler OnFileFormatsUpdate;
22 |
23 | public IReadOnlyList GlossaryFormats { get; private set; }
24 |
25 | public event EventHandler OnGlossaryFormatsUpdate;
26 |
27 | public async Task> GetDocumentFormatsAsync()
28 | {
29 | if (FileFormats?.Count > 0) return FileFormats;
30 | else return await GetFormatsInternal();
31 | }
32 |
33 | private async Task> GetFormatsInternal()
34 | {
35 | if (String.IsNullOrEmpty(AzureResourceName)) throw new CredentialsException("name");
36 | for (int i = 0; i < 3; i++)
37 | {
38 | Azure.Response> result = null;
39 | try
40 | {
41 | result = await documentTranslationClient.GetSupportedDocumentFormatsAsync();
42 | }
43 | catch (Azure.RequestFailedException ex)
44 | {
45 | if (ex.Status == 401 || ex.Status == 403) throw new CredentialsException(ex.Message, ex);
46 | }
47 | catch (System.AggregateException ex)
48 | {
49 | throw new Exception("Unknown host: " + ex.Message, ex);
50 | }
51 |
52 | if (result?.Value.Count > 0)
53 | {
54 | Debug.WriteLine($"GetFormats: Response: {JsonSerializer.Serialize(result, new JsonSerializerOptions() { IncludeFields = true })}");
55 | foreach (var item in result.Value)
56 | {
57 | //Add the file formats and extensions from the service
58 | FileFormats.Add(new LocalFormats.LocalDocumentTranslationFileFormat(item.Format, new List((List)item.FileExtensions)));
59 | foreach (string ext in item.FileExtensions)
60 | {
61 | Extensions.Add(ext.ToLowerInvariant());
62 | }
63 | }
64 | //Add the formats and extensions for the locally provided formats
65 | foreach (var localFormat in LocalFormats.LocalFormats.Formats)
66 | {
67 | LocalFormats.LocalDocumentTranslationFileFormat localDocumentTranslationFileFormat = new(
68 | localFormat.Format,
69 | localFormat.FileExtensions,
70 | localFormat.ConvertToMarkdown,
71 | localFormat.ConvertFromMarkdown
72 | );
73 | FileFormats.Add(localDocumentTranslationFileFormat);
74 | foreach (string ext in localFormat.FileExtensions)
75 | {
76 | Extensions.Add(ext.ToLowerInvariant());
77 | }
78 | }
79 | OnFileFormatsUpdate?.Invoke(this, EventArgs.Empty);
80 | return FileFormats;
81 | }
82 | else
83 | {
84 | Debug.WriteLine("GetFormatsInternal: Get file formats failed.");
85 | await Task.Delay(1000);
86 | }
87 | }
88 | return null;
89 | }
90 |
91 | public async Task> GetGlossaryFormatsAsync()
92 | {
93 | if (GlossaryFormats?.Count > 0) return GlossaryFormats;
94 | else return await GetGlossaryFormatsInternal();
95 | }
96 |
97 | private async Task> GetGlossaryFormatsInternal()
98 | {
99 | for (int i = 0; i < 3; i++)
100 | {
101 | Azure.Response> result = null;
102 | try
103 | {
104 | result = await documentTranslationClient.GetSupportedGlossaryFormatsAsync();
105 | }
106 | catch (Azure.RequestFailedException ex)
107 | {
108 | if (ex.Status == 401 || ex.Status == 403) throw new CredentialsException(ex.Message, ex);
109 | }
110 |
111 | if (result?.Value.Count > 0)
112 | {
113 | Debug.WriteLine($"GetGlossaryFormats: Response: {JsonSerializer.Serialize(result, new JsonSerializerOptions() { IncludeFields = true })}");
114 | GlossaryFormats = result.Value;
115 | foreach (var item in result.Value)
116 | {
117 | foreach (string ext in item.FileExtensions)
118 | {
119 | GlossaryExtensions.Add(ext.ToLowerInvariant());
120 | }
121 | }
122 | OnGlossaryFormatsUpdate?.Invoke(this, EventArgs.Empty);
123 | return GlossaryFormats;
124 | }
125 | else
126 | {
127 | Debug.WriteLine("GetGlossaryFormatsInternal: Get glossary formats failed.");
128 | await Task.Delay(1000);
129 | }
130 | }
131 | return null;
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/DocumentTranslationService/Glossary.cs:
--------------------------------------------------------------------------------
1 | using Azure.AI.Translation.Document;
2 | using Azure.Storage.Blobs;
3 | using Azure.Storage.Sas;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Diagnostics;
7 | using System.IO;
8 | using System.Threading.Tasks;
9 |
10 | namespace DocumentTranslationService.Core
11 | {
12 | ///
13 | /// Holds the glossary and the functions to maintain it.
14 | ///
15 | public class Glossary
16 | {
17 | ///
18 | /// Dictionary of Glossary Filename and various glossary information
19 | /// The string is the file name.
20 | ///
21 | public Dictionary Glossaries { get; private set; } = new();
22 | ///
23 | /// Dictionary of plain Uri glossaries
24 | /// For use with Managed Identity
25 | ///
26 | public Dictionary PlainUriGlossaries { get; private set; }
27 |
28 | ///
29 | /// Holds the Container for the glossary files
30 | ///
31 | private BlobContainerClient containerClient;
32 | private readonly DocumentTranslationService translationService;
33 |
34 | ///
35 | /// Fires when a file submitted as glossary was not used.
36 | ///
37 | public event EventHandler> OnGlossaryDiscarded;
38 |
39 | ///
40 | /// Fires when the upload complete.
41 | /// Returns the number of files uploaded, and the combined size.
42 | ///
43 | public event EventHandler<(int, long)> OnUploadComplete;
44 |
45 | ///
46 | /// Constructor
47 | ///
48 | ///
49 | ///
50 | public Glossary(DocumentTranslationService translationService, List glossaryFiles)
51 | {
52 | if ((glossaryFiles is null) || (glossaryFiles.Count == 0))
53 | {
54 | Glossaries = null;
55 | return;
56 | }
57 | foreach (string file in glossaryFiles)
58 | {
59 | Glossaries.TryAdd(file, null);
60 | }
61 | this.translationService = translationService;
62 | }
63 |
64 | ///
65 | /// Upload the glossary files named in the GlossaryFiles property.
66 | ///
67 | /// The storage connection string to use for container creation
68 | /// The GUID-infused base name to use as the container name
69 | /// Task
70 | /// Serious optimization possible here. The container should be permanent, and upload only changed files, or no files at all, and still use them.
71 | public async Task<(int, long)> UploadAsync(string storageConnectionString, string containerNameBase)
72 | {
73 | //Expand directory
74 | if (Glossaries is null) return (0, 0);
75 | List discards = new();
76 | foreach (var glossary in Glossaries)
77 | {
78 | if (!File.Exists(glossary.Key))
79 | {
80 | Debug.WriteLine($"Glossary file ignored: {glossary.Key}");
81 | discards.Add(glossary.Key);
82 | OnGlossaryDiscarded?.Invoke(this, discards);
83 | Glossaries = null;
84 | return (0, 0);
85 | }
86 | if (File.GetAttributes(glossary.Key) == FileAttributes.Directory)
87 | {
88 | Glossaries.Remove(glossary.Key);
89 | foreach (var file in Directory.EnumerateFiles(glossary.Key))
90 | {
91 | Glossaries.Add(file, null);
92 | }
93 | }
94 | }
95 | //Remove files that don't match the allowed extensions
96 | foreach (var glossary in Glossaries)
97 | {
98 | if (!(translationService.GlossaryExtensions.Contains(Path.GetExtension(glossary.Key))))
99 | {
100 | Glossaries.Remove(glossary.Key);
101 | discards.Add(glossary.Key);
102 | }
103 | }
104 | if (discards is not null)
105 | foreach (string fileName in discards)
106 | {
107 | Debug.WriteLine($"Glossary files ignored: {fileName}");
108 | OnGlossaryDiscarded?.Invoke(this, discards);
109 | }
110 | //Exit if no files are left
111 | if (Glossaries.Count == 0)
112 | {
113 | Glossaries = null;
114 | return (0, 0);
115 | }
116 |
117 | //Create glossary container
118 | Debug.WriteLine("START - glossary container creation.");
119 | BlobContainerClient glossaryContainer = new(storageConnectionString, containerNameBase + "gls");
120 | await glossaryContainer.CreateIfNotExistsAsync();
121 | this.containerClient = glossaryContainer;
122 |
123 | //Do the upload
124 | Debug.WriteLine("START - glossary upload.");
125 | System.Threading.SemaphoreSlim semaphore = new(10); //limit the number of concurrent uploads
126 | PlainUriGlossaries = new(Glossaries);
127 | int fileCounter = 0;
128 | long uploadSize = 0;
129 | List uploads = new();
130 | foreach (var glossary in Glossaries)
131 | {
132 | await semaphore.WaitAsync();
133 | string filename = glossary.Key;
134 | //Use a GUID instead of the file name in the container, because the glossary files in separate local folders might have the same file name.
135 | BlobClient blobClient = new(translationService.StorageConnectionString, glossaryContainer.Name, Guid.NewGuid().ToString() + Path.GetExtension(filename));
136 | uploads.Add(blobClient.UploadAsync(filename, true));
137 | Uri sasUriGlossaryBlob = blobClient.GenerateSasUri(BlobSasPermissions.All, DateTimeOffset.UtcNow + TimeSpan.FromHours(5));
138 | Debug.WriteLine($"Glossary URI: {sasUriGlossaryBlob.AbsoluteUri}");
139 | TranslationGlossary translationGlossary = new(sasUriGlossaryBlob, Path.GetExtension(glossary.Key)[1..].ToUpperInvariant());
140 | Glossaries[glossary.Key] = translationGlossary;
141 | TranslationGlossary plainUriTranslationGlossary = new(blobClient.Uri, Path.GetExtension(glossary.Key)[1..].ToUpperInvariant());
142 | PlainUriGlossaries[glossary.Key] = plainUriTranslationGlossary;
143 | fileCounter++;
144 | uploadSize += new FileInfo(filename).Length;
145 | semaphore.Release();
146 | Debug.WriteLine(String.Format($"Glossary file {filename} uploaded."));
147 | }
148 | await Task.WhenAll(uploads);
149 | Debug.WriteLine($"Glossary: {fileCounter} files, {uploadSize} bytes uploaded.");
150 | OnUploadComplete?.Invoke(this, (fileCounter, uploadSize));
151 | return (fileCounter, uploadSize);
152 | }
153 |
154 | public async Task DeleteAsync()
155 | {
156 | if (Glossaries is not null)
157 | {
158 | try
159 | {
160 | Azure.Response response = await containerClient.DeleteAsync();
161 | return response;
162 | }
163 | catch (Azure.RequestFailedException ex)
164 | {
165 | Debug.WriteLine($"Glossary deletion failed: {containerClient.Name}: {ex.Message}");
166 | }
167 | }
168 | return null;
169 | }
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/DocumentTranslationService/HttpClientFactory.cs:
--------------------------------------------------------------------------------
1 | using DocumentTranslationService.Core;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.ComponentModel;
5 | using System.Linq;
6 | using System.Net;
7 | using System.Net.Http;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 |
11 | namespace DocumentTranslationService
12 | {
13 | public class HttpClientFactory
14 | {
15 | public static HttpClient GetHttpClient()
16 | {
17 | var settings = AppSettingsSetter.Read();
18 |
19 | if (settings.UsingProxy == false)
20 | {
21 | return new HttpClient();
22 | }
23 |
24 | var proxy = new WebProxy
25 | {
26 | UseDefaultCredentials = settings.ProxyUseDefaultCredentials,
27 | };
28 |
29 | if (!string.IsNullOrEmpty(settings.ProxyAddress))
30 | {
31 | proxy.Address = new Uri(settings.ProxyAddress);
32 | }
33 |
34 | var httpClientHandler = new HttpClientHandler { Proxy = proxy };
35 |
36 | return new HttpClient(handler: httpClientHandler, disposeHandler: true);
37 |
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/DocumentTranslationService/InvalidCategoryException.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Text Translation Service Facade
3 | */
4 |
5 | using System;
6 | using System.Runtime.Serialization;
7 |
8 | namespace DocumentTranslationService.Core
9 | {
10 | ///
11 | /// Throws when an invalid categoryID value is encountered
12 | ///
13 | [Serializable]
14 | public class InvalidCategoryException : Exception
15 | {
16 | public InvalidCategoryException()
17 | {
18 | }
19 |
20 | public InvalidCategoryException(string message) : base(message)
21 | {
22 | }
23 |
24 | public InvalidCategoryException(string message, Exception innerException) : base(message, innerException)
25 | {
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/DocumentTranslationService/KeyVaultAccess.cs:
--------------------------------------------------------------------------------
1 | using Azure.Identity;
2 | using Azure.Security.KeyVault.Secrets;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 | using System.Threading.Tasks;
7 |
8 | namespace DocumentTranslationService.Core
9 | {
10 | public class KeyVaultAccess(string keyVaultName)
11 | {
12 | public string KeyVaultName { get; init; } = keyVaultName;
13 |
14 | ///
15 | /// Retrieve the Translator credentials from the key vault
16 | /// Caller should catch the Azure.RequestFailed exception.
17 | ///
18 | /// DocTransAppSettings class
19 | ///
20 | public async Task GetKVCredentialsAsync()
21 | {
22 | string VaultUri;
23 | if (KeyVaultName.Contains('.'))
24 | VaultUri = KeyVaultName;
25 | else
26 | VaultUri = KeyVaultName.EndsWith(".vault.azure.cn")
27 | ? $"https://{KeyVaultName}.vault.azure.cn/"
28 | : $"https://{KeyVaultName}.vault.azure.net/";
29 |
30 | // Configure the authority host for Azure China
31 | var options = new InteractiveBrowserCredentialOptions
32 | {
33 | AuthorityHost = DetermineAuthorityHost(VaultUri, KeyVaultName)
34 | };
35 |
36 | SecretClient client = new(new Uri(VaultUri), new InteractiveBrowserCredential(options));
37 | List secretNames = ["AzureRegion", "DocTransEndpoint", "StorageConnectionString", "ResourceKey", "TextTransEndpoint"];
38 | List>> tasks = [];
39 | Azure.Response[] kvSecrets;
40 |
41 | foreach (string secret in secretNames)
42 | tasks.Add(client.GetSecretAsync(secret));
43 |
44 | try
45 | {
46 | kvSecrets = await Task.WhenAll(tasks);
47 | }
48 | catch (CredentialUnavailableException ex)
49 | {
50 | Debug.WriteLine($"Azure Key Vault: {ex.Message}\nPlease log in to your work or school account.");
51 | throw new KeyVaultAccessException("msg_NotLoggedIn", ex);
52 | }
53 | catch (Azure.RequestFailedException ex)
54 | {
55 | Debug.WriteLine($"Azure Key Vault: {ex.Message}");
56 | throw new KeyVaultAccessException("msg_KeyVaultRequestFailed", ex);
57 | }
58 | catch (Exception ex)
59 | {
60 | Debug.WriteLine($"Azure Key Vault: {ex.Message}");
61 | throw new KeyVaultAccessException("msg_KeyVaultRequestFailed", ex);
62 | }
63 |
64 | DocTransAppSettings settings = new();
65 | foreach (var kvSecret in kvSecrets)
66 | {
67 | switch (kvSecret.Value.Name)
68 | {
69 | case "AzureRegion":
70 | settings.AzureRegion = kvSecret.Value.Value;
71 | break;
72 | case "DocTransEndpoint":
73 | settings.AzureResourceName = kvSecret.Value.Value;
74 | break;
75 | case "StorageConnectionString":
76 | settings.ConnectionStrings ??= new();
77 | settings.ConnectionStrings.StorageConnectionString = kvSecret.Value.Value;
78 | break;
79 | case "ResourceKey":
80 | settings.SubscriptionKey = kvSecret.Value.Value;
81 | break;
82 | case "TextTransEndpoint":
83 | settings.TextTransEndpoint = kvSecret.Value.Value;
84 | break;
85 | default:
86 | break;
87 | }
88 | }
89 | settings.AzureKeyVaultName = KeyVaultName;
90 | return settings;
91 | }
92 |
93 | private static Uri DetermineAuthorityHost(string vaultUri, string keyVaultName)
94 | {
95 | if (vaultUri.EndsWith(".vault.azure.cn", StringComparison.OrdinalIgnoreCase) ||
96 | keyVaultName.Contains("azure.cn", StringComparison.OrdinalIgnoreCase))
97 | {
98 | return AzureAuthorityHosts.AzureChina;
99 | }
100 | else if (vaultUri.EndsWith(".vault.azure.us", StringComparison.OrdinalIgnoreCase) ||
101 | keyVaultName.Contains("azure.us", StringComparison.OrdinalIgnoreCase))
102 | {
103 | return AzureAuthorityHosts.AzureGovernment;
104 | }
105 | else
106 | {
107 | return AzureAuthorityHosts.AzurePublicCloud;
108 | }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/DocumentTranslationService/KeyVaultAccessException.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Text Translation Service Facade
3 | */
4 |
5 | using System;
6 | using System.Runtime.Serialization;
7 |
8 | namespace DocumentTranslationService.Core
9 | {
10 | ///
11 | /// Throws when there is trouble with getting secrets from Azure KeyVault
12 | ///
13 | [Serializable]
14 | public class KeyVaultAccessException : Exception
15 | {
16 | public KeyVaultAccessException()
17 | {
18 | }
19 |
20 | public KeyVaultAccessException(string message) : base(message)
21 | {
22 | }
23 |
24 | public KeyVaultAccessException(string message, Exception innerException) : base(message, innerException)
25 | {
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/DocumentTranslationService/Language.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Net.Http;
5 | using System.Text.Json;
6 | using System.Threading.Tasks;
7 |
8 | namespace DocumentTranslationService.Core
9 | {
10 |
11 | public partial class DocumentTranslationService
12 | {
13 | ///
14 | /// Holds the set of languages. If list is empty, call GetLanguagesAsync first.
15 | ///
16 | public Dictionary Languages { get; private set; } = new();
17 |
18 | ///
19 | /// Fires when the 'Languages' list finished updating.
20 | ///
21 | public event EventHandler OnLanguagesUpdate;
22 | public bool ShowExperimental { get; set; }
23 |
24 | private bool? lastShowExperimental = null;
25 | private string lastLanguage;
26 |
27 | ///
28 | /// Read the set of languages from the service and store in the Languages list.
29 | /// This function can run before any authentication is set up, because the server side does not need authentication for this call.
30 | ///
31 | /// The language you want the language list in. Default is the thread locale
32 | /// Task
33 | public async Task GetLanguagesAsync(string acceptLanguage = null)
34 | {
35 | await GetLanguagesAsyncInternal(acceptLanguage, false);
36 | if (ShowExperimental)
37 | {
38 | Dictionary nonExpLangs = new(Languages);
39 | await GetLanguagesAsyncInternal(acceptLanguage, true);
40 | foreach (var lang in Languages)
41 | if (nonExpLangs.ContainsKey(lang.Key)) lang.Value.Experimental = false;
42 | else lang.Value.Experimental = true;
43 | }
44 | OnLanguagesUpdate?.Invoke(this, EventArgs.Empty);
45 | return;
46 | }
47 |
48 |
49 | ///
50 | /// Read the set of languages from the service and store in the Languages list.
51 | /// This function can run before any authentication is set up, because the server side does not need authentication for this call.
52 | ///
53 | /// The language you want the language list in. Default is the thread locale
54 | /// Whether to show the experimental languages
55 | /// Task
56 | private async Task GetLanguagesAsyncInternal(string acceptLanguage = null, bool showExperimental = false)
57 | {
58 | if (string.IsNullOrEmpty(acceptLanguage)) acceptLanguage = System.Threading.Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName;
59 | //Cut this call short if we have everything and no change in language of the language names, or in the experimental state.
60 | if ((acceptLanguage == lastLanguage) && (showExperimental == lastShowExperimental) && (Languages.Count > 10)) return;
61 | lastLanguage = acceptLanguage;
62 | lastShowExperimental = showExperimental;
63 | string textTransUri = TextTransUri;
64 | for (int i = 0; i < 3; i++) //retry loop
65 | {
66 | HttpRequestMessage request = new()
67 | {
68 | Method = HttpMethod.Get
69 | };
70 | request.Headers.AcceptLanguage.Add(new System.Net.Http.Headers.StringWithQualityHeaderValue(acceptLanguage));
71 | request.RequestUri = showExperimental
72 | ? new Uri($"{textTransUri}/languages?api-version=3.0&scope=translation&flight=experimental")
73 | : new Uri($"{textTransUri}/languages?api-version=3.0&scope=translation");
74 | HttpClient client = HttpClientFactory.GetHttpClient();
75 | HttpResponseMessage response = await client.SendAsync(request);
76 |
77 | if (response.IsSuccessStatusCode)
78 | {
79 | Languages.Clear();
80 | string resultJson = await response.Content.ReadAsStringAsync();
81 | using JsonDocument doc = JsonDocument.Parse(resultJson);
82 | var langprop = doc.RootElement.GetProperty("translation");
83 | foreach (var item in langprop.EnumerateObject())
84 | {
85 | Language langEntry = new(null, null);
86 | var langCode = item.Name;
87 | langEntry.LangCode = langCode;
88 | foreach (var prop in item.Value.EnumerateObject())
89 | {
90 | string n = prop.Name;
91 | string v = prop.Value.GetString();
92 | switch (n)
93 | {
94 | case "name":
95 | langEntry.Name = v;
96 | break;
97 | case "nativeName":
98 | langEntry.NativeName = v;
99 | break;
100 | case "dir":
101 | { if (v == "rtl") langEntry.Bidi = true; else langEntry.Bidi = false; }
102 | break;
103 | default:
104 | break;
105 | }
106 | }
107 | if (!Languages.TryAdd(langCode, langEntry))
108 | Debug.WriteLine($"Duplicate language entry: {langCode}");
109 | }
110 | Debug.WriteLine($"Languages received: {Languages.Count}, Experimental: {showExperimental}");
111 | return;
112 | }
113 | else if (response.StatusCode is System.Net.HttpStatusCode.NotFound or System.Net.HttpStatusCode.Unauthorized)
114 | textTransUri = "https://api.cognitive.microsofttranslator.us/"; //try Azure Gov in case Azure public is blocked.
115 | else await Task.Delay(1000); //wait one seconds before retry
116 | }
117 | return;
118 | }
119 | }
120 |
121 | ///
122 | /// Holds information about a language
123 | ///
124 | public class Language : ICloneable
125 | {
126 | public Language(string langCode, string name)
127 | {
128 | LangCode = langCode;
129 | Name = name;
130 | }
131 |
132 | ///
133 | /// ISO639 language code
134 | ///
135 | public string LangCode { get; set; }
136 | ///
137 | /// Friendly name of the language in the language of the Accept-Language setting
138 | ///
139 | public string Name { get; set; }
140 | ///
141 | /// Name of the language in its own language
142 | ///
143 | public string NativeName { get; set; }
144 | ///
145 | /// Is this a bidirectional language?
146 | ///
147 | public bool Bidi { get; set; }
148 |
149 | ///
150 | /// Indicates whether this language has been selected, for multi-language operations
151 | ///
152 | public bool IsChecked { get; set; }
153 |
154 | ///
155 | /// Is this an experimental language?
156 | ///
157 | public bool Experimental;
158 |
159 | public object Clone()
160 | {
161 | return (Language)MemberwiseClone();
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/DocumentTranslationService/LocalFormats/LocalDocumentTranslationFileFormat.cs:
--------------------------------------------------------------------------------
1 | using Azure.AI.Translation.Document;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace DocumentTranslationService.LocalFormats
9 | {
10 | public delegate string ConvertToMarkdown(string[] input);
11 | public delegate string ConvertFromMarkdown(string input);
12 |
13 | ///
14 | /// Hold the information about a file format, the associated extensions, and how it can be converted to and from Markdown.
15 | ///
16 | public struct LocalDocumentTranslationFileFormat
17 | {
18 | public LocalDocumentTranslationFileFormat(string name, List extensions) : this()
19 | {
20 | Format = name;
21 | FileExtensions = extensions;
22 | ConvertToMarkdown = null;
23 | ConvertFromMarkdown = null;
24 | }
25 |
26 | public LocalDocumentTranslationFileFormat(string name, List extensions, ConvertToMarkdown convertToMarkdown, ConvertFromMarkdown convertFromMarkdown) : this(name, extensions)
27 | {
28 | ConvertToMarkdown = convertToMarkdown;
29 | ConvertFromMarkdown = convertFromMarkdown;
30 | }
31 |
32 | public string Format { get; set; }
33 | public List FileExtensions { get; set; }
34 | public ConvertToMarkdown ConvertToMarkdown { get; set; }
35 | public ConvertFromMarkdown ConvertFromMarkdown { get; set; }
36 | public readonly bool IsLocal => ConvertToMarkdown != null && ConvertFromMarkdown != null;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/DocumentTranslationService/LocalFormats/LocalFormats.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 |
6 | namespace DocumentTranslationService.LocalFormats
7 | {
8 | ///
9 | /// Extensible list of locally converted formats. Add any new formats here
10 | /// Underlying assumption is that the format translated by the service is Markdown.
11 | /// You supply the functions to convert the local format to Markdown and from Markdown.
12 | ///
13 | public static class LocalFormats
14 | {
15 | ///
16 | /// List of formats that can be converted to and from Markdown
17 | /// Add to this list to add additional local file formats.
18 | /// Extension must start with a period and be all lowercase.
19 | ///
20 | public static readonly List Formats = new() { new LocalDocumentTranslationFileFormat()
21 | {
22 | Format = "SubRip",
23 | FileExtensions = new() { ".srt" },
24 | ConvertToMarkdown = SRTMarkdownConverter.ConvertToMarkdown,
25 | ConvertFromMarkdown = SRTMarkdownConverter.ConvertToSRT
26 | } };
27 |
28 | ///
29 | /// Creates a new list of files to process, replacing the original filename where a local converter exists, with a temporary file in MarkDown format
30 | ///
31 | /// Original set of file names
32 | /// List of service-translatable file names
33 | /// No converter found for this file's format
34 | public static List PreprocessSourceFiles(List filenames)
35 | {
36 | List result = new();
37 | foreach (string filename in filenames)
38 | {
39 | if (IsLocalFormat(filename))
40 | {
41 | var format = Formats.Find(f => f.FileExtensions.Contains(Path.GetExtension(filename.ToLowerInvariant())));
42 | if (format.ConvertToMarkdown != null)
43 | {
44 | Debug.WriteLine($"Converting {filename} to Markdown");
45 | string markdown = format.ConvertToMarkdown(File.ReadAllLines(filename));
46 | string newFilename = Path.Combine(Path.GetTempPath(), Path.GetFileName(filename) + ".md");
47 | File.WriteAllText(newFilename, markdown);
48 | result.Add(newFilename);
49 | }
50 | else throw new Exception($"No conversion function found for {filename}, but listed as local format.");
51 | }
52 | else result.Add(filename);
53 | }
54 | return result;
55 | }
56 |
57 | ///
58 | /// After translation, convert from MarkDown back to original format
59 | ///
60 | /// List of filenames, whether they need conversion or not
61 | /// Exception if there is noi conversion function found for this filename extension
62 | public static void PostprocessTargetFiles(List filenames)
63 | {
64 | foreach (string filename in filenames)
65 | {
66 | if (IsLocalFormat(filename, out string originalextension))
67 | {
68 | var format = Formats.Find(f => f.FileExtensions.Contains(originalextension));
69 | if (format.ConvertFromMarkdown != null)
70 | {
71 | Debug.WriteLine($"Converting {filename} from Markdown to {originalextension}");
72 | string markdown = File.ReadAllText(filename);
73 | string newFilename = filename[..filename.LastIndexOf('.')];
74 | File.WriteAllText(newFilename, format.ConvertFromMarkdown(markdown));
75 | //#if !DEBUG
76 | //Delete the temporary files
77 | File.Delete(filename);
78 | File.Delete(Path.Combine(Path.GetTempPath(), Path.GetFileName(filename)));
79 | //#endif
80 | }
81 | else throw new Exception($"No conversion function found for {filename}, but listed as local format.");
82 | }
83 | }
84 | }
85 |
86 | ///
87 | /// Determine if the file is a locally processed format
88 | ///
89 | /// Single file name
90 | /// TRUE if this is a locally converted file
91 | private static bool IsLocalFormat(string filename)
92 | {
93 | foreach (var format in Formats)
94 | {
95 | if (format.IsLocal)
96 | {
97 | if (format.FileExtensions.Contains(Path.GetExtension(filename.ToLowerInvariant())))
98 | return true;
99 | }
100 | }
101 | return false;
102 | }
103 |
104 |
105 |
106 | ///
107 | /// In post-processing: Determine if the file is a local format, and if so, return the original extension.
108 | ///
109 | /// Post-processed file name
110 | /// Return the original extension
111 | /// True of the original extension indicates local processing was necessary
112 | private static bool IsLocalFormat(string filename, out string originalextension)
113 | {
114 | string _originalextension = GetSubstringBetweenLastTwoPeriods(filename)?.ToLower();
115 | if (string.IsNullOrEmpty(_originalextension))
116 | {
117 | originalextension = null;
118 | return false;
119 | }
120 | foreach (var format in Formats)
121 | {
122 | if (format.IsLocal)
123 | {
124 | if (
125 | (Path.GetExtension(filename.ToLowerInvariant()) == ".md") &&
126 | format.FileExtensions.Contains("." + _originalextension)
127 | )
128 | {
129 | originalextension = "." + _originalextension;
130 | return true;
131 | }
132 | else
133 | originalextension = Path.GetExtension(filename.ToLowerInvariant());
134 | return false;
135 | }
136 | }
137 | originalextension = Path.GetExtension(filename.ToLowerInvariant());
138 | return false;
139 | }
140 |
141 |
142 | private static string GetSubstringBetweenLastTwoPeriods(string input)
143 | {
144 | int lastPeriodIndex = input.LastIndexOf('.');
145 | int secondToLastPeriodIndex = input.LastIndexOf('.', lastPeriodIndex - 1);
146 |
147 | if (lastPeriodIndex == -1)
148 | {
149 | // If there is no period, return null.
150 | return null;
151 | }
152 |
153 | if (secondToLastPeriodIndex == -1)
154 | {
155 | // If there is only one period or none, return null.
156 | return null;
157 | }
158 |
159 | int startIndex = secondToLastPeriodIndex + 1;
160 | int length = lastPeriodIndex - startIndex;
161 |
162 | string substring = input.Substring(startIndex, length);
163 | return substring;
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/DocumentTranslationService/LocalFormats/SRTCaptionInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace DocumentTranslationService.LocalFormats
8 | {
9 | ///
10 | /// Holds the attributes of a single caption for SRT/VTT files.
11 | ///
12 | internal struct SRTCaptionInfo
13 | {
14 | ///
15 | /// Holds the sequence number of the caption
16 | /// Sequence numbers in SRT files start at 1
17 | ///
18 | public int SequenceNumber { get; set; }
19 |
20 | ///
21 | /// Holds the time code of the caption
22 | ///
23 | public string TimeCode { get; set; }
24 |
25 | ///
26 | /// Denotes the number of lines in the caption and their lengths
27 | ///
28 | public List StringLengths { get; set; }
29 |
30 | ///
31 | /// Indicates whether the caption is a continuous sentence with the other lines of this caption
32 | ///
33 | public bool Continuous { get; set; }
34 |
35 | public SRTCaptionInfo()
36 | {
37 | SequenceNumber = 0;
38 | TimeCode = string.Empty;
39 | StringLengths = [];
40 | Continuous = true;
41 | }
42 |
43 | public void Clear()
44 | {
45 | SequenceNumber = 0;
46 | TimeCode = string.Empty;
47 | StringLengths?.Clear();
48 | Continuous = true;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/DocumentTranslationService/LocalFormats/SRTMarkdownConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Collections.Generic;
3 | using System;
4 |
5 | namespace DocumentTranslationService.LocalFormats
6 | {
7 | public static class SRTMarkdownConverter
8 | {
9 | ///
10 | /// Convert SRT to Markdown. The SRT markup is encoded in comments. The encoding itself is a JSON array of a CaptionInfo object in Base64 format.
11 | /// The lines of a single caption are combined into a single line (Microsoft does not combine lines of the markdown, as is required.)
12 | ///
13 | /// The content of an SRT file in string array form.
14 | /// Markdown document with comments
15 | public static string ConvertToMarkdown(string[] srtLines)
16 | {
17 | ///TODO: This currently only combines the lines from a single caption. However, often a sentence continues over multiple captions.
18 | ///It would be better to combine multiple captions and translate as a complete sentence after sent end punc is found, and then split them
19 | ///out again over multiple captions after translation.
20 | string markdownText = string.Empty;
21 | string currentText = string.Empty;
22 | SRTCaptionInfo captionInfo = new();
23 | foreach (string line in srtLines)
24 | {
25 | if (IsSequenceNumber(line, out int sequenceNumber))
26 | {
27 | captionInfo.Clear();
28 | captionInfo.SequenceNumber = sequenceNumber;
29 | continue;
30 | }
31 | else
32 | if (IsTimeCode(line))
33 | {
34 | captionInfo.TimeCode = line;
35 | continue;
36 | }
37 | else
38 | if (string.IsNullOrWhiteSpace(line)) //empty line denotes end of caption
39 | {
40 | markdownText += currentText;
41 | currentText = string.Empty;
42 | markdownText += $"\n\n";
43 | captionInfo.Clear();
44 | continue;
45 | }
46 | //normal caption text
47 | currentText += line + " ";
48 | captionInfo.StringLengths.Add(line.Length);
49 | }
50 | if (captionInfo.SequenceNumber > 1) //write out any unwritten caption info
51 | {
52 | markdownText += $"\n\n";
53 | }
54 | return markdownText;
55 | }
56 |
57 | ///
58 | /// Convert Markdown to SRT. The SRT markup is encoded in comments. The encoding itself is a JSON array of a CaptionInfo object in Base64 format.
59 | ///
60 | /// Text in Markdown format as a string.
61 | /// SRT document
62 | public static string ConvertToSRT(string markdownText)
63 | {
64 | string srtText = string.Empty;
65 | List lines = SplitIntoLines(markdownText);
66 | string currentText = string.Empty;
67 | foreach (string line in lines)
68 | {
69 | if (line.StartsWith("", index) + 3);
100 | if (index >= 1) index--;
101 | if (nextIndex == -1)
102 | {
103 | lines.Add(markdownText[index..].Trim());
104 | break;
105 | }
106 | lines.Add(markdownText[index..nextIndex].Trim());
107 | index = nextIndex + 1;
108 | }
109 | return lines;
110 | }
111 |
112 | ///
113 | /// Receive a string and a list of lengths. Split the string into lines of the given lengths, by using the nearest word break to the length.
114 | /// Search forward and backward from the length to find the nearest word break. Consider punctuation as a word break opportunity.
115 | ///
116 | /// The text to split
117 | /// A list of desired string lengths
118 | ///
119 | private static string SplitByLength(string currentText, List stringLengths)
120 | {
121 | if (string.IsNullOrEmpty(currentText)) return string.Empty;
122 | if (stringLengths.Count <= 1) return currentText + "\n";
123 | string result = string.Empty;
124 | for (int i = 0; i < stringLengths.Count - 1; i++)
125 | {
126 | int length = stringLengths[i];
127 | int forwardIndex = Math.Min(length, currentText.Length);
128 | int backwardIndex = Math.Max(0, currentText.Length - length);
129 | while (forwardIndex < currentText.Length && !char.IsWhiteSpace(currentText[forwardIndex]) && !char.IsPunctuation(currentText[forwardIndex]))
130 | {
131 | forwardIndex++;
132 | }
133 | while (backwardIndex > 0 && !char.IsWhiteSpace(currentText[backwardIndex]) && !char.IsPunctuation(currentText[backwardIndex]))
134 | {
135 | backwardIndex--;
136 | }
137 | int index = Math.Abs(length - forwardIndex) < Math.Abs(length - backwardIndex) ? forwardIndex : backwardIndex;
138 | if (index >= currentText.Length) index = currentText.Length - 1;
139 | result += string.Concat(currentText.AsSpan(0, index), "\n");
140 | currentText = currentText[index..];
141 | if (currentText.Length == 0) break;
142 | if (currentText[0] == ' ') currentText = currentText[1..];
143 | if (currentText.Length <= 2)
144 | {
145 | result += currentText;
146 | break;
147 | }
148 | }
149 | return string.Concat(result, currentText, "\n");
150 | }
151 |
152 |
153 |
154 | ///
155 | /// Determine if a line is a SRT style time code. Time codes are in the format hh:mm:ss,fff --> hh:mm:ss,fff
156 | ///
157 | /// A line of the SRT file.
158 | /// Whether the line contains an SRT style time code.
159 | private static bool IsTimeCode(string line)
160 | {
161 | if (!line.Contains("-->")) return false;
162 | ///This pattern is meant to match the time code format of SRT files and of VTT files.
163 | ///VTT uses the US decimal separator and allows hours to be optional
164 | string timeCodePattern = @"(\d{2}:){1,2}\d{2}[,.]\d{3}\s-->\s(\d{2}:){1,2}\d{2}[,.]\d{3}";
165 | return System.Text.RegularExpressions.Regex.IsMatch(line, timeCodePattern, System.Text.RegularExpressions.RegexOptions.Compiled);
166 | }
167 |
168 | ///
169 | /// Determine if a line is a sequence number. SRT style sequence numbers are one integer on a line by itself.
170 | ///
171 | /// A line of the SRT file
172 | /// If there is a single number on the line, return it.
173 | ///
174 | private static bool IsSequenceNumber(string line, out int sequenceNumber)
175 | {
176 | return int.TryParse(line, out sequenceNumber);
177 | }
178 |
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/DocumentTranslationService/LocalFormats/StringCompression.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Compression;
4 | using System.Text;
5 |
6 | ///Credit: https://stackoverflow.com/questions/7343465/compression-decompression-string-with-c-sharp
7 |
8 | namespace DocumentTranslationService.LocalFormats
9 | {
10 | public static class StringCompression
11 | {
12 | ///
13 | /// Compresses a string and returns a deflate compressed, Base64 encoded string.
14 | ///
15 | /// String to compress
16 | public static string Compress(string uncompressedString)
17 | {
18 | byte[] compressedBytes;
19 |
20 | using (var uncompressedStream = new MemoryStream(Encoding.UTF8.GetBytes(uncompressedString)))
21 | {
22 | using var compressedStream = new MemoryStream();
23 | // setting the leaveOpen parameter to true to ensure that compressedStream will not be closed when compressorStream is disposed
24 | // this allows compressorStream to close and flush its buffers to compressedStream and guarantees that compressedStream.ToArray() can be called afterward
25 | // although MSDN documentation states that ToArray() can be called on a closed MemoryStream, I don't want to rely on that very odd behavior should it ever change
26 | using (var compressorStream = new DeflateStream(compressedStream, CompressionLevel.Fastest, true))
27 | {
28 | uncompressedStream.CopyTo(compressorStream);
29 | }
30 |
31 | // call compressedStream.ToArray() after the enclosing DeflateStream has closed and flushed its buffer to compressedStream
32 | compressedBytes = compressedStream.ToArray();
33 | }
34 |
35 | return Convert.ToBase64String(compressedBytes);
36 | }
37 |
38 | ///
39 | /// Decompresses a deflate compressed, Base64 encoded string and returns an uncompressed string.
40 | ///
41 | /// String to decompress.
42 | public static string Decompress(string compressedString)
43 | {
44 | byte[] decompressedBytes;
45 |
46 | var compressedStream = new MemoryStream(Convert.FromBase64String(compressedString));
47 |
48 | using (var decompressorStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
49 | {
50 | using var decompressedStream = new MemoryStream();
51 | decompressorStream.CopyTo(decompressedStream);
52 |
53 | decompressedBytes = decompressedStream.ToArray();
54 | }
55 |
56 | return Encoding.UTF8.GetString(decompressedBytes);
57 | }
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/DocumentTranslationService/Logger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 |
5 | namespace DocumentTranslationService.Core
6 | {
7 | public class Logger
8 | {
9 | private const string AppName = "Document Translation";
10 | private const string logFileName = "doctrLog.txt";
11 | private readonly StreamWriter streamWriter;
12 | private readonly string filename;
13 |
14 | internal Logger()
15 | {
16 | filename = Path.GetTempFileName();
17 | streamWriter = File.CreateText(filename);
18 | streamWriter.AutoFlush = true;
19 | }
20 |
21 | ~Logger()
22 | {
23 | Close();
24 | }
25 |
26 | internal void WriteLine(string line)
27 | {
28 | Debug.WriteLine(line);
29 | streamWriter.WriteLine($"{DateTime.Now}: {line}");
30 | }
31 | internal void WriteLine()
32 | {
33 | streamWriter.WriteLine();
34 | }
35 |
36 | internal void Close()
37 | {
38 | string finalfilename = GetFinalFilename();
39 | if (File.Exists(filename))
40 | try
41 | {
42 | streamWriter.Close();
43 | File.Copy(filename, finalfilename, true);
44 | File.Delete(filename);
45 | }
46 | catch { };
47 | }
48 |
49 | private static string GetFinalFilename()
50 | {
51 | string finalfilename;
52 | if (OperatingSystem.IsWindows())
53 | {
54 | Directory.CreateDirectory(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + AppName);
55 | finalfilename = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + AppName + Path.DirectorySeparatorChar + logFileName;
56 | }
57 | else
58 | {
59 | finalfilename = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + Path.DirectorySeparatorChar + AppName + "_" + Path.DirectorySeparatorChar + logFileName;
60 | }
61 | return finalfilename;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/DocumentTranslationService/StatusResponse.cs:
--------------------------------------------------------------------------------
1 | using Azure.AI.Translation.Document;
2 |
3 | namespace DocumentTranslationService.Core
4 | {
5 | #region Helperclasses
6 |
7 | public class StatusResponse
8 | {
9 | public DocumentTranslationOperation Status;
10 | public string Message;
11 |
12 | public StatusResponse(DocumentTranslationOperation documentTranslationOperation, string message = null)
13 | {
14 | Status = documentTranslationOperation;
15 | Message = message;
16 | }
17 | }
18 | #endregion Helperclasses
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/DocumentTranslationService/TestCredentials.cs:
--------------------------------------------------------------------------------
1 | using Azure.Storage.Blobs;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Net.Http;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace DocumentTranslationService.Core
9 | {
10 | partial class DocumentTranslationService
11 | {
12 |
13 | ///
14 | /// Validate the credentials supplied as properties, and throw a CredentialsExceptions for the failing ones.
15 | /// This is meant to be lightweight, but it is not free in terms of time and resources: The method makes
16 | /// the simplest possible test call to observe the result.
17 | /// Use this in a try block and check the value of the Exception to see which credential failed.
18 | ///
19 | ///
20 | ///
21 | ///
22 | ///
23 | public async Task TryCredentials()
24 | {
25 | List credTestTasks = new()
26 | {
27 | //Test the resource key
28 | TryCredentialsKey(SubscriptionKey, AzureRegion, TextTransUri),
29 | //Test the name of the resource
30 | TryCredentialsName(),
31 | //Test the storage account
32 | TryCredentialsStorage()
33 | };
34 | await Task.WhenAll(credTestTasks);
35 | //Test for free subscription
36 | await TryPaidSubscription();
37 | }
38 |
39 | private async Task TryCredentialsStorage()
40 | {
41 | string containerNameBase = "doctr" + Guid.NewGuid().ToString();
42 | try
43 | {
44 | BlobContainerClient testContainer = new(StorageConnectionString, containerNameBase + "test");
45 | await testContainer.CreateAsync();
46 | await testContainer.DeleteIfExistsAsync();
47 | }
48 | catch (Exception ex)
49 | {
50 | throw new CredentialsException("Storage: " + ex.Message, ex);
51 | }
52 | }
53 |
54 | private async Task TryCredentialsName()
55 | {
56 | string DocTransEndpoint;
57 | if (!AzureResourceName.Contains('.')) DocTransEndpoint = "https://" + AzureResourceName + baseUriTemplate;
58 | else DocTransEndpoint = AzureResourceName;
59 | try
60 | {
61 | HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = new Uri(DocTransEndpoint + "/documents/formats") };
62 | HttpClient client = new();
63 | HttpResponseMessage response;
64 | response = await client.SendAsync(request);
65 | }
66 | catch (HttpRequestException ex)
67 | {
68 | throw new CredentialsException("Document Translation Endpoint: " + ex.Message, ex);
69 | }
70 | catch (System.UriFormatException ex)
71 | {
72 | throw new CredentialsException("Document Translation Endpoint: " + ex.Message, ex);
73 | }
74 | catch (Exception ex)
75 | {
76 | throw new CredentialsException("Document Translation Endpoint: " + ex.Message, ex);
77 | }
78 | }
79 |
80 | private static async Task TryCredentialsKey(string subscriptionKey, string azureRegion, string TextTransUri)
81 | {
82 | if (string.IsNullOrEmpty(TextTransUri)) TextTransUri = "https://api.cognitive.microsofttranslator.com";
83 | using HttpRequestMessage request = new() { Method = HttpMethod.Post, RequestUri = new Uri(TextTransUri + "/detect?api-version=3.0") };
84 | request.Headers.Add("Ocp-Apim-Subscription-Key", subscriptionKey);
85 | if (azureRegion?.ToLowerInvariant() != "global") request.Headers.Add("Ocp-Apim-Subscription-Region", azureRegion);
86 | request.Content = new StringContent("[{ \"Text\": \"English\" }]", Encoding.UTF8, "application/json");
87 | HttpClient client = HttpClientFactory.GetHttpClient();
88 | try
89 | {
90 | client.Timeout = TimeSpan.FromSeconds(15);
91 | HttpResponseMessage response = await client.SendAsync(request);
92 | if (!response.IsSuccessStatusCode)
93 | throw new CredentialsException("Invalid key, or key does not match region.");
94 | }
95 | catch (Exception ex)
96 | {
97 | throw new CredentialsException("Text Translation Endpoint: " + ex.Message, ex);
98 | }
99 | }
100 |
101 | private async Task TryPaidSubscription()
102 | {
103 | string DocTransEndpoint;
104 | if (!AzureResourceName.Contains('.')) DocTransEndpoint = "https://" + AzureResourceName + baseUriTemplate;
105 | else DocTransEndpoint = AzureResourceName;
106 | Azure.AI.Translation.Document.DocumentTranslationClient documentTranslationClient = new(new Uri(DocTransEndpoint), new Azure.AzureKeyCredential(SubscriptionKey));
107 |
108 | try
109 | {
110 | var result = await documentTranslationClient.GetSupportedDocumentFormatsAsync();
111 | }
112 | catch (Azure.RequestFailedException ex)
113 | {
114 | throw new CredentialsException("Subscription Type: " + ex.Message, ex);
115 | }
116 | catch (Exception ex)
117 | {
118 | throw new CredentialsException("Subscription Type: " + ex.Message, ex);
119 | }
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Microsoft Document Translation
2 |
3 | Copyright (c) Microsoft Corporation
4 |
5 | All rights reserved.
6 |
7 | ## MIT License
8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11 |
12 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13 |
14 |
15 | ## Third Party Notices
16 |
17 | This project uses:
18 |
19 | Command Line Utils - Nate McMaster
20 |
21 | Under the Apache license: https://github.com/natemcmaster/CommandLineUtils/blob/main/LICENSE.txt
22 |
23 | Available from: https://github.com/natemcmaster/CommandLineUtils
24 |
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Microsoft Document Translation
2 |
3 | Translate local files or network files in many different formats, to more than 100 different languages.
4 | Supported formats include HTML, PDF, all Office document formats, Markdown, MHTML, Outlook .MSG, XLIFF, CSV, TSV and plain text.
5 | The complete [list of document formats is here](https://docs.microsoft.com/azure/cognitive-services/translator/document-translation/overview#supported-document-formats).
6 |
7 | You can select up to 1000 files and translate them to one or more different languages with a single command.
8 | The Windows UI gives you options to comfortably select source files, one or more target languages, and the folder you want to deposit the translations in.
9 | It comes with a command line utility that does the same thing using a command line interface.
10 | Document Translation uses the Azure Translator Service to perform the translations. You need a subscription to Azure, and register
11 | a Translator resource as well as a storage resource. [The documentation](https://microsofttranslator.github.io/DocumentTranslation) gives
12 | detailed instructions on how to obtain those.
13 |
14 | For the translation you can specify a glossary (custom dictionary) to use. You can also make use of a custom translation system
15 | you may have built with [Custom Translator](http://customtranslator.ai).
16 |
17 | You can manage the credentials for accessing the Azure services in Azure Key Vault - the app will read it from there,
18 | based on your identity. Good if you want to manage the credentials centrally.
19 |
20 | Works with Azure sovereign clouds.
21 |
22 | **Document Translation UI**
23 |
24 | The main UI provides document translation: Multiple documents to multiple languages.
25 |
26 | 
27 |
28 |
29 | **Text Translation UI**
30 |
31 | A simple copy-and-paste text translation interface is present in the Windows UI.
32 |
33 | 
34 |
35 | ## Download
36 |
37 | A Windows installer (.MSI) and signed binaries (.ZIP) for manual installation on other OSes are provided in
38 | the [releases folder](https://github.com/microsofttranslator/documenttranslation/releases).
39 |
40 | ## Documentation
41 |
42 | See the [complete documentation of the tool](https://microsofttranslator.github.io/DocumentTranslation).
43 |
44 | The documentation is stored in the /docs folder of the project.
45 |
46 | ## Implementation
47 |
48 | Document Translation is written and compiled for .Net 6. The command line utility should be compatible with other platforms
49 | running .Net 6, namely MacOS and Linux. Tested on Windows 10, Windows 11 and Mac OS X at this point. Please let us know via an issue
50 | if you find problems with other platforms running .Net 6.
51 | Signed binaries are provided in the [releases](https://github.com/microsofttranslator/documenttranslation/releases) folder.
52 | To compile yourself, run Visual Studio 2022 and have the .Net 6 SDK installed.
53 | You can compile and run the tool in Visual Studio 2022.
54 |
55 | This tool makes use of the Azure Document Translation service. The Azure Document Translation service translates
56 | a set of documents that reside in an Azure storage container, and delivers the translations in another Azure storage
57 | container. This app provides a local interface to that service, allowing you to translate a locally residing file
58 | or a folder, and receiving the translation of these documents in a local folder.
59 | The tool uploads the local documents, invokes the translation, monitors the translation progress,
60 | downloads the translated documents to your local machine, and then deletes the containers from the service.
61 | Each run is independent of each other by giving the containers it uses a unique name within the common storage account.
62 | Multiple people may run translations concurrently, using the same credentials and the same storage account.
63 |
64 | Project "doctr" contains the command line processing based on Nate McMaster's Command Line Utilities. All user interaction
65 | is handled here.
66 | Project 'DocumentTranslationService' contains three relevant classes: DocumentTranslationService handles all the interaction
67 | with the Azure service.
68 | DocumentTranslationBusiness handles the local file operations and business logic.
69 | Class 'Glossary' handles the upload of the glossary, when a glossary is specified.
70 |
71 | Works with Azure sovereign clouds. The app accepts fully qualified service endpoints.
72 |
73 | ## Privacy and Security
74 |
75 | This client side app is a lightweight frontend to the Azure Document Translation service.
76 | The Azure Document Translation service uses Azure Blob Storage to read the documents to be translated from and it
77 | deposits the translated documents into Azure Blob Storage. All processing and storage is within the user-provided
78 | accounts. This app does not have its own Azure credentials; user supplies the identities and authentication for Azure services.
79 | In a successful run, the app uploads the user-suplied local documents to Azure Blob Storage in user's Azure account,
80 | initiates the translation, waits for completion, downloads the translated documents to the user-specified location,
81 | and then deletes all original and translated copies of the documents from Azure Blob Storage.
82 | In an unsuccessful run the deletion may be skipped, leaving abandoned storage containers behind. On average every 10th run of the app
83 | will automatically delete any left-behind storage containers that are older than one week.
84 | The command line command `doctr clear` forces a deletion of storage containers older than one week. For a faster deletion of
85 | storage containers, user will have to perform the deletion manually within the Azure storage account. A faster deletion has the
86 | chance to disrupt the translation runs of other users using the same Azure credentials.
87 |
88 | The Azure privacy statement applies.
89 |
90 | The app stores the Azure credentials in a settings file in JSON format, unencrypted, in the user's app settings folder:
91 | `C:\Users\\AppData\Roaming\Document Translation` as `appsettings.json` on Windows, and in the `/usr/` folder on MacOS
92 | and other Unix flavors.
93 | To avoid storing any Azure credentials on the client, please use the Azure Key Vault. In this case only the URL to the customer's
94 | Key Vault is stored in the user's settings file. Other Azure credentials are stored in the user's key vault. See the Key Vault section
95 | in the Document Translator documentation.
96 | The app stores UI settings (not credentials) in the `uisettings.json` file in the user settings folder. It stores a log of the last
97 | run in `docTrLog.txt` in the same folder. It stores references to custom translation systems in `CustomCategories.json`,
98 | also in the same folder.
99 |
100 | The app does not create any local copies of the original or translated documents, not even temporary,
101 | EXCEPT in the case of document formats that are locally supplied. As of September 2023 the locally supplied formats
102 | are SubRIP (SRT) and WebVTT (VTT) formats. The app will create a temporary file in the user's temp folder, storing
103 | the content of the file to be translated in MarkDown format. It will create a temporary file in the MarkDown format in
104 | the target folder for translated documents. A successful run of the translation will delete the temporary files.
105 | While converting between the local format and MarkDown, the app processes the content of the file in memory.
106 |
107 | The app does not include any telemetry or instrumentation. It does not report usage to any service.
108 |
109 |
110 | ## Issues
111 |
112 | Please submit an issue for questions or comments, and of course for any bug or problem you encouter
113 | [here](https://github.com/MicrosoftTranslator/DocumentTranslation/issues).
114 |
115 | ## Contributions
116 | Please contribute your bug fix and functionality additions. Submit a pull request. We will review and integrate
117 | quickly - or reject with comments.
118 |
119 | ## Future plans
120 |
121 | - Option to extend the set of file formats with format conversions that are processed locally, as a library within this tool.
122 | - Web interface with .Net 6 MAUI
123 | - A shared storage for the glossary, so that multiple clients can refer to a
124 | single company-wide glossary.
125 |
126 |
127 | ## Credits
128 | The tool uses following Nuget packages:
129 | - Nate McMaster's Command Line Utilities for the CLI command and options processing.
130 | - Azure.Storage.Blobs for the interaction with the Azure storage service.
131 | - Azure.AI.Translation.Document, a client library for the Azure Document Translation Service
132 | - Azure.Identity for authentication to Key Vault
133 | - Azure.Security.KeyVault.Secrets for reading the credentials from Azure Key Vault
134 |
135 | Our sincere thanks to the authors of these packages.
136 |
--------------------------------------------------------------------------------
/docs/images/AppPrivateEndpoint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/AppPrivateEndpoint.png
--------------------------------------------------------------------------------
/docs/images/AzureKeyVault.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/AzureKeyVault.png
--------------------------------------------------------------------------------
/docs/images/AzureKeyVaultOverview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/AzureKeyVaultOverview.png
--------------------------------------------------------------------------------
/docs/images/AzurePrivateEndpoint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/AzurePrivateEndpoint.png
--------------------------------------------------------------------------------
/docs/images/Glossary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/Glossary.png
--------------------------------------------------------------------------------
/docs/images/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/Logo.png
--------------------------------------------------------------------------------
/docs/images/Running.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/Running.png
--------------------------------------------------------------------------------
/docs/images/SettingsDialog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/SettingsDialog.png
--------------------------------------------------------------------------------
/docs/images/TextTranslate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/TextTranslate.png
--------------------------------------------------------------------------------
/docs/images/TranslateDocuments.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/TranslateDocuments.png
--------------------------------------------------------------------------------
/docs/images/connectionstring.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/connectionstring.png
--------------------------------------------------------------------------------
/docs/images/storageaccount1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/storageaccount1.png
--------------------------------------------------------------------------------
/docs/images/translatoraccount.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/translatoraccount.png
--------------------------------------------------------------------------------
/docs/images/translatorkey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MicrosoftTranslator/DocumentTranslation/cc395704b7c210578279055e829bdff50c4953ad/docs/images/translatorkey.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 |
2 | The Document Translation tool translates documents in your local storage using the Microsoft Azure Translator service.
3 | It can translate Office documents (Word, Excel, PowerPoint), HTML documents, PDF documents, Outlook Messages, Markdown, MHTML,
4 | plain text, RTF and XLIFF files.
5 |
6 | It comes in a Windows UI and a command line interface. The command line interface runs MacOS and Windows, or any system
7 | that a [.Net 6 runtime](https://dotnet.microsoft.com/download/dotnet/6.0) is available for. Tested only on Windows and Mac.
8 |
9 | -------------------
10 | ## Content
11 |
12 | [Running on Windows](#running-on-Windows)
13 |
14 | [Command Line Interface](#command-line-interface)
15 |
16 | [Running on Mac OS X](#running-on-mac-os-x)
17 |
18 | ---------------
19 |
20 | ## Running on Windows
21 |
22 | ### Install
23 | Run the latest **DocumentTranslation.Setup.msi** from [Github releases](https://github.com/MicrosoftTranslator/DocumentTranslation/releases).
24 | It will install the document translation tool on your Windows computer.
25 |
26 | ### Minimum requirements
27 | - An Azure subscription
28 | - A Translator resource with a pricing tier of S1 or higher
29 | - A Blob storage resource in your Azure subscription
30 | - A Windows 10 or later operating system able to run .Net 6.
31 | If installation fails, install .Net 6 manually from https://dotnet.microsoft.com/download/dotnet/6.0.
32 |
33 | #### How to obtain the service credentials
34 |
35 | If you prefer to not maintain the acess secrets on your computer, or if your organization manages the
36 | Azure account for you, you may use Azure Key Vault to manage the credentials.
37 |
38 | ##### Azure Key Vault
39 |
40 | If your organization manages the Azure subscription for you, ask your Azure administrator for the name of
41 | the Azure Key Vault. Enter the URI of the Azure Key Vault in the Settings page.
42 | You don't have to enter any other settings in this case.
43 | If you are an Azure administrator, see [here](#azure-key-vault-administration) how to set up Azure Key Vault.
44 |
45 | If a Key Vault URI is specified, your browser will open and ask you to log in.
46 | If a Key Vault URI is specified, the other entries on this Settings page have no effect.
47 |
48 |
49 | ##### Translator resource key and endpoints
50 |
51 | You can use an existing paid Azure Translator resource. Document translation will not work with a free Translator resource.
52 | If you don't have a Translator resource, create one:
53 |
54 | ------------------------------
55 | 
56 |
57 | Make sure you pick a pricing tier of S1 or higher.
58 |
59 | Visit the properties of your Translator resource.
60 |
61 | -------------------
62 | 
63 |
64 | -------------------
65 | Copy the Key to the "Resource Key" field in the Settings/Authentication tab.
66 |
67 | Copy the Text Translation endpoint to the "Text Translation Endpoint" field in the Settings/Authentication tab.
68 |
69 | Copy the Document Translation endpoint into the "Document Translation" field in the Settings/Authentication tab.
70 |
71 | Enter the "Azure Region" where your Translator resource is located in the Settings/Authentication tab.
72 |
73 | 
74 |
75 | ##### Virtual Network/Private Endpoint
76 |
77 | When using a virtual network or private endpoint, copy the Text Translation endpoint and Document Translation endpoint
78 | from the "Virtual Network" tab of the "Keys and Endpoint" section of the Translator resource.
79 |
80 | 
81 |
82 | Copy this endpoint into the Document Translation app's Settings page and add
83 | `translator/text/v3.0`
84 | to the end of the Text Translation Endpoint URL.
85 |
86 | 
87 |
88 | The same modification to the Text Translation URL entered into Key Vault is necessary when using Key Vault
89 | to manage the credentials with Private Endpoints.
90 |
91 |
92 | ##### Storage connection string
93 | You can use an existing Azure storage account.
94 | If you don't have an Azure storage account, create one:
95 |
96 | ------------------------
97 | 
98 |
99 | -------------------------
100 |
101 | Visit the properties of your storage account.
102 | Copy the entire "Connection String". It is a very long string.
103 |
104 | ------------------
105 | 
106 |
107 | --------------
108 |
109 | Paste this string to the "Storage Connection String" field in the Settings/Authentication tab.
110 |
111 | ##### Azure Key Vault administration
112 |
113 | Ignore this section if you are entering the secrets into the Settings page directly.
114 |
115 | To create a Key Vault for use with Document Translation:
116 | - Set up resources for Translator and for Storage as described above
117 | - Create an Azure Key Vault resource
118 | - Create Secrets for each of
119 | - AzureRegion
120 | - DocTransEndpoint
121 | - ResourceKey
122 | - StorageConnectionString
123 | - TextTransEndpoint
124 |
125 | and copy the **secret from the Translator** resource and the **connection string from the Storage** resource.
126 |
127 | The names of the secrets must be exactly like this (casing is significant),
128 | and the Key Vault resource should look like this:
129 |
130 | 
131 |
132 | In the Access control (IAM) section add the users to the **Key Vault Secrets User** role assignment.
133 |
134 | Provide the users with the **URI** of the Key Vault. In the example the URI is "https://eus2kv.vault.azure.net/".
135 |
136 | 
137 |
138 |
139 | ### Translate Documents
140 |
141 | After you have entered the credentials, you are able to start translating documents.
142 | 
143 |
144 | Select your source and target languages. You may choose "Auto-Detect" as the source language.
145 | Leave Category empty, unless your administrator or your language service provider has told you to use a certain category,
146 | or you have defined your own custom category with [Custom Translator](http://customtranslator.ai).
147 |
148 | Choose your local documents that you want to translate. Pressing "Select" opens a file picker interface. You may pick as many files
149 | to translate as you like, up to 1000.
150 | Additional limits are documented in [API documentation: Limits](https://docs.microsoft.com/azure/cognitive-services/translator/document-translation/get-started-with-document-translation?tabs=csharp#content-limits)
151 |
152 | Choose a folder to store the translated documents in. You may choose any folder on your computer,
153 | or any network folder that you have create and write permissions to.
154 | If you are translating to a single language and have translated to this language before,
155 | the app will suggest using the same folder as last time you translated to this target language.
156 | For the target folder you may enter a '*' where you want the app to place the language code of the target language.
157 | A table of the language codes with their friendly names is in the Languages tab of the help section ('?').
158 |
159 | You may optionally provide a glossary of words or phrases you want to have translated in a specific way. Add only words and phrases that you are sure must
160 | be translated a certain way, and only those that do not translate naturally as intended. This works best for compound nouns like product names or
161 | phrases that shall remain untranslated.
162 |
163 | You can supply a simple mapping file like this, as a TSV (tab separated variables) file:
164 |
165 | 
166 |
167 |
168 | After selecting the languages, one or more documents to translate, and the target location, you are ready to hit "Translate Documents".
169 |
170 | 
171 |
172 | The status bar at the bottom gives an indication of the status of the translation, and whether an error was encountered.
173 |
174 | After the progress bar reaches 100%, you can retrieve your documents in the target location.
175 |
176 |
177 | -----------------------
178 | ## Command Line Interface
179 | The Microsoft Document Translation Command Line Interface gives quick access to document translation functions.
180 | It is a simple program which makes use of the server-side document translation functionality, giving it a client-based
181 | command line interface, allowing you to translate local documents, in any of the the supported file formats. Use `doctr formats`
182 | to list the [available formats](https://docs.microsoft.com/azure/cognitive-services/translator/document-translation/overview#supported-document-formats).
183 | The CLI tool is designed to be used in document workflow automation and in batch processing scripts.
184 |
185 | ### Download
186 | Please download the latest binary from the "Releases" section and extract the content of the zip file to a folder of your choice,
187 | recommended is a folder in your operating system's PATH.
188 |
189 | ### Minimum requirements
190 | - An Azure subscription
191 | - A Translator resource in your Azure subscription
192 | - A Blob storage resource in your Azure subscription
193 | - An Linux, MacOS or Windows operating system able to run .Net 6. The tool is written in .Net 6.0 and able to run on other platforms
194 | that .Net 6.0 is present on. Tested only on Windows and MacOS. Try to run as is. If it fails,
195 | install .Net 6 from https://dotnet.microsoft.com/download/dotnet/6.0.
196 |
197 | #### How to obtain the service credentials
198 |
199 | ##### Translator resource key and name
200 |
201 | You can use an existing Translator resource that you have.
202 | If you don't have a Translator resource, create one:
203 |
204 | ------------------------------
205 | 
206 |
207 | ------------------------------
208 | Visit the properties of your Translator resource.
209 |
210 | -------------------
211 | 
212 |
213 | -------------------
214 |
215 | Copy the key and paste it into the "key" credential. It doesn't matter whether you use key 1 or key 2.
216 | Use `doctr config set --key=` to enter.
217 |
218 | Copy the resource name, in the example "TranslatorText", and paste it into the "name" credential.
219 | Use `doctr config set --name=` to enter.
220 |
221 |
222 | ##### Storage connection string
223 | You can use an existing Azure storage account that you have.
224 | If you don't have an Azure storage account, create one:
225 |
226 | ------------------------
227 | 
228 |
229 | -------------------------
230 |
231 | Visit the properties of your storage account.
232 | Copy the entire "Connection String". It is a very long string.
233 |
234 | ------------------
235 | 
236 |
237 | --------------
238 |
239 | Paste this string to the "Storage connection string" credential.
240 | Use `doctr config set --storage=""` to enter, with the quotes.
241 |
242 | ### Usage
243 | Use `doctr --help` or `doctr --help` to get detailed information about the command.
244 | On a Mac, use `dotnet doctr.dll --help` or `dotnet doctr.dll --help` to get detailed information about the command.
245 |
246 | #### Configure the tool
247 | The configuration contains the credentials for the needed Azure resources:
248 | The minimum needed credentials are
249 | - The resource key to the Translator resource.
250 | - The name of the Translator resource
251 | - A storage connection string.
252 | You can obtain all of these from the Azure portal.
253 |
254 | Command | Required/Optional
255 | ----------------------------|-----------------------------------------
256 | `doctr config --set storage ` | Required |
257 | `doctr config --set key ` | Required |
258 | `doctr config --set name ` | Required |
259 | `doctr config --set category ` | Optional |
260 |
261 | The configuration settings are stored in the file appsettings.json, in the user's roaming app settings folder, typically
262 | C:\Users\\AppData\Roaming\Document Translation
263 | You may edit the file by hand, using a text editor of your choice.
264 |
265 | You can inspect the settings using the following commands:
266 |
267 | Command | Function
268 | ----------------------------|-----------------------------------------
269 | `doctr config list` | List the current configuration settings.
270 | `doctr config test` | Validate the credentials and report which one is failing.
271 |
272 | The test function will always fail with an invalid key message if the Azure key is from a region other than "global".
273 | Document translation will still work, just this test is failing.
274 |
275 | #### List capabilities
276 |
277 | Command | Function
278 | -------------------|---------------------
279 | `doctr languages` | List the available languages. Can be listed before credentials are set.
280 | `doctr formats` | List the file formats available for translation. Requires credentials key, name and storage to be set.
281 | `doctr glossary` | List the glossary formats available for use as glossary. Requires credentials key, name and storage to be set.
282 |
283 | #### Translate
284 |
285 | Command | Function
286 | --------|----------
287 | `doctr translate [] --to ` | Translate a document or the content of a folder to another language.
288 |
289 | If provided, the target folder must be a folder, even if the source document is an individual document. If not provided, the translated document will be placed in a folder
290 | that has the same name as the source folder, plus `.`.
291 |
292 | Optional parameters to the translate command | Function
293 | ---------------------------------------------|----------
294 | `--from ` | The language to translate from. If omitted, the system performs automatic language detection.
295 | `--key ` | This key will override the setting in the appsettings.json file. Use this if you want to avoid storing the key in a settings file.
296 | `--category ` | The custom Translator category ID.
297 | `--glossary ` | The glossaries to use for this run. The glossary contains phrases with a defined translation in a table format.
298 |
299 | #### Clear
300 | If a translation run gets interrupted or fails, it may also fail to clean up after itself and leave behind documents in the storage account.
301 | A repeated run will always use a fresh storage container for its operation. The 'clear' command deletes storage containers from failed or abandoned runs
302 | for all DOCTR runs that are using the storage account you provided in the settings. In order to not disrupt any other runs of the service,
303 | it limits the deletion to containers that are older than one week.
304 |
305 | Command | Function
306 | --------|---------
307 | `doctr clear` | Delete residue from abandoned or failed translation runs in the storage account
308 |
309 |
310 |
311 | ----------------------------
312 |
313 | ## Running on Mac OS X
314 |
315 | At this point point only the command line version runs on the Mac. The GUI version is waiting for .Net 6 MAUI to be released.
316 |
317 | ### Download and install
318 |
319 | - Install .Net 6 runtime for Mac from https://dotnet.microsoft.com/. You do not need the SDK, just the runtime.
320 | Download and install the .Net 6 runtim for the appropriate processor platform of your Mac.
321 |
322 | - Download the zip file in the releases folder and expand to a suitable directory on the Mac HD.
323 |
324 |
325 | ### Command line syntax notes
326 |
327 | The command line syntax is the same as listed [above](#command-line-interface), but instead of `doctr ` you run `dotnet doctr.dll `.
328 |
329 | When pasting the storage connection string in `dotnet doctr.dll config set --storage ""` please make sure you use
330 | quotes around the connection string, otherwise the string will be cut off prematurely.
331 |
332 |
--------------------------------------------------------------------------------