├── .github └── FUNDING.yml ├── .gitignore ├── App.config ├── Images ├── AnalyzeScreen.png ├── Cancel.png ├── LanguagesScreen.png ├── Logo.ico ├── Open.png ├── Restart.png ├── Sort.png ├── ToolsScreen.png ├── Translate.png ├── TranslateResxScreen.png ├── TranslateTextScreen.png └── Update.png ├── LICENSE ├── MainWindow.Designer.cs ├── MainWindow.cs ├── MainWindow.resx ├── Panels ├── AnalyzeControlPanel.Designer.cs ├── AnalyzeControlPanel.cs ├── AnalyzeControlPanel.resx ├── LanguageComboBox.cs ├── TextControlPanel.Designer.cs ├── TextControlPanel.cs ├── TextControlPanel.resx ├── ToolsControlPanel.Designer.cs ├── ToolsControlPanel.cs └── ToolsControlPanel.resx ├── Program.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs ├── Resources.ar-SA.resx ├── Resources.de-DE-hints.xml ├── Resources.de-DE.resx ├── Resources.es-ES.resx ├── Resources.fr-FR.resx ├── Resources.he-IL.resx ├── Resources.ja-JP.resx ├── Resources.nl-NL.resx ├── Resources.pl-PL.resx ├── Resources.pt-BR.resx ├── Resources.resx ├── Resources.zh-CN.resx ├── Settings.Designer.cs └── Settings.settings ├── README.md ├── ResxProvider.cs ├── ResxTranslator.csproj ├── ResxTranslator.sln ├── SettingsProvider.cs ├── StringExtensions.cs ├── Translator.cs └── packages.config /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [stevencohn] 2 | -------------------------------------------------------------------------------- /.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 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Images/AnalyzeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/AnalyzeScreen.png -------------------------------------------------------------------------------- /Images/Cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/Cancel.png -------------------------------------------------------------------------------- /Images/LanguagesScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/LanguagesScreen.png -------------------------------------------------------------------------------- /Images/Logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/Logo.ico -------------------------------------------------------------------------------- /Images/Open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/Open.png -------------------------------------------------------------------------------- /Images/Restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/Restart.png -------------------------------------------------------------------------------- /Images/Sort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/Sort.png -------------------------------------------------------------------------------- /Images/ToolsScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/ToolsScreen.png -------------------------------------------------------------------------------- /Images/Translate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/Translate.png -------------------------------------------------------------------------------- /Images/TranslateResxScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/TranslateResxScreen.png -------------------------------------------------------------------------------- /Images/TranslateTextScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/TranslateTextScreen.png -------------------------------------------------------------------------------- /Images/Update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevencohn/ResxTranslator/63afea829a7d54c9cf4a0ac1a6459c620e47d66a/Images/Update.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Steven 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MainWindow.cs: -------------------------------------------------------------------------------- 1 | //************************************************************************************************ 2 | // Copyright © 2020 Steven M Cohn. All rights reserved. 3 | //************************************************************************************************ 4 | 5 | #pragma warning disable S1066 // Collapsible "if" statements should be merged 6 | 7 | namespace ResxTranslator 8 | { 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Diagnostics; 12 | using System.Drawing; 13 | using System.IO; 14 | using System.Linq; 15 | using System.Text.RegularExpressions; 16 | using System.Threading; 17 | using System.Web; 18 | using System.Windows.Forms; 19 | using System.Xml.Linq; 20 | 21 | 22 | public partial class MainWindow : Form 23 | { 24 | private const string CodePattern = @".+\.(?.+)\.resx"; 25 | private const string FilePattern = @"(?.+)(\.(?.+)){0,1}\.resx"; 26 | private const string NL = "\n"; 27 | 28 | private int strings = 0; 29 | private bool backendUpdate = false; 30 | private CancellationTokenSource cancellation; 31 | 32 | 33 | public MainWindow() 34 | { 35 | InitializeComponent(); 36 | 37 | Width = Math.Min(1700, Screen.FromControl(this).WorkingArea.Width - 600); 38 | Height = Math.Min(1000, Screen.FromControl(this).WorkingArea.Height - 300); 39 | 40 | PopulateLanguages(); 41 | 42 | languageList.Dock = DockStyle.Fill; 43 | logBox.Dock = DockStyle.Fill; 44 | logBox.Visible = true; 45 | 46 | cancelButton.Top = translateButton.Top; 47 | cancelButton.Left = translateButton.Left; 48 | restartButton.Top = translateButton.Top; 49 | restartButton.Left = translateButton.Left; 50 | 51 | var settings = new SettingsProvider(); 52 | var inputPath = settings.Get("inputPath"); 53 | if (string.IsNullOrEmpty(inputPath)) 54 | { 55 | var args = Environment.GetCommandLineArgs(); 56 | if (args.Length > 1) 57 | { 58 | if (File.Exists(args[1])) 59 | { 60 | inputPath = args[1]; 61 | } 62 | 63 | } 64 | } 65 | 66 | if (!string.IsNullOrEmpty(inputPath)) 67 | { 68 | inputBox.Text = inputPath; 69 | } 70 | } 71 | 72 | 73 | private void PopulateLanguages() 74 | { 75 | foreach (var code in Translator.Codes) 76 | { 77 | var name = Translator.GetDisplayName(code); 78 | 79 | codeBox.Items.Add($"{name} [{code}]"); 80 | languageList.Items.Add(name); 81 | } 82 | } 83 | 84 | 85 | private void PrepTabOnSelectedIndexChanged(object sender, EventArgs e) 86 | { 87 | switch (tabs.SelectedIndex) 88 | { 89 | case 2: analyzeControlPanel.SetFilePath(inputBox.Text); break; 90 | case 3: toolsControlPanel.SetFilePath(inputBox.Text); break; 91 | } 92 | } 93 | 94 | 95 | // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 96 | // Translate File 97 | 98 | private void ChangedResxInput(object sender, EventArgs e) 99 | { 100 | var fileOK = inputBox.Text.Length > 0 && File.Exists(inputBox.Text); 101 | 102 | if (fileOK) 103 | { 104 | var match = Regex.Match( 105 | Path.GetFileName(inputBox.Text), CodePattern); 106 | 107 | if (match.Success) 108 | { 109 | var code = match.Groups["code"].Value.ToLower(); 110 | codeBox.SelectedIndex = Translator.Codes.IndexOf(code); 111 | } 112 | else 113 | { 114 | codeBox.SelectedIndex = Translator.Codes.IndexOf("en"); 115 | } 116 | } 117 | 118 | if (sender == inputBox) 119 | { 120 | AutosetLanguages(); 121 | } 122 | 123 | if (sender == inputBox || sender == languageList) 124 | { 125 | if (Translator.Estimate(inputBox.Text, out strings, out var seconds)) 126 | { 127 | var langs = Math.Max(languageList.CheckedIndices.Count, 1); 128 | var span = new TimeSpan(0, 0, seconds * langs); 129 | estimationLabel.Text = strings == 0 130 | ? string.Empty 131 | : $"{strings} strings. Estimated completion in {span}"; 132 | } 133 | } 134 | 135 | if (logBox.Visible) 136 | { 137 | languageList.Visible = true; 138 | logBox.Visible = false; 139 | logBox.Clear(); 140 | } 141 | 142 | translateButton.Enabled = 143 | fileOK && 144 | languageList.CheckedIndices.Count > 0 && 145 | estimationLabel.Text.Length > 0; 146 | } 147 | 148 | 149 | private void AutosetLanguages() 150 | { 151 | backendUpdate = true; 152 | 153 | var regex = new Regex(@"\.(([a-z]{2,3})\-[A-Z]{2})\.resx"); 154 | 155 | var dir = Path.GetDirectoryName(inputBox.Text); 156 | if (Directory.Exists(dir)) 157 | { 158 | for (int i = 0; i < languageList.Items.Count; i++) 159 | { 160 | languageList.Items[0].Checked = false; 161 | } 162 | 163 | var files = Directory.GetFiles(dir, "*.resx") 164 | .Where(f => regex.IsMatch(f)) 165 | .ToList(); 166 | 167 | foreach (var file in files) 168 | { 169 | var match = regex.Match(file); 170 | 171 | // group 2 is the inner capture 172 | var lang = match.Groups[2].Value; 173 | 174 | if (lang == "zh") 175 | { 176 | // group 1 is the outer capture 177 | lang = match.Groups[1].Value; 178 | } 179 | 180 | var index = Translator.Codes.IndexOf(lang); 181 | if (index >= 0) 182 | { 183 | languageList.Items[index].Checked = true; 184 | } 185 | } 186 | 187 | } 188 | 189 | backendUpdate = false; 190 | } 191 | 192 | 193 | private void BrowseFiles(object sender, EventArgs e) 194 | { 195 | if (openFileDialog.ShowDialog() == DialogResult.OK) 196 | { 197 | if (sender == browseFileButton) 198 | { 199 | inputBox.Text = openFileDialog.FileName; 200 | } 201 | } 202 | } 203 | 204 | 205 | private void BrowseFolders(object sender, EventArgs e) 206 | { 207 | if (folderBrowserDialog.ShowDialog() == DialogResult.OK) 208 | { 209 | outputBox.Text = folderBrowserDialog.SelectedPath; 210 | } 211 | } 212 | 213 | 214 | private void CompareChanged(object sender, EventArgs e) 215 | { 216 | estimationLabel.ForeColor = newStringsBox.Checked ? Color.Gray : Color.Black; 217 | } 218 | 219 | 220 | private void CheckedLanguage(object sender, ItemCheckedEventArgs e) 221 | { 222 | if (!backendUpdate) 223 | { 224 | ChangedResxInput(sender, e); 225 | } 226 | } 227 | 228 | 229 | private async void TranslateFile(object sender, EventArgs e) 230 | { 231 | LockControls(); 232 | 233 | translateButton.Visible = false; 234 | cancelButton.Visible = true; 235 | 236 | if (!newStringsBox.Checked) 237 | { 238 | // this is an estimate 239 | progressBar.Maximum = strings * languageList.CheckedIndices.Count; 240 | } 241 | 242 | var inputPath = Path.GetFullPath(inputBox.Text); 243 | var fromCode = Translator.Codes[codeBox.SelectedIndex]; 244 | 245 | var outputDir = outputBox.Text.Length > 0 246 | ? Path.GetFullPath(outputBox.Text) 247 | : Path.GetDirectoryName(inputPath); 248 | 249 | if (!Directory.Exists(outputDir)) 250 | { 251 | Directory.CreateDirectory(outputDir); 252 | } 253 | 254 | string filepath = null; 255 | var match = Regex.Match(inputPath, FilePattern); 256 | if (match.Success) 257 | { 258 | filepath = match.Groups["file"].Value; 259 | } 260 | 261 | if (sortBox.Checked) 262 | { 263 | var root = XElement.Load(inputPath); 264 | ResxProvider.SortData(root); 265 | root.Save(inputPath, SaveOptions.None); 266 | } 267 | 268 | var translator = new Translator(); 269 | cancellation = new CancellationTokenSource(); 270 | 271 | var watch = new Stopwatch(); 272 | watch.Start(); 273 | 274 | foreach (var index in languageList.CheckedIndices) 275 | { 276 | var toCode = Translator.Codes[(int)index]; 277 | 278 | var displayName = Translator.GetDisplayName(toCode); 279 | var cultureName = Translator.GetCultureName(toCode); 280 | var outputFile = $"{filepath}.{cultureName}.resx"; 281 | 282 | statusLabel.Text = $"Translating to {toCode} - {displayName} ({cultureName})"; 283 | 284 | // load source resx for every target language 285 | // this will be translated in memory and stored for each language 286 | var root = XElement.Load(inputPath); 287 | 288 | var data = ResxProvider.CollectStrings(root); 289 | 290 | if (newStringsBox.Checked && File.Exists(outputFile)) 291 | { 292 | data = ResxProvider.CollectNewStrings(data, outputFile); 293 | var span = new TimeSpan(0, 0, (int)(data.Count * 0.1)); 294 | estimationLabel.Text = $"{data.Count} {toCode} strings. Estimated completion in {span}"; 295 | 296 | // count per file 297 | progressBar.Maximum = data.Count; 298 | progressBar.Value = 0; 299 | } 300 | 301 | try 302 | { 303 | Log($"{NL}Translating {data.Count} strings to {toCode}{NL}", Color.Green); 304 | 305 | translator.LoadHints($"{filepath}.{cultureName}-hints.xml"); 306 | 307 | var success = await translator.TranslateResx( 308 | data, fromCode, toCode, cancellation, 309 | (message, color, increment) => 310 | { 311 | Log(message, color); 312 | 313 | if (increment) 314 | { 315 | progressBar.Increment(1); 316 | } 317 | }); 318 | 319 | if (cancellation.IsCancellationRequested) 320 | { 321 | break; 322 | } 323 | 324 | if (success) 325 | { 326 | root = ApplyTranslations(root, data, inputPath, outputFile, out var deleted); 327 | 328 | var hintCount = 0; 329 | if (hintOverrideBox.Checked && translator.Hints.HasElements) 330 | { 331 | hintCount = ResxProvider.MergeHints(root, translator.Hints); 332 | Log($"Merged {hintCount} hints{NL}", Color.Green); 333 | } 334 | 335 | if (data.Count > 0 || hintCount > 0 || deleted > 0) 336 | { 337 | root.Save(outputFile); 338 | Log($"Saved {outputFile}{NL}", Color.Blue); 339 | } 340 | else 341 | { 342 | Log($"No changes{NL}", Color.Blue); 343 | } 344 | } 345 | } 346 | catch (HttpException exc) 347 | { 348 | Log(exc.Message + NL, Color.Red); 349 | } 350 | } 351 | 352 | if (clearBox.Checked && !cancellation.IsCancellationRequested) 353 | { 354 | // we're done with this file so clear the !EDIT markers 355 | var count = Translator.ClearMarkers(inputPath); 356 | if (count > 0) 357 | Log($"Cleared {count} !EDIT markers{NL}"); 358 | } 359 | 360 | cancelButton.Visible = false; 361 | restartButton.Visible = true; 362 | 363 | if (cancellation.IsCancellationRequested) 364 | { 365 | statusLabel.Text = "Cancelled"; 366 | progressBar.Value = progressBar.Maximum; 367 | } 368 | else 369 | { 370 | statusLabel.Text = "Done"; 371 | } 372 | 373 | watch.Stop(); 374 | Log($"ellapsed time {watch.ElapsedMilliseconds}ms{NL}", Color.DarkCyan); 375 | 376 | cancellation.Dispose(); 377 | 378 | var settings = new SettingsProvider(); 379 | settings.Set("inputPath", inputPath); 380 | settings.Save(); 381 | } 382 | 383 | 384 | private void LockControls() 385 | { 386 | languageList.Visible = false; 387 | logBox.Visible = true; 388 | logBox.Clear(); 389 | 390 | inputBox.Enabled = false; 391 | codeBox.Enabled = false; 392 | browseFileButton.Enabled = false; 393 | outputBox.Enabled = false; 394 | browseFolderButton.Enabled = false; 395 | newStringsBox.Enabled = false; 396 | hintOverrideBox.Enabled = false; 397 | sortBox.Enabled = false; 398 | clearBox.Enabled = false; 399 | } 400 | 401 | 402 | private void Log(string message, Color? color = null) 403 | { 404 | if (color == null || color.Equals(Color.Black)) 405 | { 406 | logBox.AppendText(message); 407 | return; 408 | } 409 | 410 | var fore = logBox.SelectionColor; 411 | logBox.SelectionColor = (Color)color; 412 | logBox.AppendText(message); 413 | logBox.SelectionColor = fore; 414 | } 415 | 416 | 417 | private XElement ApplyTranslations( 418 | XElement root, List data, string inputPath, string outputFile, 419 | out int deletedCount) 420 | { 421 | // add or update changes... 422 | 423 | if (newStringsBox.Checked && File.Exists(outputFile)) 424 | { 425 | root = XElement.Load(outputFile); 426 | foreach (var d in data) 427 | { 428 | var element = root.Elements("data") 429 | .FirstOrDefault(e => e.Attribute("name").Value == d.Attribute("name").Value); 430 | 431 | if (element != null) 432 | { 433 | if (element.Element("value") is XElement evalue && 434 | d.Element("value") is XElement dvalue) 435 | { 436 | evalue.Value = dvalue.Value; 437 | } 438 | 439 | // also pick up any changes to the comment 440 | var ecomment = element.Element("comment"); 441 | var dcomment = d.Element("comment"); 442 | if (dcomment != null) 443 | { 444 | if (ecomment != null) 445 | { 446 | // update comment 447 | ecomment.Value = Translator.ClearMarker(dcomment.Value); 448 | } 449 | else 450 | { 451 | // restore missing comment 452 | element.Add(new XElement("comment", 453 | Translator.ClearMarker(dcomment.Value))); 454 | } 455 | } 456 | } 457 | else 458 | { 459 | root.Add(d); 460 | } 461 | } 462 | } 463 | 464 | // remove deleted items... 465 | 466 | var sourceData = XElement.Load(inputPath).Elements("data"); 467 | 468 | var deleted = root.Elements("data") 469 | .Where(e => !sourceData.Any(d => d.Attribute("name").Value == e.Attribute("name").Value)) 470 | .ToList(); 471 | 472 | deleted.ForEach(d => 473 | { 474 | Log($"Deleted {d.Attribute("name").Value}{NL}", Color.DarkRed); 475 | }); 476 | 477 | deletedCount = deleted.Count; 478 | deleted.Remove(); 479 | 480 | if (sortBox.Checked) 481 | { 482 | ResxProvider.SortData(root); 483 | } 484 | 485 | return root; 486 | } 487 | 488 | 489 | private void CancelTranslation(object sender, EventArgs e) 490 | { 491 | cancellation.Cancel(); 492 | cancelButton.Enabled = false; 493 | } 494 | 495 | 496 | private void Restart(object sender, EventArgs e) 497 | { 498 | inputBox.Enabled = true; 499 | codeBox.Enabled = true; 500 | browseFileButton.Enabled = true; 501 | outputBox.Enabled = true; 502 | browseFolderButton.Enabled = true; 503 | newStringsBox.Enabled = true; 504 | hintOverrideBox.Enabled = true; 505 | sortBox.Enabled = true; 506 | clearBox.Enabled = true; 507 | 508 | languageList.Visible = true; 509 | logBox.Visible = false; 510 | logBox.Clear(); 511 | 512 | statusLabel.Text = "Status..."; 513 | progressBar.Value = 0; 514 | 515 | restartButton.Visible = false; 516 | translateButton.Visible = true; 517 | } 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /Panels/AnalyzeControlPanel.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace ResxTranslator.Panels 2 | { 3 | partial class AnalyzeControlPanel 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Component Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AnalyzeControlPanel)); 32 | this.introLabel = new System.Windows.Forms.Label(); 33 | this.logbox = new System.Windows.Forms.RichTextBox(); 34 | this.cmdPanel = new System.Windows.Forms.Panel(); 35 | this.analyzeButton = new System.Windows.Forms.Button(); 36 | this.sourceBox = new System.Windows.Forms.TextBox(); 37 | this.browseButton = new System.Windows.Forms.Button(); 38 | this.sourceLabel = new System.Windows.Forms.Label(); 39 | this.introPanel = new System.Windows.Forms.Panel(); 40 | this.openFileDialog = new System.Windows.Forms.OpenFileDialog(); 41 | this.cmdPanel.SuspendLayout(); 42 | this.introPanel.SuspendLayout(); 43 | this.SuspendLayout(); 44 | // 45 | // introLabel 46 | // 47 | this.introLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); 48 | this.introLabel.AutoSize = true; 49 | this.introLabel.ImeMode = System.Windows.Forms.ImeMode.NoControl; 50 | this.introLabel.Location = new System.Drawing.Point(6, 26); 51 | this.introLabel.Name = "introLabel"; 52 | this.introLabel.Size = new System.Drawing.Size(551, 20); 53 | this.introLabel.TabIndex = 11; 54 | this.introLabel.Text = "Results - entries with either !NODUP or !SKIP in their comments will be ignored"; 55 | // 56 | // logbox 57 | // 58 | this.logbox.Dock = System.Windows.Forms.DockStyle.Fill; 59 | this.logbox.Location = new System.Drawing.Point(0, 106); 60 | this.logbox.Margin = new System.Windows.Forms.Padding(0); 61 | this.logbox.Name = "logbox"; 62 | this.logbox.Size = new System.Drawing.Size(1013, 651); 63 | this.logbox.TabIndex = 12; 64 | this.logbox.Text = ""; 65 | // 66 | // cmdPanel 67 | // 68 | this.cmdPanel.Controls.Add(this.analyzeButton); 69 | this.cmdPanel.Controls.Add(this.sourceBox); 70 | this.cmdPanel.Controls.Add(this.browseButton); 71 | this.cmdPanel.Controls.Add(this.sourceLabel); 72 | this.cmdPanel.Dock = System.Windows.Forms.DockStyle.Top; 73 | this.cmdPanel.Location = new System.Drawing.Point(0, 0); 74 | this.cmdPanel.Name = "cmdPanel"; 75 | this.cmdPanel.Size = new System.Drawing.Size(1013, 55); 76 | this.cmdPanel.TabIndex = 10; 77 | // 78 | // analyzeButton 79 | // 80 | this.analyzeButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); 81 | this.analyzeButton.BackgroundImage = global::ResxTranslator.Properties.Resources.Translate; 82 | this.analyzeButton.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center; 83 | this.analyzeButton.Enabled = false; 84 | this.analyzeButton.ImeMode = System.Windows.Forms.ImeMode.NoControl; 85 | this.analyzeButton.Location = new System.Drawing.Point(950, 5); 86 | this.analyzeButton.Name = "analyzeButton"; 87 | this.analyzeButton.Size = new System.Drawing.Size(60, 47); 88 | this.analyzeButton.TabIndex = 7; 89 | this.analyzeButton.UseVisualStyleBackColor = true; 90 | this.analyzeButton.Click += new System.EventHandler(this.AnalyzeOnClick); 91 | // 92 | // sourceBox 93 | // 94 | this.sourceBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 95 | | System.Windows.Forms.AnchorStyles.Right))); 96 | this.sourceBox.Location = new System.Drawing.Point(119, 15); 97 | this.sourceBox.Margin = new System.Windows.Forms.Padding(3, 3, 3, 8); 98 | this.sourceBox.Name = "sourceBox"; 99 | this.sourceBox.Size = new System.Drawing.Size(759, 26); 100 | this.sourceBox.TabIndex = 5; 101 | this.sourceBox.TextChanged += new System.EventHandler(this.EnableControlsOnTextChanged); 102 | // 103 | // browseButton 104 | // 105 | this.browseButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); 106 | this.browseButton.AutoSize = true; 107 | this.browseButton.Image = ((System.Drawing.Image)(resources.GetObject("browseButton.Image"))); 108 | this.browseButton.ImeMode = System.Windows.Forms.ImeMode.NoControl; 109 | this.browseButton.Location = new System.Drawing.Point(884, 5); 110 | this.browseButton.Name = "browseButton"; 111 | this.browseButton.Size = new System.Drawing.Size(60, 47); 112 | this.browseButton.TabIndex = 6; 113 | this.browseButton.UseVisualStyleBackColor = true; 114 | this.browseButton.Click += new System.EventHandler(this.BrowseOnClick); 115 | // 116 | // sourceLabel 117 | // 118 | this.sourceLabel.AutoSize = true; 119 | this.sourceLabel.ImeMode = System.Windows.Forms.ImeMode.NoControl; 120 | this.sourceLabel.Location = new System.Drawing.Point(9, 18); 121 | this.sourceLabel.Name = "sourceLabel"; 122 | this.sourceLabel.Size = new System.Drawing.Size(104, 20); 123 | this.sourceLabel.TabIndex = 4; 124 | this.sourceLabel.Text = "Source Resx:"; 125 | // 126 | // introPanel 127 | // 128 | this.introPanel.Controls.Add(this.introLabel); 129 | this.introPanel.Dock = System.Windows.Forms.DockStyle.Top; 130 | this.introPanel.Location = new System.Drawing.Point(0, 55); 131 | this.introPanel.Name = "introPanel"; 132 | this.introPanel.Size = new System.Drawing.Size(1013, 51); 133 | this.introPanel.TabIndex = 13; 134 | // 135 | // openFileDialog 136 | // 137 | this.openFileDialog.DefaultExt = "resx"; 138 | this.openFileDialog.Filter = "Resx Files (*.resx)|*.resx|Hint Files (*-hints.xml)|*-hints.xml"; 139 | this.openFileDialog.Title = "Choose source resx file"; 140 | // 141 | // AnalyzeControlPanel 142 | // 143 | this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); 144 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 145 | this.Controls.Add(this.logbox); 146 | this.Controls.Add(this.introPanel); 147 | this.Controls.Add(this.cmdPanel); 148 | this.Name = "AnalyzeControlPanel"; 149 | this.Size = new System.Drawing.Size(1013, 757); 150 | this.cmdPanel.ResumeLayout(false); 151 | this.cmdPanel.PerformLayout(); 152 | this.introPanel.ResumeLayout(false); 153 | this.introPanel.PerformLayout(); 154 | this.ResumeLayout(false); 155 | 156 | } 157 | 158 | #endregion 159 | 160 | private System.Windows.Forms.Label introLabel; 161 | private System.Windows.Forms.RichTextBox logbox; 162 | private System.Windows.Forms.Panel cmdPanel; 163 | private System.Windows.Forms.Button analyzeButton; 164 | private System.Windows.Forms.TextBox sourceBox; 165 | private System.Windows.Forms.Button browseButton; 166 | private System.Windows.Forms.Label sourceLabel; 167 | private System.Windows.Forms.Panel introPanel; 168 | private System.Windows.Forms.OpenFileDialog openFileDialog; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Panels/AnalyzeControlPanel.cs: -------------------------------------------------------------------------------- 1 | //************************************************************************************************ 2 | // Copyright © 2020 Steven M Cohn. All rights reserved. 3 | //************************************************************************************************ 4 | 5 | namespace ResxTranslator.Panels 6 | { 7 | using System; 8 | using System.Drawing; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Windows.Forms; 12 | using System.Xml.Linq; 13 | 14 | public partial class AnalyzeControlPanel : UserControl 15 | { 16 | private const string NL = "\n"; 17 | 18 | 19 | public AnalyzeControlPanel() 20 | { 21 | InitializeComponent(); 22 | } 23 | 24 | 25 | /// 26 | /// Set the initial file path, Called from the main window to inherit the path 27 | /// from the main tab 28 | /// 29 | /// 30 | public void SetFilePath(string path) 31 | { 32 | sourceBox.Text = path; 33 | } 34 | 35 | 36 | private void BrowseOnClick(object sender, EventArgs e) 37 | { 38 | if (openFileDialog.ShowDialog() == DialogResult.OK) 39 | { 40 | sourceBox.Text = openFileDialog.FileName; 41 | } 42 | } 43 | 44 | 45 | private void EnableControlsOnTextChanged(object sender, EventArgs e) 46 | { 47 | analyzeButton.Enabled = 48 | sourceBox.Text.Length > 0 && 49 | File.Exists(sourceBox.Text); 50 | } 51 | 52 | 53 | private void AnalyzeOnClick(object sender, EventArgs e) 54 | { 55 | logbox.Clear(); 56 | 57 | var root = XElement.Load(sourceBox.Text); 58 | 59 | var data = root.Elements("data") 60 | .Where(d => d.Attribute("type") == null) 61 | .Select(d => new 62 | { 63 | Data = d, 64 | Comment = d.Element("comment")?.Value 65 | }) 66 | .Where(a => string.IsNullOrWhiteSpace(a.Comment) || 67 | !(a.Comment.ContainsICIC("!SKIP") || a.Comment.ContainsICIC("!NODUP"))) 68 | .Select(d => d.Data); 69 | 70 | Log($"Resx contains {data.Count()} strings" + NL); 71 | 72 | var groups = data.GroupBy(d => d.Element("value").Value); 73 | Log($"Found {groups.Count()} unique strings" + NL); 74 | 75 | var unique = groups.Where(g => g.Count() > 1); 76 | Log($"Found {unique.Count()} duplicate strings" + NL + NL); 77 | 78 | Log(new String('=', 100) + NL); 79 | 80 | var delims = new[] { ' ', '\n', '\r' }; 81 | 82 | var words = unique.Where(g => !delims.Any(d => g.Key.Contains(d))); 83 | Log($"found {words.Count()} single-word duplicates" + NL, Color.DarkRed); 84 | foreach (var group in words.OrderBy(w => w.Key)) 85 | { 86 | Log(NL + group.Key + NL, Color.Red); 87 | foreach (var item in group) 88 | { 89 | Log($" . . . {item.Attribute("name").Value}" + NL); 90 | } 91 | } 92 | 93 | Log(NL + new String('=', 100) + NL); 94 | 95 | var phrases = unique.Where(g => delims.Any(d => g.Key.Contains(d))); 96 | Log($"found {phrases.Count()} multi-word duplicate phrases" + NL, Color.DarkBlue); 97 | foreach (var group in phrases.OrderBy(p => p.Key)) 98 | { 99 | Log(NL + group.Key + NL, Color.Blue); 100 | foreach (var item in group) 101 | { 102 | Log($" . . . {item.Attribute("name").Value}" + NL); 103 | } 104 | } 105 | } 106 | 107 | 108 | private void Log(string message, Color? color = null) 109 | { 110 | if (color == null || color.Equals(Color.Black)) 111 | { 112 | logbox.AppendText(message); 113 | return; 114 | } 115 | 116 | var fore = logbox.SelectionColor; 117 | logbox.SelectionColor = (Color)color; 118 | logbox.AppendText(message); 119 | logbox.SelectionColor = fore; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Panels/AnalyzeControlPanel.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 | 123 | iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO 124 | vAAADrwBlbxySQAAAAd0SU1FB9ULGg0YMGpJmnAAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAu 125 | MjHxIGmVAAABBElEQVQ4T6WRsRGDMAxFPQU7ULtiCNbwCmxAnYo7j5AiDRMwgHsq5qBU9KUYGzDkcine 126 | nbH9voRsiOgviptHlmWhYRgE7wNN00TrugpF4YhznkIIvNRvBM3zTNiX9NfzcSIXjuAMsgToZc/7EADW 127 | /jYkyluASkkWgtt1lGbg+DyFZQG5rAElvgbgd0BePSd20ve9hGmAzCCJ2sEV2klgGUEpgMFaBpq1fMby 128 | VcsDtPKU8qaQ99XLYsSzHGexBdxX/8hcOXhDzmp1CcAwrqvnosqo7hyfxVdAK1odgzGXSNuQufo4jvsA 129 | 0HUdX3CSjgG1bUtN05Bloa5rEaqq2sTI7uN3yLwBpM724PGQQrAAAAAASUVORK5CYII= 130 | 131 | 132 | 133 | 17, 17 134 | 135 | -------------------------------------------------------------------------------- /Panels/LanguageComboBox.cs: -------------------------------------------------------------------------------- 1 | //************************************************************************************************ 2 | // Copyright © 2020 Steven M Cohn. All rights reserved. 3 | //************************************************************************************************ 4 | 5 | namespace ResxTranslator.Panels 6 | { 7 | using System.Windows.Forms; 8 | 9 | 10 | internal class LanguageComboBox : ComboBox 11 | { 12 | public LanguageComboBox() 13 | { 14 | PopulateLanguages(); 15 | } 16 | 17 | 18 | private void PopulateLanguages() 19 | { 20 | foreach (var code in Translator.Codes) 21 | { 22 | Items.Add(Translator.GetDisplayName(code)); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Panels/TextControlPanel.cs: -------------------------------------------------------------------------------- 1 | //************************************************************************************************ 2 | // Copyright © 2020 Steven M Cohn. All rights reserved. 3 | //************************************************************************************************ 4 | 5 | namespace ResxTranslator.Panels 6 | { 7 | using System; 8 | using System.Diagnostics; 9 | using System.Drawing; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using System.Web; 13 | using System.Windows.Forms; 14 | 15 | 16 | public partial class TextControlPanel : UserControl 17 | { 18 | private const string NL = "\n"; 19 | 20 | private CancellationTokenSource cancellation; 21 | 22 | 23 | public TextControlPanel() 24 | { 25 | InitializeComponent(); 26 | cancellation = new CancellationTokenSource(); 27 | 28 | fromCodeBox.Items.Insert(0, "auto (Detect)"); 29 | fromCodeBox.SelectedIndex = 0; 30 | 31 | var index = Translator.Codes.IndexOf( 32 | Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName); 33 | 34 | toCodeBox.SelectedIndex = index < 0 ? Translator.Codes.IndexOf("en") : index; 35 | } 36 | 37 | 38 | private void ChangeLanguagesOnSelectedIndexChanged(object sender, EventArgs e) 39 | { 40 | translateButton.Enabled = 41 | fromCodeBox.SelectedIndex >= 0 && 42 | toCodeBox.SelectedIndex >= 0 && 43 | textBox.Text.Length > 0; 44 | } 45 | 46 | 47 | private void EnableControlsOnTextChanged(object sender, EventArgs e) 48 | { 49 | translateButton.Enabled = textBox.Text.Trim().Length > 0; 50 | } 51 | 52 | 53 | private async void TranslateOnClick(object sender, EventArgs e) 54 | { 55 | translateButton.Visible = false; 56 | cancelButton.Visible = true; 57 | 58 | logbox.Clear(); 59 | logbox.ForeColor = Color.Black; 60 | 61 | var fromCode = fromCodeBox.SelectedIndex == 0 62 | ? "auto" 63 | : Translator.Codes[fromCodeBox.SelectedIndex - 1]; 64 | 65 | var toCode = Translator.Codes[toCodeBox.SelectedIndex]; 66 | 67 | var translator = new Translator(); 68 | 69 | var watch = new Stopwatch(); 70 | watch.Start(); 71 | 72 | try 73 | { 74 | using (cancellation = new CancellationTokenSource()) 75 | { 76 | var parts = textBox.Text.Split('\n'); 77 | for (int i = 0; i < parts.Length; i++) 78 | { 79 | var result = await translator.Translate( 80 | parts[i], fromCode, toCode, cancellation); 81 | 82 | watch.Stop(); 83 | 84 | if (cancellation.IsCancellationRequested) 85 | { 86 | Log("Cancelled" + NL, Color.Red); 87 | break; 88 | } 89 | else 90 | { 91 | Log(result + NL); 92 | 93 | if (translator.Inflated(parts[i], result)) 94 | { 95 | Log("*** possible inflation detected ***" + NL, Color.Maroon); 96 | } 97 | 98 | Log($"ellapsed time {watch.ElapsedMilliseconds}ms{NL}", Color.DarkCyan); 99 | } 100 | } 101 | } 102 | } 103 | catch (TaskCanceledException) 104 | { 105 | Log("Cancelled" + NL, Color.DarkRed); 106 | } 107 | catch (HttpException exc) 108 | { 109 | Log(exc.Message + NL, Color.Red); 110 | } 111 | finally 112 | { 113 | if (watch.IsRunning) 114 | { 115 | watch.Stop(); 116 | } 117 | } 118 | 119 | cancelButton.Visible = false; 120 | translateButton.Visible = true; 121 | } 122 | 123 | 124 | private void CancelOnClick(object sender, EventArgs e) 125 | { 126 | cancellation.Cancel(); 127 | } 128 | 129 | 130 | private void Log(string message, Color? color = null) 131 | { 132 | if (color == null || color.Equals(Color.Black)) 133 | { 134 | logbox.AppendText(message); 135 | return; 136 | } 137 | 138 | var fore = logbox.SelectionColor; 139 | logbox.SelectionColor = (Color)color; 140 | logbox.AppendText(message); 141 | logbox.SelectionColor = fore; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Panels/TextControlPanel.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 | 123 | iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO 124 | vAAADrwBlbxySQAAAixJREFUOE+Vks1rE0EYxvdfMGlDLR48VDx4KKgXoeCpUC9+IFhxt6bqwVxUbEVL 125 | xRYqFNuq1KKk1A9QelAQTaEo6EXEbjZNTBs02qClS2k+mrTutkm7ybqbp7PZzSbbBMHDb5h553med3Z2 126 | KAAWZGER8XfDCN+lEbjRDH/XEcwMXQI/+QLrqQ0iserNSV5OIzFxC1x7LabO7KgK69yLiGeCyLcF5LMC 127 | +EcnwdK2qkYLdC1m3W4zpDCkJjtKZmYXOFfDNqMNXtc+eNsMDV2PsOdzIYRShe8IXHDoG0w9vr18CykW 128 | ROjqHtPMXWlB6ncUy55rZiP24iGsxjZBCV/6SaLRiXFg5uEIJOkv5IQe4us4hlV+Bdod8Y9PlU6qneI9 129 | Cyr+pNXoZEDbEXxwD1lZRS75C+vLItTcGvgxYi7XEaaHR0FFR1osxSKhp+NQVfKV+RyWxs9WmAv09IKK 130 | uY9XbHCdJyAkJNJZgiwrUIQ5hLsbK3UD90GtfOwuK9rh62IgJiUomTgiA4fxdbAPG+ks1MwifvYeLNM6 131 | EHr9AZQSn4LPadeLzt2Y/+SHnIlirr9Jr5E7mb59HWlxE2vcKLyMrmXPNyK58Ed/DNHnp0t/or0Bgc4D 132 | +tzERv5GE3zn6vQ17cDss1f6O9AGNb2A8M39pZB/UgN/z+WC2QzQUMR5RIaayWurqWIyaKtDkNxJ0aNh 133 | ToqIwTf4cecoWKbMSO9EoM+FJc5LJFa9ZfH/gNoC52604oOVNVUAAAAASUVORK5CYII= 134 | 135 | 136 | 137 | 138 | iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO 139 | vAAADrwBlbxySQAABCFJREFUSEudlG1MW1UYx/thZpl+1US3ROc+7bvyMnFuiW7qZJEQNVgMiLxFYcRO 140 | IE4KbDSwiS4RCYaXZZsEwbFZ3gR5mRKTQkNbShF5K1D6etve9t4LKLDygb/nXNp6gdUhT/LLzT3nPP// 141 | uc9zzpW53e53GYYxu1wubi84nU7KtN1uTzabzQdljwoibiJgJxaLBf39/SAiu+aC9BLDZ4MykYMsWqQJ 142 | BoMBfX196O3tFWltbUV2djYaGxvDY5SJiQnRgHyFxuPxHAvKRA6yWDRoaGhAWloaUlJSRJKTk5GQkAC5 143 | XB4eo6jV6v0Z0FKMj4/DZDKJjI2NQa/Xw2g0Ijc3FzU1NeLYwsLC/gxC6HQ6sSxWq1V8pwaZmZkYGBgA 144 | KWd43b4NJicnxfJQE5vNhvr6eigUil3NdjicGpttHwaU9vZ2scG1tbXIy8tDR0cHEXRsWzNrcWr+mN2n 145 | ATnjKCwsRGJiIlQqFbTGGXSPeNA+7A1zc8A7lVHLZZ2+wr8dXSzhiy2iin2vxirsh7YZ0BprtVqUlZUh 146 | NTUV+fn5SP/4IpKuTSC+kse5L4UwZyr4QNxlwR1TIjijQxRvwxqt9NeFDeg9KC8vF49lRkaGeObn5uZw 147 | q02PsyoGMSVL/x8lvx42GBoaQkFBAZqamrC4uBg+MT2kNPGVwsMF9kD4JlOm5hm0/O6FedEd7keP7uEG 148 | J0qXUNTCoa6fQ3pd5A1Qg2EXEdJOeHDhJoeXLgtQ3PZDP7VlEsng/WoBf1p8WF5ioZvx4eSV7fMhZE6n 149 | O14/zbQVfO+ZjCvhAqGJ4hZ2xDDN3P9h0Gs4e5VflybFkt1XdXNgvCwMsz4IPEs2x4fnpchIcx/rGrYf 150 | OVliT38+x8a8ULS1MKna92Kb1n30kxt8MjktTmnS+a8E/GryieIXbvHwcyx+MfgQK1kTIngbZLLjeQvx 151 | T6ZOO45kWRClFJBWxTxFx+XfLr9JymaXJuXd5mFxsvi6k8Mp1ZJoNu/w4b2q3aUUxWkc/9T61jMZM/an 152 | 06c3j+UsbH52j3+urhOP191fS1I0/e16OVhj+vyu1w9BYPGbyQ/1MOkX6YHXx6KijYts8EHj5hPNmrXq 153 | LkNg5WfjBqR0jW5A2bqKV1TLOEcaTpvqcLMw232YDUL70a3348zV7V8RlN+K63fsh9Qja593jQbYnSZ3 154 | tAG8880KLjbysLpYFP3Ik9ss4LUgFWqOGLLIIc2W9iIo/W9U9Wwe7DQG5J2jG3c7DIG7P+keDDdpHqxl 155 | 3fiLHOElfFTL41IzhzfI70K609evCVDd40gfHmFAY3AQB9pHVw83a1YPV3asf5hUvcLEEXGaQC8YJSQg 156 | hW5g59yW4n/E+evLsSdKhXlp0l6JVvJCUCZynCrFAfJnLItWCv6YEn5jzygFJuoSJ/8HbVeAmG5aes4A 157 | AAAASUVORK5CYII= 158 | 159 | 160 | -------------------------------------------------------------------------------- /Panels/ToolsControlPanel.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace ResxTranslator.Panels 2 | { 3 | partial class ToolsControlPanel 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Component Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ToolsControlPanel)); 32 | this.logbox = new System.Windows.Forms.RichTextBox(); 33 | this.updateLabel = new System.Windows.Forms.Label(); 34 | this.updateButton = new System.Windows.Forms.Button(); 35 | this.sortLabel = new System.Windows.Forms.Label(); 36 | this.sortButton = new System.Windows.Forms.Button(); 37 | this.cmdPanel = new System.Windows.Forms.Panel(); 38 | this.sourceBox = new System.Windows.Forms.TextBox(); 39 | this.browseButton = new System.Windows.Forms.Button(); 40 | this.sourceLabel = new System.Windows.Forms.Label(); 41 | this.openFileDialog = new System.Windows.Forms.OpenFileDialog(); 42 | this.toolPanel = new System.Windows.Forms.Panel(); 43 | this.cmdPanel.SuspendLayout(); 44 | this.toolPanel.SuspendLayout(); 45 | this.SuspendLayout(); 46 | // 47 | // logbox 48 | // 49 | this.logbox.Dock = System.Windows.Forms.DockStyle.Fill; 50 | this.logbox.Location = new System.Drawing.Point(3, 256); 51 | this.logbox.Margin = new System.Windows.Forms.Padding(0); 52 | this.logbox.Name = "logbox"; 53 | this.logbox.Size = new System.Drawing.Size(998, 465); 54 | this.logbox.TabIndex = 21; 55 | this.logbox.Text = ""; 56 | // 57 | // updateLabel 58 | // 59 | this.updateLabel.AutoSize = true; 60 | this.updateLabel.ImeMode = System.Windows.Forms.ImeMode.NoControl; 61 | this.updateLabel.Location = new System.Drawing.Point(133, 132); 62 | this.updateLabel.Name = "updateLabel"; 63 | this.updateLabel.Size = new System.Drawing.Size(475, 20); 64 | this.updateLabel.TabIndex = 20; 65 | this.updateLabel.Text = "Update values in hints file; based on its ./Resources.resx"; 66 | // 67 | // updateButton 68 | // 69 | this.updateButton.BackgroundImage = global::ResxTranslator.Properties.Resources.Update; 70 | this.updateButton.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center; 71 | this.updateButton.Enabled = false; 72 | this.updateButton.ImeMode = System.Windows.Forms.ImeMode.NoControl; 73 | this.updateButton.Location = new System.Drawing.Point(39, 116); 74 | this.updateButton.Name = "updateButton"; 75 | this.updateButton.Size = new System.Drawing.Size(85, 47); 76 | this.updateButton.TabIndex = 19; 77 | this.updateButton.UseVisualStyleBackColor = true; 78 | this.updateButton.Click += new System.EventHandler(this.UpdateHintsOnClick); 79 | // 80 | // sortLabel 81 | // 82 | this.sortLabel.AutoSize = true; 83 | this.sortLabel.ImeMode = System.Windows.Forms.ImeMode.NoControl; 84 | this.sortLabel.Location = new System.Drawing.Point(133, 44); 85 | this.sortLabel.Name = "sortLabel"; 86 | this.sortLabel.Size = new System.Drawing.Size(287, 20); 87 | this.sortLabel.TabIndex = 18; 88 | this.sortLabel.Text = "Sort all elements in resource file"; 89 | // 90 | // sortButton 91 | // 92 | this.sortButton.BackgroundImage = global::ResxTranslator.Properties.Resources.Sort; 93 | this.sortButton.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center; 94 | this.sortButton.Enabled = false; 95 | this.sortButton.ImeMode = System.Windows.Forms.ImeMode.NoControl; 96 | this.sortButton.Location = new System.Drawing.Point(39, 28); 97 | this.sortButton.Name = "sortButton"; 98 | this.sortButton.Size = new System.Drawing.Size(85, 47); 99 | this.sortButton.TabIndex = 17; 100 | this.sortButton.UseVisualStyleBackColor = true; 101 | this.sortButton.Click += new System.EventHandler(this.SortResourcesOnClick); 102 | // 103 | // cmdPanel 104 | // 105 | this.cmdPanel.Controls.Add(this.sourceBox); 106 | this.cmdPanel.Controls.Add(this.browseButton); 107 | this.cmdPanel.Controls.Add(this.sourceLabel); 108 | this.cmdPanel.Dock = System.Windows.Forms.DockStyle.Top; 109 | this.cmdPanel.Location = new System.Drawing.Point(3, 3); 110 | this.cmdPanel.Name = "cmdPanel"; 111 | this.cmdPanel.Size = new System.Drawing.Size(998, 55); 112 | this.cmdPanel.TabIndex = 16; 113 | // 114 | // sourceBox 115 | // 116 | this.sourceBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 117 | | System.Windows.Forms.AnchorStyles.Right))); 118 | this.sourceBox.Location = new System.Drawing.Point(119, 15); 119 | this.sourceBox.Margin = new System.Windows.Forms.Padding(3, 3, 3, 8); 120 | this.sourceBox.Name = "sourceBox"; 121 | this.sourceBox.Size = new System.Drawing.Size(786, 26); 122 | this.sourceBox.TabIndex = 5; 123 | this.sourceBox.TextChanged += new System.EventHandler(this.EnableControlsOnTextChanged); 124 | // 125 | // browseButton 126 | // 127 | this.browseButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); 128 | this.browseButton.AutoSize = true; 129 | this.browseButton.Image = ((System.Drawing.Image)(resources.GetObject("browseButton.Image"))); 130 | this.browseButton.ImeMode = System.Windows.Forms.ImeMode.NoControl; 131 | this.browseButton.Location = new System.Drawing.Point(911, 5); 132 | this.browseButton.Name = "browseButton"; 133 | this.browseButton.Size = new System.Drawing.Size(60, 47); 134 | this.browseButton.TabIndex = 6; 135 | this.browseButton.UseVisualStyleBackColor = true; 136 | this.browseButton.Click += new System.EventHandler(this.BrowseOnClick); 137 | // 138 | // sourceLabel 139 | // 140 | this.sourceLabel.AutoSize = true; 141 | this.sourceLabel.ImeMode = System.Windows.Forms.ImeMode.NoControl; 142 | this.sourceLabel.Location = new System.Drawing.Point(9, 18); 143 | this.sourceLabel.Name = "sourceLabel"; 144 | this.sourceLabel.Size = new System.Drawing.Size(104, 20); 145 | this.sourceLabel.TabIndex = 4; 146 | this.sourceLabel.Text = "Source Resx:"; 147 | // 148 | // openFileDialog 149 | // 150 | this.openFileDialog.DefaultExt = "resx"; 151 | this.openFileDialog.Filter = "Resx Files (*.resx)|*.resx|Hint Files (*-hints.xml)|*-hints.xml"; 152 | this.openFileDialog.Title = "Choose source resx file"; 153 | // 154 | // toolPanel 155 | // 156 | this.toolPanel.Controls.Add(this.sortButton); 157 | this.toolPanel.Controls.Add(this.sortLabel); 158 | this.toolPanel.Controls.Add(this.updateLabel); 159 | this.toolPanel.Controls.Add(this.updateButton); 160 | this.toolPanel.Dock = System.Windows.Forms.DockStyle.Top; 161 | this.toolPanel.Location = new System.Drawing.Point(3, 58); 162 | this.toolPanel.Name = "toolPanel"; 163 | this.toolPanel.Size = new System.Drawing.Size(998, 198); 164 | this.toolPanel.TabIndex = 22; 165 | // 166 | // ToolsControlPanel 167 | // 168 | this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); 169 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 170 | this.Controls.Add(this.logbox); 171 | this.Controls.Add(this.toolPanel); 172 | this.Controls.Add(this.cmdPanel); 173 | this.Name = "ToolsControlPanel"; 174 | this.Padding = new System.Windows.Forms.Padding(3); 175 | this.Size = new System.Drawing.Size(1004, 724); 176 | this.cmdPanel.ResumeLayout(false); 177 | this.cmdPanel.PerformLayout(); 178 | this.toolPanel.ResumeLayout(false); 179 | this.toolPanel.PerformLayout(); 180 | this.ResumeLayout(false); 181 | 182 | } 183 | 184 | #endregion 185 | 186 | private System.Windows.Forms.RichTextBox logbox; 187 | private System.Windows.Forms.Label updateLabel; 188 | private System.Windows.Forms.Button updateButton; 189 | private System.Windows.Forms.Label sortLabel; 190 | private System.Windows.Forms.Button sortButton; 191 | private System.Windows.Forms.Panel cmdPanel; 192 | private System.Windows.Forms.TextBox sourceBox; 193 | private System.Windows.Forms.Button browseButton; 194 | private System.Windows.Forms.Label sourceLabel; 195 | private System.Windows.Forms.OpenFileDialog openFileDialog; 196 | private System.Windows.Forms.Panel toolPanel; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Panels/ToolsControlPanel.cs: -------------------------------------------------------------------------------- 1 | //************************************************************************************************ 2 | // Copyright © 2020 Steven M Cohn. All rights reserved. 3 | //************************************************************************************************ 4 | 5 | namespace ResxTranslator.Panels 6 | { 7 | using System; 8 | using System.Data; 9 | using System.Drawing; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Windows.Forms; 13 | using System.Xml.Linq; 14 | 15 | 16 | public partial class ToolsControlPanel : UserControl 17 | { 18 | private const string NL = "\n"; 19 | 20 | 21 | public ToolsControlPanel() 22 | { 23 | InitializeComponent(); 24 | } 25 | 26 | 27 | /// 28 | /// Set the initial file path, Called from the main window to inherit the path 29 | /// from the main tab 30 | /// 31 | /// 32 | public void SetFilePath(string path) 33 | { 34 | sourceBox.Text = path; 35 | } 36 | 37 | 38 | private void EnableControlsOnTextChanged(object sender, EventArgs e) 39 | { 40 | var path = Environment.ExpandEnvironmentVariables(sourceBox.Text.Trim()); 41 | if (path.Length > 0 && File.Exists(path)) 42 | { 43 | var type = Path.GetExtension(path); 44 | if (type.Equals(".resx", StringComparison.InvariantCultureIgnoreCase)) 45 | { 46 | sortButton.Enabled = sortLabel.Enabled = true; 47 | updateButton.Enabled = updateLabel.Enabled = false; 48 | } 49 | else 50 | { 51 | sortButton.Enabled = sortLabel.Enabled = false; 52 | updateButton.Enabled = updateLabel.Enabled = true; 53 | } 54 | } 55 | else 56 | { 57 | sortButton.Enabled = sortLabel.Enabled = false; 58 | updateButton.Enabled = updateLabel.Enabled = false; 59 | } 60 | } 61 | 62 | 63 | private void BrowseOnClick(object sender, EventArgs e) 64 | { 65 | if (openFileDialog.ShowDialog() == DialogResult.OK) 66 | { 67 | sourceBox.Text = openFileDialog.FileName; 68 | } 69 | } 70 | 71 | 72 | /// 73 | /// Intelligently sorts resources in the specified resource file 74 | /// 75 | /// 76 | /// 77 | private void SortResourcesOnClick(object sender, EventArgs e) 78 | { 79 | logbox.Clear(); 80 | 81 | var path = Environment.ExpandEnvironmentVariables(sourceBox.Text); 82 | if (File.Exists(path)) 83 | { 84 | var root = XElement.Load(path); 85 | ResxProvider.SortData(root); 86 | root.Save(path, SaveOptions.None); 87 | Log($"{Path.GetFileName(path)} sorted and saved{NL}", Color.Green); 88 | } 89 | } 90 | 91 | 92 | /// 93 | /// 94 | /// 95 | /// 96 | /// 97 | private void UpdateHintsOnClick(object sender, EventArgs e) 98 | { 99 | logbox.Clear(); 100 | 101 | var path = Environment.ExpandEnvironmentVariables(sourceBox.Text); 102 | if (!File.Exists(path)) 103 | { 104 | Log($"could not find {path}{NL}", Color.Red); 105 | return; 106 | } 107 | 108 | var xpath = Path.Combine(Path.GetDirectoryName(path), "Resources.resx"); 109 | if (!File.Exists(xpath)) 110 | { 111 | Log($"could not find {xpath}{NL}", Color.Red); 112 | return; 113 | } 114 | 115 | var xroot = XElement.Load(xpath); 116 | var hroot = XElement.Load(path); 117 | var updated = 0; 118 | var renamed = 0; 119 | var errors = 0; 120 | 121 | var hints = hroot.Elements("hint").ToList(); 122 | 123 | hints.ForEach(hint => 124 | { 125 | hint.Remove(); 126 | 127 | var name = hint.Attribute("name").Value; 128 | var data = xroot.Elements("data") 129 | .FirstOrDefault(d => d.Attribute("name").Value.Equals( 130 | name, StringComparison.InvariantCultureIgnoreCase)); 131 | 132 | if (data != null) 133 | { 134 | // should only be a case-difference 135 | var dataname = data.Attribute("name").Value; 136 | if (dataname != name) 137 | { 138 | Log($"correcting name {dataname}{NL}"); 139 | hint.Attribute("name").Value = dataname; 140 | renamed++; 141 | } 142 | 143 | var source = hint.Element("source"); 144 | if (source != null) 145 | { 146 | var value = data.Element("value").Value.Trim(); 147 | if (source.Value.Trim() != value) 148 | { 149 | Log($"updating source for {name}{NL}"); 150 | source.Value = data.Element("value").Value; 151 | updated++; 152 | } 153 | } 154 | else 155 | { 156 | Log($"adding missing source for {name}{NL}"); 157 | hint.AddFirst(new XElement("source", data.Element("value").Value)); 158 | updated++; 159 | } 160 | } 161 | else 162 | { 163 | errors++; 164 | } 165 | }); 166 | 167 | Log($"{NL}Summary{NL}", Color.DarkBlue); 168 | 169 | if (updated == 0 && renamed == 0 && errors == 0) 170 | { 171 | Log($"... No updates needed{NL}"); 172 | return; 173 | } 174 | 175 | if (updated > 0) 176 | { 177 | Log($"... {updated} sources updated{NL}", Color.Blue); 178 | } 179 | 180 | if (renamed > 0) 181 | { 182 | Log($"... {renamed} name corrections", Color.Blue); 183 | } 184 | 185 | if (errors > 0) 186 | { 187 | Log($"... {errors} errors were found!", Color.Red); 188 | } 189 | 190 | if (updated > 0 || renamed > 0) 191 | { 192 | hroot.Add(hints.OrderBy(d => d.Attribute("name").Value)); 193 | hroot.Save(path, SaveOptions.None); 194 | } 195 | } 196 | 197 | 198 | private void Log(string message, Color? color = null) 199 | { 200 | if (color == null || color.Equals(Color.Black)) 201 | { 202 | logbox.AppendText(message); 203 | return; 204 | } 205 | 206 | var fore = logbox.SelectionColor; 207 | logbox.SelectionColor = (Color)color; 208 | logbox.AppendText(message); 209 | logbox.SelectionColor = fore; 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /Panels/ToolsControlPanel.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 | 123 | iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO 124 | vAAADrwBlbxySQAAAAd0SU1FB9ULGg0YMGpJmnAAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAu 125 | MjHxIGmVAAABBElEQVQ4T6WRsRGDMAxFPQU7ULtiCNbwCmxAnYo7j5AiDRMwgHsq5qBU9KUYGzDkcine 126 | nbH9voRsiOgviptHlmWhYRgE7wNN00TrugpF4YhznkIIvNRvBM3zTNiX9NfzcSIXjuAMsgToZc/7EADW 127 | /jYkyluASkkWgtt1lGbg+DyFZQG5rAElvgbgd0BePSd20ve9hGmAzCCJ2sEV2klgGUEpgMFaBpq1fMby 128 | VcsDtPKU8qaQ99XLYsSzHGexBdxX/8hcOXhDzmp1CcAwrqvnosqo7hyfxVdAK1odgzGXSNuQufo4jvsA 129 | 0HUdX3CSjgG1bUtN05Bloa5rEaqq2sTI7uN3yLwBpM724PGQQrAAAAAASUVORK5CYII= 130 | 131 | 132 | 133 | 17, 17 134 | 135 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | //************************************************************************************************ 2 | // Copyright © 2020 Steven M Cohn. All rights reserved. 3 | //************************************************************************************************ 4 | 5 | namespace ResxTranslator 6 | { 7 | using System; 8 | using System.Globalization; 9 | using System.Threading; 10 | using System.Windows.Forms; 11 | 12 | 13 | static class Program 14 | { 15 | /// 16 | /// The main entry point for the application. 17 | /// 18 | [STAThread] 19 | static void Main() 20 | { 21 | var info = CultureInfo.GetCultureInfo("en-US"); 22 | Thread.CurrentThread.CurrentCulture = info; 23 | Thread.CurrentThread.CurrentUICulture = info; 24 | 25 | Application.EnableVisualStyles(); 26 | Application.SetCompatibleTextRenderingDefault(false); 27 | Application.Run(new MainWindow()); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("RoboTranslator")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("")] 8 | [assembly: AssemblyProduct("RoboTranslator")] 9 | [assembly: AssemblyCopyright("Copyright © 2020")] 10 | [assembly: AssemblyTrademark("")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | [assembly: Guid("6542dd09-8229-42dd-ab6d-19aaa1488eb6")] 15 | 16 | [assembly: AssemblyVersion("1.0.1.0")] 17 | [assembly: AssemblyFileVersion("1.0.1.0")] 18 | -------------------------------------------------------------------------------- /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 ResxTranslator.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", "17.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("ResxTranslator.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 I'm not translated!. 65 | /// 66 | internal static string ConfigString { 67 | get { 68 | return ResourceManager.GetString("ConfigString", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Foobar. 74 | /// 75 | internal static string DemoString { 76 | get { 77 | return ResourceManager.GetString("DemoString", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to This should be overriden in German. 83 | /// 84 | internal static string OverrideMe { 85 | get { 86 | return ResourceManager.GetString("OverrideMe", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to This will not use the hints file. 92 | /// 93 | internal static string OverrideObsolete { 94 | get { 95 | return ResourceManager.GetString("OverrideObsolete", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized resource of type System.Drawing.Bitmap. 101 | /// 102 | internal static System.Drawing.Bitmap Sort { 103 | get { 104 | object obj = ResourceManager.GetObject("Sort", resourceCulture); 105 | return ((System.Drawing.Bitmap)(obj)); 106 | } 107 | } 108 | 109 | /// 110 | /// Looks up a localized resource of type System.Drawing.Bitmap. 111 | /// 112 | internal static System.Drawing.Bitmap Translate { 113 | get { 114 | object obj = ResourceManager.GetObject("Translate", resourceCulture); 115 | return ((System.Drawing.Bitmap)(obj)); 116 | } 117 | } 118 | 119 | /// 120 | /// Looks up a localized resource of type System.Drawing.Bitmap. 121 | /// 122 | internal static System.Drawing.Bitmap Update { 123 | get { 124 | object obj = ResourceManager.GetObject("Update", resourceCulture); 125 | return ((System.Drawing.Bitmap)(obj)); 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Properties/Resources.ar-SA.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 | I'm not translated! 123 | SKIP 124 | 125 | 126 | فوبار 127 | The bar is fooed 128 | 129 | 130 | ينبغي تجاوز هذا باللغة الألمانية 131 | override example 132 | 133 | 134 | هذا لن يستخدم ملف التلميحات 135 | obsolete override example 136 | 137 | 138 | ..\images\sort.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | 141 | ..\Images\Translate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 142 | 143 | 144 | ..\images\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 145 | 146 | -------------------------------------------------------------------------------- /Properties/Resources.de-DE-hints.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | This should be overriden in German 6 | 7 | 8 | Dies sollte in deutscher Sprache ersetzt werden 9 | 10 | 11 | 12 | 13 | This is an obsolete translation 14 | 15 | 16 | Dies ist eine veraltete Übersetzung 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Properties/Resources.de-DE.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 | I'm not translated! 123 | SKIP 124 | 125 | 126 | Foobar 127 | The bar is fooed 128 | 129 | 130 | Dies sollte in deutscher Sprache ersetzt werden 131 | override example 132 | 133 | 134 | Dies ist eine veraltete Übersetzung 135 | obsolete override example 136 | 137 | 138 | ..\images\sort.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | 141 | ..\Images\Translate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 142 | 143 | 144 | ..\images\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 145 | 146 | -------------------------------------------------------------------------------- /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 | I'm not translated! 123 | SKIP 124 | 125 | 126 | Foobar 127 | The bar is fooed 128 | 129 | 130 | Esto debería anularse en alemán. 131 | override example 132 | 133 | 134 | Esto no utilizará el archivo de sugerencias. 135 | obsolete override example 136 | 137 | 138 | ..\images\sort.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | 141 | ..\Images\Translate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 142 | 143 | 144 | ..\images\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 145 | 146 | -------------------------------------------------------------------------------- /Properties/Resources.fr-FR.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 | I'm not translated! 123 | SKIP 124 | 125 | 126 | Foobar 127 | The bar is fooed 128 | 129 | 130 | Cela devrait être remplacé en allemand 131 | override example 132 | 133 | 134 | Cela n'utilisera pas le fichier d'astuces 135 | obsolete override example 136 | 137 | 138 | ..\images\sort.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | 141 | ..\Images\Translate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 142 | 143 | 144 | ..\images\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 145 | 146 | -------------------------------------------------------------------------------- /Properties/Resources.he-IL.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 | I'm not translated! 123 | SKIP 124 | 125 | 126 | פובר 127 | The bar is fooed 128 | 129 | 130 | יש לעקוף את זה בגרמנית 131 | override example 132 | 133 | 134 | זה לא ישתמש בקובץ הרמזים 135 | obsolete override example 136 | 137 | 138 | ..\images\sort.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | 141 | ..\Images\Translate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 142 | 143 | 144 | ..\images\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 145 | 146 | -------------------------------------------------------------------------------- /Properties/Resources.ja-JP.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 | I'm not translated! 123 | SKIP 124 | 125 | 126 | フーバー 127 | The bar is fooed 128 | 129 | 130 | これはドイツ語でオーバーライドされる必要があります 131 | override example 132 | 133 | 134 | これはヒントファイルを使用しません 135 | obsolete override example 136 | 137 | 138 | ..\images\sort.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | 141 | ..\Images\Translate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 142 | 143 | 144 | ..\images\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 145 | 146 | -------------------------------------------------------------------------------- /Properties/Resources.nl-NL.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 | I'm not translated! 123 | SKIP 124 | 125 | 126 | Foobar 127 | The bar is fooed 128 | 129 | 130 | In het Duits zou dit moeten worden overschreven 131 | override example 132 | 133 | 134 | Hierdoor wordt het hintbestand niet gebruikt 135 | obsolete override example 136 | 137 | 138 | ..\images\sort.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | 141 | ..\Images\Translate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 142 | 143 | 144 | ..\images\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 145 | 146 | -------------------------------------------------------------------------------- /Properties/Resources.pl-PL.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 | I'm not translated! 123 | SKIP 124 | 125 | 126 | Foobara 127 | The bar is fooed 128 | 129 | 130 | Należy to zastąpić w języku niemieckim 131 | override example 132 | 133 | 134 | Nie spowoduje to użycia pliku podpowiedzi 135 | obsolete override example 136 | 137 | 138 | ..\images\sort.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | 141 | ..\Images\Translate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 142 | 143 | 144 | ..\images\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 145 | 146 | -------------------------------------------------------------------------------- /Properties/Resources.pt-BR.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 | I'm not translated! 123 | SKIP 124 | 125 | 126 | Foobar 127 | The bar is fooed 128 | 129 | 130 | Isso deve ser substituído em alemão 131 | override example 132 | 133 | 134 | Isso não usará o arquivo de dicas 135 | obsolete override example 136 | 137 | 138 | ..\images\sort.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | 141 | ..\Images\Translate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 142 | 143 | 144 | ..\images\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 145 | 146 | -------------------------------------------------------------------------------- /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 | 122 | I'm not translated! 123 | !SKIP 124 | 125 | 126 | Foobar 127 | The bar is fooed 128 | 129 | 130 | This should be overriden in German 131 | override example 132 | 133 | 134 | This will not use the hints file 135 | obsolete override example 136 | 137 | 138 | ..\images\sort.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | 141 | ..\Images\Translate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 142 | 143 | 144 | ..\images\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 145 | 146 | -------------------------------------------------------------------------------- /Properties/Resources.zh-CN.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 | I'm not translated! 123 | SKIP 124 | 125 | 126 | 富巴 127 | The bar is fooed 128 | 129 | 130 | 这应该在德语中被覆盖 131 | override example 132 | 133 | 134 | 这不会使用提示文件 135 | obsolete override example 136 | 137 | 138 | ..\images\sort.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | 141 | ..\Images\Translate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 142 | 143 | 144 | ..\images\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 145 | 146 | -------------------------------------------------------------------------------- /Properties/Settings.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 ResxTranslator.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.7.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ResxTranslator 2 | Translates entire resx files or individual bits of text to multiple languages. 3 | 4 | There are four main tabs 5 | 6 | - Translate Resx file - translates a resx file to the specified languages, fully explained below. 7 | - Translate Text - translates a given text to a selected language. Use this for quick translations 8 | - Analyze - identifies duplicate words and phrases in the resx file, useful for consolidating and cleaning up resource strings 9 | - Tools - Additional resx management tools 10 | 11 | Inspired by https://github.com/salarcode/AutoResxTranslator 12 | 13 | ## Translate Resx File 14 | Translates an entire .resx file to one or more languages. 15 | 16 | 1. Choose a .resx file to translate. It will detect the language based on the language/culture 17 | code in the filename, or English if no code is included. It will remember the last file 18 | translated. 19 | 2. Choose the output directory where new .resx files should be stored. If this is left blank 20 | then new files are stored in the same directory as the source file. 21 | 3. Choose the target translation languages. 22 | 23 | _Options_ 24 | 25 | * Enable the _Translate only new strings_ checkbox to translate only new strings that were added 26 | to the source resx file that are not yet in the target resx file(s). It will detect entries 27 | that were deleted in the source file and remove them from the target files. It also detects 28 | the keyword **!EDIT** in the comment of each entry; if this keyword is found, it will translate 29 | that entry and replace the value in all taret files; if this keyword is not found and the 30 | resource identifier exists in both the source and target file, that entry is left untouched. 31 | 32 | * Enable the _Clear markers_ checkbox to remove the **!EDIT** keyword from all entires in the source 33 | resx file. If generating all languages in a single batch then you can safely enable this option. 34 | If translating one language at a time, enable this only when processing the last target file 35 | in your workflow, otherwise subsequent runs may not produce the correct results. 36 | 37 | ### Language Selections 38 | 39 | ![Translate Resx](Images/LanguagesScreen.png) 40 | 41 | ### Working 42 | 43 | ![Translate Resx](Images/TranslateResxScreen.png) 44 | 45 | ### Skipping Resources 46 | 47 | If the resource file includes control or configuration entries that should not be translated 48 | then flag these by including the word **!SKIP** in the entry's comment. It must be capitalized. 49 | The comment can include other text besides the word **!SKIP**. 50 | 51 | ### Using an Override Hint File 52 | 53 | While Google translator is generally quite good, there are nuances in languages that it can't 54 | predict and may make a native speaker question the appropriateness of the translation. For each 55 | language, you can provide an optional hint file that contains preferred translations for each 56 | resource identifier. This must be an XML file located in the same directory as the source resx 57 | file and must be named _name_._code_-hints.xml, for example Resources.de-DE-hints.xml. 58 | 59 | The contents of the hints file should look similar to the following: 60 | 61 | ```xml 62 | 63 | 64 | 65 | 66 | copy of the source text from the main resx file 67 | 68 | 69 | my preferred translation here 70 | 71 | 72 | 73 | 74 | copy of the source text from the main resx file 75 | 76 | 77 | my preferred translation here 78 | 79 | 80 | 81 | ``` 82 | When resources are translated to that language, ResxTranslator will first look in this hint file 83 | each resource identifier and use the available text before attempting to use Google translate. 84 | 85 | Notice that the file includes a copy of the original source text. ResxTranslator uses this to 86 | detect if the source item has been updated since this hint was last created and display a warning 87 | that the hint may be out of date and needs correcting. 88 | 89 | ## Translate Text 90 | Translate one string, phrase, or paragraph of text. 91 | 92 | ![Translate Text](Images/TranslateTextScreen.png) 93 | 94 | ### Inflation Detection 95 | 96 | The free Google translator will sometime add extra spaces around non-alphanumeric characters 97 | when translating. For example "x+1" may become "x + 1" (from no spaces around the plus sign, 98 | to spaces around the plus sign.) x 99 | 100 | 101 | ResxTranslator attempts to detect this string *inflation* and displays a warning for each 102 | string that may need manual tuning. Of course, the program itself has no way of knowing the 103 | exact context of the translation so it simply compares the number of spaces in the input 104 | string and the output string. Most of the time, the translation is accurate and shouldn't 105 | need to be adjusted manually. 106 | 107 | ## Analyze 108 | 109 | The Analyze tab analyzes a resx file looking for duplicate uses of words and phrase. 110 | 111 | ![Analyze Resources](Images/AnalyzeScreen.png) 112 | 113 | ### Ignoring Known Duplicate 114 | 115 | There will likely be legitimate cases where you want to use duplicates but in very 116 | different contexts. In these case, you can tag those resources with the **!NODUP** keyword; 117 | the analyzer will ignore those resources and not report them as duplicates. 118 | 119 | ## Tools 120 | 121 | The Tools tab contains operations that help maintain resx files. 122 | 123 | ![Translate Text](Images/ToolsScreen.png) 124 | 125 | ### Sort Data Items 126 | Sorts the `` items within a resx file alphabetically by resourceID. 127 | This makes it easier to find specific resources when viewing the file in a text editor. 128 | 129 | ### Update Hint Sources 130 | Updates the values of the `` elements within an override hints file. 131 | This should be used when the original source text has changed so you can verify 132 | individual items. It can also be used to fill in any missing `` elements or values 133 | or correct the case-sensitive resource name of each hint. 134 | -------------------------------------------------------------------------------- /ResxProvider.cs: -------------------------------------------------------------------------------- 1 | //************************************************************************************************ 2 | // Copyright © 2023 Steven M Cohn. All rights reserved. 3 | //************************************************************************************************ 4 | 5 | namespace ResxTranslator 6 | { 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Xml.Linq; 10 | 11 | 12 | internal static class ResxProvider 13 | { 14 | 15 | /// 16 | /// Gets a list of all translatable string resources 17 | /// 18 | /// An XElement of the root of a resx file 19 | /// The list of data elements of all translatable strings 20 | public static List CollectStrings(XElement root) 21 | { 22 | /* 23 | 24 | Foobar 25 | NO 26 | 27 | */ 28 | 29 | // this should filter out all non-string entries and leave only strings 30 | return root.Elements("data") 31 | .Where(e => 32 | // string entires always have xml:space= and do not have type= 33 | e.Attributes().Any(a => a.Name.LocalName == "space") && 34 | e.Attribute("type") == null && 35 | // TODO: what is this for? 36 | e.Attribute("name")?.Value.StartsWith(">>") != true && 37 | // !SKIP is a special flag indicating this entry should not be translated 38 | e.Element("comment")?.Value.ContainsICIC("!SKIP") != true) 39 | .ToList(); 40 | } 41 | 42 | 43 | /// 44 | /// Filters the data list by keeping only items that don't exist in the 45 | /// specified resx file. This finds all new items that need to be translated 46 | /// 47 | /// 48 | /// 49 | /// 50 | public static List CollectNewStrings(List data, string path) 51 | { 52 | try 53 | { 54 | var target = XElement.Load(path); 55 | 56 | return data.Where(d => 57 | // string entires always have xml:space= and do not have type= 58 | d.Attributes().Any(a => a.Name.LocalName == "space") && 59 | d.Attribute("type") == null && 60 | ( 61 | // collect all edited entries 62 | d.Element("comment")?.Value.ContainsICIC("!EDIT") == true || 63 | // collect entries that don't exist in target 64 | !target.Elements("data") 65 | .Any(e => e.Attribute("name")?.Value == d.Attribute("name").Value) 66 | )) 67 | .ToList(); 68 | } 69 | catch 70 | { 71 | // TODO: ... 72 | } 73 | 74 | return data; 75 | } 76 | 77 | 78 | /// 79 | /// Force merge hints from the specified file into the data collection. 80 | /// 81 | /// The data collection to modify 82 | /// The path of the hints file 83 | /// Count of hints applied 84 | public static int MergeHints(XElement root, XElement hints) 85 | { 86 | var count = 0; 87 | foreach (var hint in hints.Elements()) 88 | { 89 | var preferred = hint.Element("preferred").Value.Trim(); 90 | 91 | var element = root.Elements("data").FirstOrDefault(e => 92 | e.Attribute("name").Value == hint.Attribute("name").Value && 93 | e.Element("value").Value != preferred); 94 | 95 | if (element != null) 96 | { 97 | // if found here then we did not find the translated value in the !EDIT 98 | // values but found the named item in the root so overwrite... 99 | 100 | element.Element("value").Value = preferred; 101 | count++; 102 | } 103 | } 104 | 105 | return count; 106 | } 107 | 108 | 109 | /// 110 | /// Sorts all data elements of a given resource file, prioritizing strings first, 111 | /// followed by files. Files are ordered by type, then folder path, then name. 112 | /// 113 | /// The root of the resx file as an XElement 114 | public static void SortData(XElement root) 115 | { 116 | var data = root.Elements("data").ToList(); 117 | data.ForEach(d => d.Remove()); 118 | 119 | var strings = data 120 | .Where(e => e.Attribute("type") == null) 121 | .OrderBy(e => e.Attribute("name").Value) 122 | .ToList(); 123 | 124 | if (strings.Any()) 125 | { 126 | root.Add(strings); 127 | } 128 | 129 | // sort files by type, then directory path, then resource name... 130 | 131 | var files = data 132 | .Where(e => e.Attribute("type") != null) 133 | .Select(e => new { Element = e, Values = e.Element("value").Value.Split(';') }) 134 | .OrderBy(e => e.Values[1]) // type 135 | .ThenBy(e => e.Values[0]) // path 136 | .ThenBy(e => e.Element.Attribute("name").Value) 137 | .Select(e => e.Element) 138 | .ToList(); 139 | 140 | if (files.Any()) 141 | { 142 | root.Add(files); 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /ResxTranslator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {6542DD09-8229-42DD-AB6D-19AAA1488EB6} 8 | WinExe 9 | ResxTranslator 10 | ResxTranslator 11 | v4.8 12 | 512 13 | true 14 | true 15 | 16 | 17 | 18 | 19 | AnyCPU 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 28 | 29 | AnyCPU 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 37 | 38 | 39 | packages\GTranslate.2.1.1\lib\netstandard2.0\GTranslate.dll 40 | 41 | 42 | packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll 43 | 44 | 45 | ..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\System.dll 46 | 47 | 48 | packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll 49 | 50 | 51 | 52 | packages\System.Memory.4.5.5\lib\net461\System.Memory.dll 53 | 54 | 55 | 56 | packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll 57 | 58 | 59 | packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll 60 | 61 | 62 | packages\System.Text.Encodings.Web.6.0.0\lib\net461\System.Text.Encodings.Web.dll 63 | 64 | 65 | packages\System.Text.Json.6.0.5\lib\net461\System.Text.Json.dll 66 | 67 | 68 | packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll 69 | 70 | 71 | packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | Form 87 | 88 | 89 | MainWindow.cs 90 | 91 | 92 | UserControl 93 | 94 | 95 | AnalyzeControlPanel.cs 96 | 97 | 98 | Component 99 | 100 | 101 | UserControl 102 | 103 | 104 | TextControlPanel.cs 105 | 106 | 107 | UserControl 108 | 109 | 110 | ToolsControlPanel.cs 111 | 112 | 113 | 114 | 115 | True 116 | True 117 | Resources.resx 118 | 119 | 120 | 121 | 122 | 123 | 124 | MainWindow.cs 125 | Designer 126 | 127 | 128 | AnalyzeControlPanel.cs 129 | 130 | 131 | TextControlPanel.cs 132 | 133 | 134 | ToolsControlPanel.cs 135 | 136 | 137 | ResXFileCodeGenerator 138 | Designer 139 | Resources.Designer.cs 140 | 141 | 142 | 143 | SettingsSingleFileGenerator 144 | Settings.Designer.cs 145 | 146 | 147 | True 148 | Settings.settings 149 | True 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /ResxTranslator.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30621.155 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResxTranslator", "ResxTranslator.csproj", "{6542DD09-8229-42DD-AB6D-19AAA1488EB6}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {6542DD09-8229-42DD-AB6D-19AAA1488EB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {6542DD09-8229-42DD-AB6D-19AAA1488EB6}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {6542DD09-8229-42DD-AB6D-19AAA1488EB6}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {6542DD09-8229-42DD-AB6D-19AAA1488EB6}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {86F16553-67A2-440B-A718-82946574BC3E} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /SettingsProvider.cs: -------------------------------------------------------------------------------- 1 | //************************************************************************************************ 2 | // Copyright © 2021 Steven M Cohn. All rights reserved. 3 | //************************************************************************************************ 4 | 5 | namespace ResxTranslator 6 | { 7 | using System; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Reflection; 11 | using System.Xml.Linq; 12 | 13 | 14 | /// 15 | /// Loads, save, and manages user settings 16 | /// 17 | internal class SettingsProvider 18 | { 19 | private readonly string path; 20 | private readonly XElement root; 21 | 22 | 23 | /// 24 | /// Initialize a new provider. 25 | /// 26 | public SettingsProvider() 27 | { 28 | var attribute = Assembly.GetExecutingAssembly() 29 | .GetCustomAttributes() 30 | .FirstOrDefault(); 31 | 32 | var appData = Path.Combine( 33 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), 34 | attribute != null ? attribute.Product : "RoboTranslator"); 35 | 36 | path = Path.Combine(appData, "Settings.xml"); 37 | 38 | if (File.Exists(path)) 39 | { 40 | try 41 | { 42 | root = XElement.Load(path); 43 | } 44 | catch 45 | { 46 | // 47 | } 48 | } 49 | 50 | if (root == null) 51 | { 52 | // file not found so initialize with defaults 53 | root = new XElement("settings"); 54 | } 55 | } 56 | 57 | 58 | public string Get(string name) 59 | { 60 | var element = root.Elements(name).FirstOrDefault(); 61 | if (element != null) 62 | { 63 | return element.Value; 64 | } 65 | 66 | return null; 67 | } 68 | 69 | 70 | public void Set(string name, string value) 71 | { 72 | var element = root.Elements(name).FirstOrDefault(); 73 | if (element == null) 74 | { 75 | root.Add(new XElement(name, value)); 76 | } 77 | else 78 | { 79 | element.Value = value; 80 | } 81 | } 82 | 83 | 84 | public void Save() 85 | { 86 | var dir = Path.GetDirectoryName(path); 87 | if (!Directory.Exists(dir)) 88 | { 89 | try 90 | { 91 | Directory.CreateDirectory(dir); 92 | } 93 | catch 94 | { 95 | // 96 | } 97 | } 98 | 99 | root.Save(path, SaveOptions.None); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /StringExtensions.cs: -------------------------------------------------------------------------------- 1 | //************************************************************************************************ 2 | // Copyright © 2020 Steven M Cohn. All rights reserved. 3 | //************************************************************************************************ 4 | 5 | namespace ResxTranslator 6 | { 7 | using System; 8 | using System.Text; 9 | 10 | 11 | /// 12 | /// This is a customization of the System.Security.SecurityElement.Escape method 13 | /// that ignores apostrophies (') and double-quotes (") 14 | /// 15 | internal static class StringExtensions 16 | { 17 | private static readonly string[] EscapePairs = new string[] 18 | { 19 | // these must be all once character escape sequences or a new escaping algorithm is needed 20 | "<", "<", 21 | ">", ">", 22 | "&", "&" 23 | }; 24 | 25 | private static readonly char[] EscapeChars = new char[] { '<', '>', '&' }; 26 | 27 | 28 | /// 29 | /// Compares a string against the given instance, as non-case-sensitive. 30 | /// 31 | /// The string instance 32 | /// The other string for comparison 33 | /// True if the instance contains at least one occurance of value 34 | public static bool ContainsICIC(this string s, string value) 35 | { 36 | return s.IndexOf(value, StringComparison.InvariantCultureIgnoreCase) >= 0; 37 | } 38 | 39 | 40 | /// 41 | /// Escapes special XML characters in the given text values so those characters 42 | /// survive round-trips as user-input text 43 | /// 44 | /// The user input string 45 | /// The user string with special XML characters escaped 46 | public static string XmlEscape(this string str) 47 | { 48 | if (str == null) 49 | return null; 50 | 51 | StringBuilder sb = null; 52 | 53 | int strLen = str.Length; 54 | int index; // Pointer into the string that indicates the location of the current '&' character 55 | int newIndex = 0; // Pointer into the string that indicates the start index of the "remaining" string (that still needs to be processed). 56 | 57 | do 58 | { 59 | index = str.IndexOfAny(EscapeChars, newIndex); 60 | 61 | if (index == -1) 62 | { 63 | if (sb == null) 64 | return str; 65 | else 66 | { 67 | sb.Append(str, newIndex, strLen - newIndex); 68 | return sb.ToString(); 69 | } 70 | } 71 | else 72 | { 73 | if (sb == null) 74 | sb = new StringBuilder(); 75 | 76 | sb.Append(str, newIndex, index - newIndex); 77 | sb.Append(GetEscapeSequence(str[index])); 78 | 79 | newIndex = (index + 1); 80 | } 81 | } 82 | while (true); 83 | 84 | // no normal exit is possible 85 | } 86 | 87 | 88 | private static string GetEscapeSequence(char c) 89 | { 90 | int iMax = EscapePairs.Length; 91 | 92 | for (int i = 0; i < iMax; i += 2) 93 | { 94 | string strEscSeq = EscapePairs[i]; 95 | string strEscValue = EscapePairs[i + 1]; 96 | 97 | if (strEscSeq[0] == c) 98 | return strEscValue; 99 | } 100 | 101 | return c.ToString(); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /Translator.cs: -------------------------------------------------------------------------------- 1 | //************************************************************************************************ 2 | // Copyright © 2020 Steven M Cohn. All rights reserved. 3 | //************************************************************************************************ 4 | 5 | #pragma warning disable S125 // commented out code 6 | 7 | namespace ResxTranslator 8 | { 9 | using GTranslate.Translators; 10 | using System.Collections.Generic; 11 | using System.Drawing; 12 | using System.Globalization; 13 | using System.IO; 14 | using System.Linq; 15 | using System.Text; 16 | using System.Text.RegularExpressions; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | using System.Xml.Linq; 20 | 21 | 22 | internal enum Status 23 | { 24 | OK, 25 | Working, 26 | Error, 27 | Message 28 | } 29 | 30 | 31 | internal class Translator 32 | { 33 | //private const int PerEstimate = 1250; 34 | private const int PerEstimate = 100; 35 | 36 | 37 | public static readonly List Codes = new List 38 | { 39 | #region Code 40 | "af", // Afrikaans 41 | "sq", // Albanian 42 | "am", // Amharic 43 | "ar", // Arabic 44 | "hy", // Armenian 45 | "az", // Azerbaijani 46 | "eu", // Basque 47 | "be", // Belarusian 48 | "bn", // Bengali 49 | "bs", // Bosnian 50 | "bg", // Bulgarian 51 | "ca", // Catalan 52 | "ceb", // Cebuano 53 | "ny", // Chichewa - (n/a) 54 | "zh-CN", // Chinese (Simplified) 55 | "zh-TW", // Chinese (Traditional) 56 | "co", // Corsican 57 | "hr", // Croatian 58 | "cs", // Czech 59 | "da", // Danish 60 | "nl", // Dutch 61 | "en", // English 62 | "eo", // Esperanto 63 | "et", // Estonian 64 | "tl", // Filipino = fil-PH 65 | "fi", // Finnish 66 | "fr", // French 67 | "fy", // Frisian 68 | "gl", // Galician 69 | "ka", // Georgian 70 | "de", // German 71 | "el", // Greek 72 | "gu", // Gujarati 73 | "ha", // Hausa 74 | "ht", // Haitian Creole... not supported by Windows? 75 | "haw", // Hawaiian 76 | "he", // Hebrew = he-IL 77 | "hi", // Hindi 78 | "hmn", // Hmong - (n/a) 79 | "hu", // Hungarian 80 | "is", // Icelandic 81 | "ig", // Igbo 82 | "id", // Indonesian 83 | "ga", // Irish 84 | "it", // Italian 85 | "ja", // Japanese 86 | "jw", // Javanese - (n/a) 87 | "kn", // Kannada 88 | "kk", // Kazakh 89 | "km", // Khmer 90 | "rw", // Kinyarwanda 91 | "ko", // Korean 92 | "ku", // Kurdish (Kurmanji) 93 | "ky", // Kyrgyz 94 | "lo", // Lao 95 | "la", // Latin 96 | "lv", // Latvian 97 | "lt", // Lithuanian 98 | "lb", // Luxembourgish 99 | "mk", // Macedonian 100 | "mg", // Malagasy 101 | "ms", // Malay 102 | "ml", // Malayalam 103 | "mt", // Maltese 104 | "mi", // Maori 105 | "mr", // Marathi 106 | "mn", // Mongolian 107 | "my", // Myanmar (Burmese) 108 | "ne", // Nepali 109 | "no", // Norwegian 110 | "or", // Odia (Oriya) 111 | "ps", // Pashto 112 | "fa", // Persian 113 | "pl", // Polish 114 | "pt", // Portuguese 115 | "pa", // Punjabi 116 | "ro", // Romanian 117 | "ru", // Russian 118 | "sm", // Samoan - (n/a) 119 | "sr", // Serbian 120 | "gd", // Scots Gaelic 121 | "st", // Sesotho 122 | "sn", // Shona 123 | "sd", // Sindhi 124 | "si", // Sinhala 125 | "sk", // Slovak 126 | "sl", // Slovenian 127 | "so", // Somali 128 | "es", // Spanish 129 | "su", // Sundanese - (n/a) 130 | "sw", // Swahili 131 | "sv", // Swedish 132 | "tg", // Tajik 133 | "ta", // Tamil 134 | "tt", // Tatar 135 | "te", // Telugu 136 | "th", // Thai 137 | "tr", // Turkish 138 | "tk", // Turkmen 139 | "uk", // Ukrainian 140 | "ur", // Urdu 141 | "ug", // Uyghur 142 | "uz", // Uzbek 143 | "vi", // Vietnamese 144 | "cy", // Welsh 145 | "xh", // Xhosa 146 | "yi", // Yiddis 147 | "yo", // Yoruba 148 | "zu" // Zulu 149 | #endregion Code 150 | }; 151 | 152 | private static readonly Dictionary FileCodeMap = new Dictionary 153 | { 154 | { "tl", "fil-PH" }, 155 | { "iw", "he-IL" } 156 | }; 157 | 158 | private static readonly Dictionary FileNameMap = new Dictionary 159 | { 160 | { "ny", "Chichewa" }, 161 | { "hmn", "Hmong" }, 162 | { "ht", "Haitian Creole" }, 163 | { "jw", "Javanese" }, 164 | { "sm", "Samoan" }, 165 | { "su", "Sudanese" } 166 | }; 167 | 168 | private const string NL = "\n"; 169 | private const string InflationPattern = @"([^\s](?:[^\w\d\s\p{L}]))|((?:[^\w\d\s\p{L}])[^\s])"; 170 | 171 | private readonly Regex inflation = new Regex(InflationPattern); 172 | 173 | 174 | /// 175 | /// Hints root node 176 | /// 177 | public XElement Hints { get; private set; } 178 | 179 | 180 | // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 181 | // Languages... 182 | 183 | /// 184 | /// Gets the Windows-specific culture name, e.g. en -> en-US, for naming 185 | /// satellite assemblies 186 | /// 187 | /// 188 | /// 189 | public static string GetCultureName(string code) 190 | { 191 | return FileCodeMap.ContainsKey(code) 192 | ? FileCodeMap[code] 193 | : CultureInfo.GetCultureInfo(code).TextInfo.CultureName; 194 | } 195 | 196 | 197 | /// 198 | /// Gets the display name of the language for UI controls 199 | /// 200 | /// 201 | /// 202 | public static string GetDisplayName(string code) 203 | { 204 | if (FileCodeMap.ContainsKey(code)) 205 | { 206 | code = FileCodeMap[code]; 207 | } 208 | 209 | return FileNameMap.ContainsKey(code) 210 | ? FileNameMap[code] 211 | : CultureInfo.GetCultureInfo(code).DisplayName; 212 | } 213 | 214 | 215 | // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 216 | // Data Management... 217 | 218 | /// 219 | /// 220 | /// 221 | /// 222 | /// 223 | /// 224 | /// 225 | public static bool Estimate(string path, out int strings, out int seconds) 226 | { 227 | return Estimate(path, out strings, PerEstimate, out seconds); 228 | } 229 | 230 | 231 | public static bool Estimate(string path, out int strings, int delayInMs, out int seconds) 232 | { 233 | try 234 | { 235 | var root = XElement.Load(path); 236 | var data = ResxProvider.CollectStrings(root); 237 | 238 | strings = data.Count; 239 | seconds = data.Count * delayInMs / 1000; 240 | 241 | return true; 242 | } 243 | catch 244 | { 245 | strings = seconds = 0; 246 | return false; 247 | } 248 | } 249 | 250 | 251 | // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 252 | // Inflation test... 253 | 254 | public bool Inflated(string original, string translation) 255 | { 256 | if (original == null || translation == null) 257 | { 258 | return false; 259 | } 260 | 261 | var oi = inflation.Matches(original).Count; 262 | var ti = inflation.Matches(translation).Count; 263 | return oi != ti; 264 | } 265 | 266 | 267 | // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 268 | // One... 269 | 270 | /// 271 | /// 272 | /// 273 | /// 274 | /// 275 | /// 276 | /// 277 | public async Task Translate( 278 | string text, string fromCode, string toCode, CancellationTokenSource cancellation, 279 | StatusCallback logger = null) 280 | { 281 | if (text.Trim() == string.Empty) 282 | { 283 | return text; 284 | } 285 | 286 | using (var translator = new AggregateTranslator()) 287 | { 288 | if (fromCode == "auto") 289 | { 290 | var lang = await translator.DetectLanguageAsync(text); 291 | fromCode = lang.ISO6391; 292 | } 293 | 294 | var result = await translator.TranslateAsync(text, toCode, fromCode); 295 | return result.Translation; 296 | } 297 | } 298 | 299 | 300 | // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 301 | // Resx... 302 | 303 | /* 304 | * 305 | * NOTE, tried to batch up strings, up to 1K of chars, with a delimiter, 306 | * but the free Google Translator doesn't handle most languages correctly; 307 | * e.g. given a batch with 10 strings, Google may only return 7 of them 308 | * and it's unclear which 7 it will return? 309 | * 310 | */ 311 | 312 | public delegate void StatusCallback(string message, Color? color = null, bool increment = false); 313 | 314 | 315 | public void LoadHints(string path) 316 | { 317 | if (File.Exists(path)) 318 | { 319 | try 320 | { 321 | Hints = XElement.Load(path); 322 | return; 323 | } 324 | catch 325 | { 326 | // no-op 327 | } 328 | } 329 | 330 | Hints = new XElement("hints"); 331 | } 332 | 333 | 334 | /// 335 | /// 336 | /// 337 | /// 338 | /// 339 | /// 340 | /// 341 | /// 342 | public async Task TranslateResx( 343 | List data, string fromCode, string toCode, 344 | CancellationTokenSource cancellation, 345 | StatusCallback logger) 346 | { 347 | int index = 0; 348 | int count = 1; 349 | for (; index < data.Count && !cancellation.IsCancellationRequested; index++, count++) 350 | { 351 | var value = data[index].Element("value").Value; 352 | 353 | if (value.Length == 0) 354 | { 355 | continue; 356 | } 357 | 358 | var editing = data[index].Element("comment")?.Value.ContainsICIC("!EDIT") == true; 359 | 360 | var name = editing 361 | ? $"{data[index].Attribute("name").Value} (EDITED)" 362 | : data[index].Attribute("name").Value; 363 | 364 | logger($"{count}/{data.Count}: {name}", increment: true); 365 | 366 | var hint = FindHint(data[index], logger); 367 | if (hint != null) 368 | { 369 | data[index].Element("value").Value = hint; 370 | 371 | // 2192 is right-arrow 372 | logger($" \u2192 using hint override '{hint}'" + NL, Color.SteelBlue); 373 | 374 | if (editing) 375 | { 376 | logger("Hint override will likely need updating", Color.Red); 377 | } 378 | } 379 | else 380 | { 381 | var builder = new StringBuilder(); 382 | 383 | // handle values with multiple lines (comboboxes) 384 | var parts = value.Split('\n'); 385 | for (int i = 0; i < parts.Length && !cancellation.IsCancellationRequested; i++) 386 | { 387 | if (parts[i].Trim().Length == 0) 388 | { 389 | builder.Append(NL); 390 | } 391 | else 392 | { 393 | var result = await TranslateWithRetry( 394 | parts[i], fromCode, toCode, cancellation, logger); 395 | 396 | if (!string.IsNullOrEmpty(result) && !cancellation.IsCancellationRequested) 397 | { 398 | builder.Append(result); 399 | 400 | if (i < parts.Length - 1) 401 | builder.Append(NL); 402 | } 403 | } 404 | } 405 | 406 | if (builder.Length > 0) 407 | { 408 | var result = builder.ToString(); 409 | data[index].Element("value").Value = result; 410 | 411 | // 2192 is right-arrow 412 | logger($" \u2192 '{value}' to '{result}'" + NL); 413 | 414 | if (Inflated(value, result)) 415 | { 416 | logger("*** possible inflation detected ***" + NL, Color.Maroon); 417 | } 418 | } 419 | else 420 | { 421 | logger("unknown error" + NL, Color.Red); 422 | } 423 | } 424 | } 425 | 426 | return index == data.Count; 427 | } 428 | 429 | 430 | private string FindHint(XElement data, StatusCallback logger) 431 | { 432 | var name = data.Attribute("name").Value; 433 | var hint = Hints.Elements() 434 | .FirstOrDefault(e => e.Attribute("name").Value == name); 435 | 436 | if (hint != null) 437 | { 438 | if (data.Element("value").Value.Trim() == hint.Element("source").Value.Trim()) 439 | { 440 | return hint.Element("preferred").Value.Trim(); 441 | } 442 | 443 | logger($" \u2192 hint override is obsolete", Color.Maroon); 444 | } 445 | 446 | return null; 447 | } 448 | 449 | 450 | private async Task TranslateWithRetry( 451 | string text, string fromCode, string toCode, 452 | CancellationTokenSource cancellation, StatusCallback logger) 453 | { 454 | using (var translator = new AggregateTranslator()) 455 | { 456 | var result = await translator.TranslateAsync(text, toCode, fromCode); 457 | return result.Translation; 458 | } 459 | } 460 | 461 | 462 | /// 463 | /// Clear the !EDIT marker in the given data element 464 | /// 465 | /// A translation data element with a comment child 466 | public static string ClearMarker(string comment) 467 | { 468 | return Regex.Replace(comment, @"\b!EDIT\b", string.Empty, RegexOptions.IgnoreCase); 469 | } 470 | 471 | 472 | /// 473 | /// Remove the !EDIT markers from the source file after we're done applying 474 | /// the changes to all output files 475 | /// 476 | /// 477 | public static int ClearMarkers(string path) 478 | { 479 | var root = XElement.Load(path); 480 | var comments = root.Elements("data").Elements("comment") 481 | .Where(e => e.Value.ContainsICIC("!EDIT")); 482 | 483 | if (comments.Any()) 484 | { 485 | foreach (var comment in comments) 486 | { 487 | comment.Value = ClearMarker(comment.Value); 488 | } 489 | 490 | root.Save(path); 491 | } 492 | 493 | return comments.Count(); 494 | } 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------