Browse Source

Version 3.6.0

 * FEATURE - 'Translation Aggregator'-like view that enables viewing translations for displayed texts from multiple different translators (press ALT+1)
 * FEATURE - Substitution support. Enable dictionary lookup for strings (usually proper nouns) embedded in text to replace them with a manual translation
 * FEATURE - Papago translator support
 * MISC - Removed hard dependency on UnityEngine.UI to support older versions of the Unity engine
 * MISC - Automatically initialize LEC installation path if installed when creating configuration file
 * BUG FIX - Fixed bug where LEC was not working when run in a .NET 4.x equivalent runtime
randoman 5 years ago
parent
commit
7fac73b052
36 changed files with 764 additions and 192 deletions
  1. 6 1
      CHANGELOG.md
  2. 22 0
      README.md
  3. 12 1
      XUnity.AutoTranslator.sln
  4. 1 1
      src/Translators/Lec.ExtProtocol/Program.cs
  5. 48 1
      src/Translators/LecPowerTranslator15/LecPowerTranslator15Endpoint.cs
  6. 169 0
      src/Translators/PapagoTranslate/PapagoTranslate.cs
  7. 15 0
      src/Translators/PapagoTranslate/PapagoTranslate.csproj
  8. 1 1
      src/XUnity.AutoTranslator.Patcher/Patcher.cs
  9. 1 1
      src/XUnity.AutoTranslator.Plugin.BepIn-5x/XUnity.AutoTranslator.Plugin.BepIn-5x.csproj
  10. 1 1
      src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj
  11. 103 30
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs
  12. 39 1
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs
  13. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs
  14. 3 1
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ExtProtocol/ExtProtocolEndpoint.cs
  15. 9 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/IInitializationContext.cs
  16. 5 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/InitializationContext.cs
  17. 22 24
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationEndpointManager.cs
  18. 23 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/IniFileExtensions.cs
  19. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs
  20. 60 52
      src/XUnity.AutoTranslator.Plugin.Core/TemplatedString.cs
  21. 74 49
      src/XUnity.AutoTranslator.Plugin.Core/TextTranslationCache.cs
  22. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/UI/GUIUtil.cs
  23. 2 2
      src/XUnity.AutoTranslator.Plugin.Core/UI/TranslationAggregatorOptionsWindow.cs
  24. 36 4
      src/XUnity.AutoTranslator.Plugin.Core/UI/TranslationAggregatorViewModel.cs
  25. 25 3
      src/XUnity.AutoTranslator.Plugin.Core/UI/TranslatorViewModel.cs
  26. 24 10
      src/XUnity.AutoTranslator.Plugin.Core/UntranslatedText.cs
  27. 3 2
      src/XUnity.AutoTranslator.Plugin.Core/Utilities/CoroutineHelper.cs
  28. 38 0
      src/XUnity.AutoTranslator.Plugin.Core/Utilities/DebounceFunction.cs
  29. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj
  30. 1 1
      src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj
  31. 1 1
      src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj
  32. 1 0
      src/XUnity.AutoTranslator.Setup.Build/XUnity.AutoTranslator.Setup.Build.csproj
  33. 1 0
      src/XUnity.AutoTranslator.Setup/Program.cs
  34. 10 0
      src/XUnity.AutoTranslator.Setup/Properties/Resources.Designer.cs
  35. 3 0
      src/XUnity.AutoTranslator.Setup/Properties/Resources.resx
  36. 1 1
      src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj

+ 6 - 1
CHANGELOG.md

@@ -1,5 +1,10 @@
 ### 3.6.0
- * MISC - Removed hard dependency on UnityEngine.UI to support older versions of UnityEngine
+ * FEATURE - 'Translation Aggregator'-like view that enables viewing translations for displayed texts from multiple different translators (press ALT+1)
+ * FEATURE - Substitution support. Enable dictionary lookup for strings (usually proper nouns) embedded in text to replace them with a manual translation
+ * FEATURE - Papago translator support
+ * MISC - Removed hard dependency on UnityEngine.UI to support older versions of the Unity engine
+ * MISC - Automatically initialize LEC installation path if installed when creating configuration file
+ * BUG FIX - Fixed bug where LEC was not working when run in a .NET 4.x equivalent runtime
 
 ### 3.5.0
  * FEATURE - Harmony 2.0-prerelease support (in order to support BepInEx 5.0.0-RC1)

+ 22 - 0
README.md

@@ -148,6 +148,7 @@ The file structure should like like this
 ## Key Mapping
 The following key inputs are mapped:
  * ALT + 0: Toggle XUnity AutoTranslator UI. (That's a zero, not an O)
+ * ALT + 1: Toggle Translation Aggregator UI.
  * ALT + T: Alternate between translated and untranslated versions of all texts provided by this plugin.
  * ALT + R: Reload translation files. Useful if you change the text and texture files on the fly. Not guaranteed to work for all textures.
  * ALT + U: Manual hooking. The default hooks wont always pick up texts. This will attempt to make lookups manually.
@@ -164,6 +165,8 @@ The supported translators are:
    * No limitations, but unstable.
  * [BingTranslateLegitimate](https://anonym.to/?https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-info-overview), based on the Azure text translation. Requires an API key.
    * Free up to 2 million characters per month.
+ * [PapagoTranslate](https://anonym.to/?https://papago.naver.com/), based on the online Google translation service. Does not require authentication.
+   * No limitations, but unstable.
  * [BaiduTranslate](https://anonym.to/?https://fanyi.baidu.com/), based on Baidu translation service. Requires AppId and AppSecret.
    * Not sure on quotas on this one.
  * [YandexTranslate](https://anonym.to/?https://tech.yandex.com/translate/), based on the Yandex translation service. Requires an API key.
@@ -231,6 +234,7 @@ FromLanguage=ja                  ;The original language of the game
 [Files]
 Directory=Translation                                          ;Directory to search for cached translation files. Can use placeholder: {GameExeName}
 OutputFile=Translation\_AutoGeneratedTranslations.{lang}.txt   ;File to insert generated translations into. Can use placeholders: {GameExeName}, {lang}
+SubstitutionFile=Translation\_Substitutions.{lang}.txt         ;File that contains substitution applied before translations. Can use placeholders: {GameExeName}, {lang}
 
 [TextFrameworks]
 EnableUGUI=True                  ;Enable or disable UGUI translation
@@ -279,6 +283,11 @@ DetectDuplicateTextureNames=False;Indicates if the plugin should detect duplicat
 UserAgent=                       ;Override the user agent used by APIs requiring a user agent
 DisableCertificateValidation=False ;Indiciates whether certificate validations for the .NET API should be disabled
 
+[TranslationAggregator]
+Width=400                        ;The total width of the translation aggregator window.
+Height=100                       ;The width (per translator) of the translation aggregator window.
+EnabledTranslators=              ;The id's of the translation endpoints that has been enabled in the translation aggregator window. List is separated by ';'.
+
 [GoogleLegitimate]
 GoogleAPIKey=                    ;OPTIONAL, needed if GoogleTranslateLegitimate is configured
 
@@ -408,6 +417,19 @@ It is also worth noting that this plugin will read all text files (*.txt) in the
 
 In this context, the `Translation\_AutoGeneratedTranslations.{lang}.txt` (OutputFile) will always have the lowest priority when reading translations. So if the same translation is present in two places, it will not be the one from the (OutputFile) that is used.
 
+### Substitutions
+It is also possible to add substitutions that are applied to found texts before translations are created. This is controlled through the `SubstitutionFile`, which uses the same format as normal translation text files, although things like regexes are not supported.
+
+This is useful for replacing names that are often translated incorrectly, etc.
+
+When using substitutions, the found occurrences will be parameterized in the generated translations, like so:
+
+```
+私は{{A}}=I am {{A}}
+```
+
+When creating manual translations, use this file as sparingly as you would use regexes, as it can have an effect on performance.
+
 ## Regarding Redistribution
 Redistributing this plugin for various games is absolutely encouraged. However, if you do so, please keep the following in mind:
  * **Distribute the _AutoGeneratedTranslations.{lang}.txt file along with the redistribution with as many translations as possible to ensure the online translator is hit as little as possible.**

+ 12 - 1
XUnity.AutoTranslator.sln

@@ -85,7 +85,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.RuntimeHooker.Benchm
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.BepIn-5x", "src\XUnity.AutoTranslator.Plugin.BepIn-5x\XUnity.AutoTranslator.Plugin.BepIn-5x.csproj", "{ADCCF172-7D31-42C6-B9D4-1779EAC8B403}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnity.AutoTranslator.Plugin.Core.Tests", "test\XUnity.AutoTranslator.Plugin.Core.Tests\XUnity.AutoTranslator.Plugin.Core.Tests.csproj", "{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.Core.Tests", "test\XUnity.AutoTranslator.Plugin.Core.Tests\XUnity.AutoTranslator.Plugin.Core.Tests.csproj", "{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PapagoTranslate", "src\Translators\PapagoTranslate\PapagoTranslate.csproj", "{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -311,6 +313,14 @@ Global
 		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Release|Any CPU.Build.0 = Release|Any CPU
 		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Release|x86.ActiveCfg = Release|Any CPU
 		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Release|x86.Build.0 = Release|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Debug|x86.Build.0 = Debug|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Release|x86.ActiveCfg = Release|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -344,6 +354,7 @@ Global
 		{E2F50278-9134-4DC8-9C50-4C0A52063A89} = {2A4A3DDF-338C-40C0-8E26-2A810BAAADD6}
 		{ADCCF172-7D31-42C6-B9D4-1779EAC8B403} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
 		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD} = {2A4A3DDF-338C-40C0-8E26-2A810BAAADD6}
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {EE803FED-4447-4D19-B3D6-88C56E8DFCCA}

+ 1 - 1
src/Translators/Lec.ExtProtocol/Program.cs

@@ -44,7 +44,7 @@ namespace Lec.ExtProtocol
                      if( message == null ) return;
 
                      var translatedTexts = new string[ message.UntranslatedTexts.Length ];
-                     for( int i = 0 ; i < message.UntranslatedTexts.Length ; i++ )
+                     for( int i = 0; i < message.UntranslatedTexts.Length; i++ )
                      {
                         var untranslatedText = message.UntranslatedTexts[ i ];
                         var translatedText = translator.Translate( untranslatedText );

+ 48 - 1
src/Translators/LecPowerTranslator15/LecPowerTranslator15Endpoint.cs

@@ -21,7 +21,15 @@ namespace LecPowerTranslator15
 
       public override void Initialize( IInitializationContext context )
       {
-         var pathToLec = context.GetOrCreateSetting( "LecPowerTranslator15", "InstallationPath", "" );
+         var defaultPath = GetDefaultInstallationPath();
+         var pathToLec = context.GetOrCreateSetting( "LecPowerTranslator15", "InstallationPath", defaultPath );
+
+         if( string.IsNullOrEmpty( pathToLec ) && !string.IsNullOrEmpty( defaultPath ) )
+         {
+            context.SetSetting( "LecPowerTranslator15", "InstallationPath", defaultPath );
+            pathToLec = defaultPath;
+         }
+
          if( string.IsNullOrEmpty( pathToLec ) ) throw new Exception( "The LecPowerTranslator15 requires the path to the installation folder." );
 
          var exePath = Path.Combine( context.PluginDirectory, @"Translators\Lec.ExtProtocol.exe" );
@@ -35,5 +43,44 @@ namespace LecPowerTranslator15
          if( context.SourceLanguage != "ja" ) throw new Exception( "Current implementation only supports japanese-to-english." );
          if( context.DestinationLanguage != "en" ) throw new Exception( "Current implementation only supports japanese-to-english." );
       }
+
+      public static string GetDefaultInstallationPath()
+      {
+         try
+         {
+            var path = GetInstallationPathFromRegistry();
+
+            if( !string.IsNullOrEmpty( path ) )
+            {
+               var di = new DirectoryInfo( path );
+               path = di.Parent.FullName;
+            }
+
+            return path ?? string.Empty;
+         }
+         catch
+         {
+            return string.Empty;
+         }
+      }
+
+      public static string GetInstallationPathFromRegistry()
+      {
+         try
+         {
+            if( IntPtr.Size == 8 ) // 64-bit
+            {
+               return (string)Microsoft.Win32.Registry.GetValue( @"HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\LogoMedia\LEC Power Translator 15\Configuration", "ApplicationPath", null );
+            }
+            else // 32-bit
+            {
+               return (string)Microsoft.Win32.Registry.GetValue( @"HKEY_LOCAL_MACHINE\SOFTWARE\LogoMedia\LEC Power Translator 15\Configuration", "ApplicationPath", null );
+            }
+         }
+         catch
+         {
+            return null;
+         }
+      }
    }
 }

+ 169 - 0
src/Translators/PapagoTranslate/PapagoTranslate.cs

@@ -0,0 +1,169 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using SimpleJSON;
+using XUnity.AutoTranslator.Plugin.Core;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints.Http;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace PapagoTranslate
+{
+   public class PapagoTranslate : HttpEndpoint
+   {
+      private static readonly HashSet<string> SupportedLanguages = new HashSet<string> { "en", "ko", "zh-CN", "zh-TW", "es", "fr", "ru", "vi", "th", "id", "de", "ja" };
+
+      private static readonly string Url = "https://papago.naver.com/apis/n2mt/translate";
+      private static readonly string Website = "https://papago.naver.com";
+      private static readonly string JsonTemplate = "{{\"deviceId\":\"{0}\",\"dict\":false,\"dictDisplay\":0,\"honorific\":false,\"instant\":false,\"source\":\"{1}\",\"target\":\"{2}\",\"text\":\"{3}\"}}";
+      private static readonly string FormUrlEncodedTemplate = "data={0}";
+
+      private CookieContainer _cookies;
+      private string _deviceId;
+      private int _translationCount = 0;
+
+      public override string Id => "PapagoTranslate";
+
+      public override string FriendlyName => "Papago Translator";
+
+      public override int MaxTranslationsPerRequest => 10;
+
+      public override void Initialize( IInitializationContext context )
+      {
+         context.DisableCertificateChecksFor( "papago.naver.com" );
+
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The language '{context.DestinationLanguage}' is not supported by Papago Translate." );
+      }
+
+      public override IEnumerator OnBeforeTranslate( IHttpTranslationContext context )
+      {
+         if( _translationCount % 133 == 0 )
+         {
+            _cookies = new CookieContainer();
+            _deviceId = Guid.NewGuid().ToString();
+
+            // terminate session?????
+
+            var client = new XUnityWebClient();
+            var request = new XUnityWebRequest( Website );
+            request.Cookies = _cookies;
+
+            SetupDefaultHeaders( request );
+
+            var response = client.Send( request );
+            while( response.MoveNext() ) yield return response.Current;
+
+            // dont actually cared about the response, just the cookies
+         }
+      }
+
+      public override void OnCreateRequest( IHttpRequestCreationContext context )
+      {
+         var fullTranslationText = string.Join( "\n", context.UntranslatedTexts );
+         var jsonString = string.Format( JsonTemplate, _deviceId, context.SourceLanguage, context.DestinationLanguage, JsonHelper.Escape( fullTranslationText ) );
+         var base64 = Convert.ToBase64String( Encoding.UTF8.GetBytes( jsonString ) );
+         var obfuscatedBase64 = Obfuscate( 16, base64 );
+         var data = string.Format( FormUrlEncodedTemplate, Uri.EscapeDataString( obfuscatedBase64 ) );
+
+         var request = new XUnityWebRequest( "POST", Url, data );
+         request.Cookies = _cookies;
+
+         SetupDefaultHeaders( request );
+         SetupApiRequestHeaders( request );
+
+         context.Complete( request );
+
+         _translationCount++;
+      }
+
+      public override void OnExtractTranslation( IHttpTranslationExtractionContext context )
+      {
+         var obj = JSON.Parse( context.Response.Data ).AsObject;
+         var token = obj[ "translatedText" ].ToString();
+         var fullTranslatedText = JsonHelper.Unescape( token.Substring( 1, token.Length - 2 ) );
+
+         if( context.UntranslatedTexts.Length == 1 )
+         {
+            context.Complete( fullTranslatedText );
+         }
+         else
+         {
+            var splittedTranslations = fullTranslatedText.Split( '\n' );
+            var allTranslations = new string[ context.UntranslatedTexts.Length ];
+            int idx = 0;
+            for( int i = 0; i < context.UntranslatedTexts.Length; i++ )
+            {
+               var untranslatedLines = context.UntranslatedTexts[ i ].Split( '\n' );
+
+               StringBuilder builder = new StringBuilder();
+               for( int j = 0; j < untranslatedLines.Length; j++ )
+               {
+                  var translatedLine = splittedTranslations[ idx++ ];
+                  if( untranslatedLines.Length - 1 == j )
+                  {
+                     builder.Append( translatedLine );
+                  }
+                  else
+                  {
+                     builder.AppendLine( translatedLine );
+                  }
+               }
+
+               allTranslations[ i ] = builder.ToString();
+            }
+
+            if( idx != splittedTranslations.Length ) context.Fail( "Received invalid number of translations in batch." );
+
+            context.Complete( allTranslations );
+         }
+      }
+
+      private static void SetupDefaultHeaders( XUnityWebRequest request )
+      {
+         request.Headers[ HttpRequestHeader.UserAgent ] = string.IsNullOrEmpty( AutoTranslatorSettings.UserAgent ) ? UserAgents.Chrome_Win10_Latest : AutoTranslatorSettings.UserAgent;
+         request.Headers[ "Accept-Language" ] = "en-US";
+      }
+
+      private static void SetupApiRequestHeaders( XUnityWebRequest request )
+      {
+         request.Headers[ "device-type" ] = "pc";
+         request.Headers[ "Accept" ] = "application/json";
+         request.Headers[ "x-apigw-partnerid" ] = "papago";
+         request.Headers[ "Content-Type" ] = "application/x-www-form-urlencoded; charset=UTF-8";
+         request.Headers[ "Origin" ] = "https://papago.naver.com";
+         request.Headers[ "Referer" ] = "https://papago.naver.com/";
+      }
+
+      private static string Obfuscate( int count, string str )
+      {
+         var builder = new StringBuilder();
+         for( int i = 0; i < count; i++ )
+         {
+            var c = str[ i ];
+            if( ( 'a' <= c && c <= 'm' ) || ( 'A' <= c && c <= 'M' ) )
+            {
+               c += (char)13;
+
+            }
+            else if( ( 'n' <= c && c <= 'z' ) || 'N' <= c && c <= 'Z' )
+            {
+               c -= (char)13;
+            }
+
+            builder.Append( c );
+         }
+
+         for( int i = count; i < str.Length; i++ )
+         {
+            builder.Append( str[ i ] );
+         }
+
+         return builder.ToString();
+      }
+   }
+}

+ 15 - 0
src/Translators/PapagoTranslate/PapagoTranslate.csproj

@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net35</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\XUnity.AutoTranslator.Plugin.Core\XUnity.AutoTranslator.Plugin.Core.csproj" />
+  </ItemGroup>
+
+  <Target Name="PostBuild" AfterTargets="PostBuildEvent">
+    <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\Translators\&quot;&#xD;&#xA;)" />
+  </Target>
+
+</Project>

+ 1 - 1
src/XUnity.AutoTranslator.Patcher/Patcher.cs

@@ -29,7 +29,7 @@ namespace XUnity.AutoTranslator.Patcher
       {
          get
          {
-            return "3.5.0";
+            return "3.6.0";
          }
       }
 

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.BepIn-5x/XUnity.AutoTranslator.Plugin.BepIn-5x.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <TargetFramework>net35</TargetFramework>
-    <Version>3.5.0</Version>
+    <Version>3.6.0</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.5.0</Version>
+      <Version>3.6.0</Version>
    </PropertyGroup>
 
    <ItemGroup>

+ 103 - 30
src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs

@@ -147,11 +147,10 @@ namespace XUnity.AutoTranslator.Plugin.Core
             DisableAutoTranslator();
 
             MainWindow = new XuaWindow( CreateXuaViewModel() );
-
-            // UNRELEASED: Not included in current release
-            //var vm = CreateTranslationAggregatorViewModel();
-            //TranslationAggregatorWindow = new TranslationAggregatorWindow( vm );
-            //TranslationAggregatorOptionsWindow = new TranslationAggregatorOptionsWindow( vm );
+            
+            var vm = CreateTranslationAggregatorViewModel();
+            TranslationAggregatorWindow = new TranslationAggregatorWindow( vm );
+            TranslationAggregatorOptionsWindow = new TranslationAggregatorOptionsWindow( vm );
          }
          catch( Exception e )
          {
@@ -584,7 +583,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
       internal void SetTranslatedText( object ui, string translatedText, TextTranslationInfo info )
       {
          info?.SetTranslatedText( translatedText );
-         
+
          if( _isInTranslatedMode && !CallOrigin.ExpectsTextToBeReturned )
          {
             SetText( ui, translatedText, true, info );
@@ -988,6 +987,13 @@ namespace XUnity.AutoTranslator.Plugin.Core
             var isSpammer = ui.IsSpammingComponent();
             var textKey = GetCacheKey( ui, text, isSpammer, false );
 
+            // potentially shortcircuit if fully templated
+            if( text != textKey.TemplatedOriginalText && !TextCache.IsTranslatable( textKey.TemplatedOriginalText ) )
+            {
+               var untemplatedTranslation = textKey.Untemplate( textKey.TemplatedOriginalText );
+               return untemplatedTranslation;
+            }
+
             // if we already have translation loaded in our _translatios dictionary, simply load it and set text
             string translation;
             if( TextCache.TryGetTranslation( textKey, false, out translation ) )
@@ -1035,13 +1041,21 @@ namespace XUnity.AutoTranslator.Plugin.Core
          {
             var textKey = GetCacheKey( null, text, false, context != null );
 
+            // potentially shortcircuit if fully templated
+            if( text != textKey.TemplatedOriginalText && !endpoint.IsTranslatable( textKey.TemplatedOriginalText ) )
+            {
+               var untemplatedTranslation = textKey.Untemplate( textKey.TemplatedOriginalText );
+               result.SetCompleted( untemplatedTranslation, true );
+               return result;
+            }
+
             // if we already have translation loaded in our _translatios dictionary, simply load it and set text
             string translation;
             if( endpoint.TryGetTranslation( textKey, out translation ) )
             {
                if( !string.IsNullOrEmpty( translation ) )
                {
-                  result.SetCompleted( translation, true );
+                  result.SetCompleted( textKey.Untemplate( translation ), true );
                }
                else
                {
@@ -1103,16 +1117,25 @@ namespace XUnity.AutoTranslator.Plugin.Core
             var untranslatedTextPart = kvp.Value;
             if( !string.IsNullOrEmpty( untranslatedTextPart ) && endpoint.IsTranslatable( untranslatedTextPart ) && IsBelowMaxLength( untranslatedTextPart ) )
             {
-               string partTranslation;
-               if( endpoint.TryGetTranslation( new UntranslatedText( untranslatedTextPart, false, false ), out partTranslation ) )
+               var textKey = new UntranslatedText( untranslatedTextPart, false, false );
+               if( endpoint.IsTranslatable( textKey.TemplatedOriginalText ) )
                {
-                  translations.Add( variableName, partTranslation );
+                  string partTranslation;
+                  if( endpoint.TryGetTranslation( textKey, out partTranslation ) )
+                  {
+                     translations.Add( variableName, textKey.Untemplate( partTranslation ) );
+                  }
+                  else if( allowStartJob )
+                  {
+                     // incomplete, must start job
+                     var context = new ParserTranslationContext( null, endpoint, translationResult, result );
+                     Translate( untranslatedTextPart, endpoint, context );
+                  }
                }
-               else if( allowStartJob )
+               else
                {
-                  // incomplete, must start job
-                  var context = new ParserTranslationContext( null, endpoint, translationResult, result );
-                  Translate( untranslatedTextPart, endpoint, context );
+                  // the template itself does not require a translation, which means the untranslated template equals the translated text
+                  translations.Add( variableName, textKey.Untemplate( textKey.TemplatedOriginalText ) );
                }
             }
             else
@@ -1152,6 +1175,17 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
             var textKey = GetCacheKey( ui, text, isSpammer, context != null );
 
+            // potentially shortcircuit if fully templated
+            if( text != textKey.TemplatedOriginalText && !TextCache.IsTranslatable( textKey.TemplatedOriginalText ) )
+            {
+               var untemplatedTranslation = textKey.Untemplate( textKey.TemplatedOriginalText );
+               if( context == null )
+               {
+                  SetTranslatedText( ui, untemplatedTranslation, info );
+               }
+               return untemplatedTranslation;
+            }
+
             // if we already have translation loaded in our _translatios dictionary, simply load it and set text
             string translation;
             if( TextCache.TryGetTranslation( textKey, !isSpammer, out translation ) )
@@ -1163,11 +1197,12 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                if( !string.IsNullOrEmpty( translation ) )
                {
+                  var untemplatedTranslation = textKey.Untemplate( translation );
                   if( context == null ) // never set text if operation is contextualized (only a part translation)
                   {
-                     SetTranslatedText( ui, textKey.Untemplate( translation ), info );
+                     SetTranslatedText( ui, untemplatedTranslation, info );
                   }
-                  return translation;
+                  return untemplatedTranslation;
                }
             }
             else
@@ -1244,6 +1279,14 @@ namespace XUnity.AutoTranslator.Plugin.Core
                               {
                                  var stabilizedTextKey = GetCacheKey( ui, stabilizedText, false, false );
 
+                                 // potentially shortcircuit if fully templated
+                                 if( stabilizedText != stabilizedTextKey.TemplatedOriginalText && !TextCache.IsTranslatable( stabilizedTextKey.TemplatedOriginalText ) )
+                                 {
+                                    var untemplatedTranslation = stabilizedTextKey.Untemplate( stabilizedTextKey.TemplatedOriginalText );
+                                    SetTranslatedText( ui, untemplatedTranslation, info );
+                                    return;
+                                 }
+
                                  QueueNewUntranslatedForClipboard( stabilizedTextKey );
 
                                  info?.Reset( originalText );
@@ -1253,8 +1296,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
                                  {
                                     if( !string.IsNullOrEmpty( translation ) )
                                     {
-                                       // stabilized, no need to untemplate
-                                       SetTranslatedText( ui, translation, info );
+                                       SetTranslatedText( ui, stabilizedTextKey.Untemplate( translation ), info );
                                     }
                                  }
                                  else
@@ -1269,7 +1311,6 @@ namespace XUnity.AutoTranslator.Plugin.Core
                                              var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, true );
                                              if( translatedText != null )
                                              {
-                                                // stabilized, no need to untemplate
                                                 SetTranslatedText( ui, translatedText, info );
                                              }
                                              return;
@@ -1283,7 +1324,6 @@ namespace XUnity.AutoTranslator.Plugin.Core
                                              var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, true );
                                              if( translatedText != null )
                                              {
-                                                // stabilized, no need to untemplate
                                                 SetTranslatedText( ui, translatedText, info );
                                              }
                                              return;
@@ -1369,16 +1409,25 @@ namespace XUnity.AutoTranslator.Plugin.Core
             var untranslatedTextPart = kvp.Value;
             if( !string.IsNullOrEmpty( untranslatedTextPart ) && TextCache.IsTranslatable( untranslatedTextPart ) && IsBelowMaxLength( untranslatedTextPart ) )
             {
-               string partTranslation;
-               if( TextCache.TryGetTranslation( new UntranslatedText( untranslatedTextPart, false, false ), false, out partTranslation ) )
+               var textKey = new UntranslatedText( untranslatedTextPart, false, false );
+               if( TextCache.IsTranslatable( textKey.TemplatedOriginalText ) )
                {
-                  translations.Add( variableName, partTranslation );
+                  string partTranslation;
+                  if( TextCache.TryGetTranslation( textKey, false, out partTranslation ) )
+                  {
+                     translations.Add( variableName, textKey.Untemplate( partTranslation ) );
+                  }
+                  else if( allowStartJob )
+                  {
+                     // incomplete, must start job
+                     var context = new ParserTranslationContext( ui, TranslationManager.CurrentEndpoint, null, result );
+                     TranslateOrQueueWebJobImmediate( ui, untranslatedTextPart, null, false, true, context );
+                  }
                }
-               else if( allowStartJob )
+               else
                {
-                  // incomplete, must start job
-                  var context = new ParserTranslationContext( ui, TranslationManager.CurrentEndpoint, null, result );
-                  TranslateOrQueueWebJobImmediate( ui, untranslatedTextPart, null, false, true, context );
+                  // the template itself does not require a translation, which means the untranslated template equals the translated text
+                  translations.Add( variableName, textKey.Untemplate( textKey.TemplatedOriginalText ) );
                }
             }
             else
@@ -1717,7 +1766,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
          {
             if( !string.IsNullOrEmpty( job.TranslatedText ) )
             {
-               translationResult.SetCompleted( job.TranslatedText, false );
+               translationResult.SetCompleted( job.Key.Untemplate( job.TranslatedText ), false );
             }
             else
             {
@@ -1736,7 +1785,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
                   var info = component.GetOrCreateTextTranslationInfo();
                   if( !string.IsNullOrEmpty( job.TranslatedText ) )
                   {
-                     SetTranslatedText( component, job.TranslatedText, info );
+                     SetTranslatedText( component, job.Key.Untemplate( job.TranslatedText ), info );
                   }
                }
             }
@@ -1847,7 +1896,31 @@ namespace XUnity.AutoTranslator.Plugin.Core
                         var key = GetCacheKey( kvp.Key, originalText, false, false );
                         if( TextCache.TryGetTranslation( key, true, out string translatedText ) && !string.IsNullOrEmpty( translatedText ) )
                         {
-                           SetTranslatedText( kvp.Key, translatedText, tti ); // no need to untemplatize the translated text
+                           SetTranslatedText( kvp.Key, key.Untemplate( translatedText ), tti ); // no need to untemplatize the translated text
+                        }
+                        else if( UnityTextParsers.GameLogTextParser.CanApply( ui ) )
+                        {
+                           var result = UnityTextParsers.GameLogTextParser.Parse( originalText );
+                           if( result.Succeeded )
+                           {
+                              var translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, false );
+                              if( translation != null )
+                              {
+                                 SetTranslatedText( ui, translation, tti );
+                              }
+                           }
+                        }
+                        else if( UnityTextParsers.RichTextParser.CanApply( ui ) && IsBelowMaxLength( originalText ) )
+                        {
+                           var result = UnityTextParsers.RichTextParser.Parse( originalText );
+                           if( result.Succeeded )
+                           {
+                              var translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, false );
+                              if( translation != null )
+                              {
+                                 SetTranslatedText( ui, translation, tti );
+                              }
+                           }
                         }
                      }
                   }

+ 39 - 1
src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs

@@ -35,6 +35,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static string ApplicationName;
       public static float Timeout = 150.0f;
 
+      public static Dictionary<string, string> Replacements = new Dictionary<string, string>();
+
       public static bool SimulateError = false;
       public static bool SimulateDelayedError = false;
 
@@ -54,6 +56,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static string Language;
       public static string FromLanguage;
       public static string OutputFile;
+      public static string SubstitutionFile;
       public static string TranslationDirectory;
       public static float Delay;
       public static int MaxCharactersPerTranslation;
@@ -61,6 +64,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static bool EnableConsole;
       public static bool EnableDebugLogs;
       public static string AutoTranslationsFilePath;
+      public static string SubstitutionFilePath;
       public static bool EnableIMGUI;
       public static bool EnableUGUI;
       public static bool EnableNGUI;
@@ -102,7 +106,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static HashSet<string> DuplicateTextureNames;
       public static TextureHashGenerationStrategy TextureHashGenerationStrategy;
 
-      public static Dictionary<string, string> Replacements;
+      public static float Height;
+      public static float Width;
+      public static HashSet<string> EnabledTranslators;
 
       public static bool CopyToClipboard;
       public static int MaxClipboardCopyCharacters;
@@ -120,6 +126,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
 
             TranslationDirectory = PluginEnvironment.Current.Preferences.GetOrDefault( "Files", "Directory", "Translation" );
             OutputFile = PluginEnvironment.Current.Preferences.GetOrDefault( "Files", "OutputFile", @"Translation\_AutoGeneratedTranslations.{lang}.txt" );
+            SubstitutionFile = PluginEnvironment.Current.Preferences.GetOrDefault( "Files", "SubstitutionFile", @"Translation\_Substitutions.{lang}.txt" );
 
             EnableIMGUI = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableIMGUI", false );
             EnableUGUI = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableUGUI", true );
@@ -177,6 +184,13 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
             UserAgent = PluginEnvironment.Current.Preferences.GetOrDefault( "Http", "UserAgent", string.Empty );
             DisableCertificateValidation = PluginEnvironment.Current.Preferences.GetOrDefault( "Http", "DisableCertificateValidation", GetInitialDisableCertificateChecks() );
 
+
+            Width = PluginEnvironment.Current.Preferences.GetOrDefault( "TranslationAggregator", "Width", 400.0f );
+            Height = PluginEnvironment.Current.Preferences.GetOrDefault( "TranslationAggregator", "Height", 100.0f );
+            EnabledTranslators = PluginEnvironment.Current.Preferences.GetOrDefault( "TranslationAggregator", "EnabledTranslators", string.Empty )
+               ?.Split( new[] { ';' }, StringSplitOptions.RemoveEmptyEntries ).ToHashSet() ?? new HashSet<string>();
+
+
             EnablePrintHierarchy = PluginEnvironment.Current.Preferences.GetOrDefault( "Debug", "EnablePrintHierarchy", false );
             EnableConsole = PluginEnvironment.Current.Preferences.GetOrDefault( "Debug", "EnableConsole", false );
             EnableDebugLogs = PluginEnvironment.Current.Preferences.GetOrDefault( "Debug", "EnableLog", false );
@@ -185,6 +199,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
             MigrationsTag = PluginEnvironment.Current.Preferences.GetOrDefault( "Migrations", "Tag", string.Empty );
 
             AutoTranslationsFilePath = Path.Combine( PluginEnvironment.Current.TranslationPath, OutputFile.Replace( "{lang}", Language ) ).Replace( "/", "\\" ).Parameterize();
+            SubstitutionFilePath = Path.Combine( PluginEnvironment.Current.TranslationPath, SubstitutionFile.Replace( "{lang}", Language ) ).Replace( "/", "\\" ).Parameterize();
             UsesWhitespaceBetweenWords = LanguageHelper.RequiresWhitespaceUponLineMerging( FromLanguage );
 
 
@@ -224,6 +239,29 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
          Save();
       }
 
+      public static void SetTranslationAggregatorBounds( float width, float height )
+      {
+         Width = width;
+         Height = height;
+         PluginEnvironment.Current.Preferences[ "TranslationAggregator" ][ "Width" ].Value = Width.ToString( CultureInfo.InvariantCulture );
+         PluginEnvironment.Current.Preferences[ "TranslationAggregator" ][ "Height" ].Value = Height.ToString( CultureInfo.InvariantCulture );
+         Save();
+      }
+
+      public static void AddTranslator( string id )
+      {
+         EnabledTranslators.Add( id );
+         PluginEnvironment.Current.Preferences[ "TranslationAggregator" ][ "EnabledTranslators" ].Value = string.Join( ";", EnabledTranslators.ToArray() );
+         Save();
+      }
+
+      public static void RemoveTranslator( string id )
+      {
+         EnabledTranslators.Remove( id );
+         PluginEnvironment.Current.Preferences[ "TranslationAggregator" ][ "EnabledTranslators" ].Value = string.Join( ";", EnabledTranslators.ToArray() );
+         Save();
+      }
+
       internal static void Save()
       {
          try

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs

@@ -23,6 +23,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Constants
       /// <summary>
       /// Gets the version of the plugin.
       /// </summary>
-      public const string Version = "3.5.0";
+      public const string Version = "3.6.0";
    }
 }

+ 3 - 1
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ExtProtocol/ExtProtocolEndpoint.cs

@@ -2,6 +2,7 @@
 using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.IO;
 using System.Linq;
 using System.Text;
 using System.Threading;
@@ -86,8 +87,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.ExtProtocol
             if( _process == null )
             {
                _process = new Process();
-               _process.StartInfo.FileName = ExecutablePath;
+               _process.StartInfo.FileName = Path.Combine( Environment.CurrentDirectory, ExecutablePath );
                _process.StartInfo.Arguments = Arguments;
+               _process.StartInfo.WorkingDirectory = new FileInfo( ExecutablePath ).Directory.FullName;
                _process.EnableRaisingEvents = false;
                _process.StartInfo.UseShellExecute = false;
                _process.StartInfo.CreateNoWindow = true;

+ 9 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/IInitializationContext.cs

@@ -29,6 +29,15 @@
       /// <returns></returns>
       T GetOrCreateSetting<T>( string section, string key );
 
+      /// <summary>
+      /// Sets the specified setting.
+      /// </summary>
+      /// <typeparam name="T"></typeparam>
+      /// <param name="section"></param>
+      /// <param name="key"></param>
+      /// <param name="value"></param>
+      void SetSetting<T>( string section, string key, T value );
+
       /// <summary>
       /// Disables the certificate check for the specified hostnames.
       /// </summary>

+ 5 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/InitializationContext.cs

@@ -49,5 +49,10 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
       {
          return PluginEnvironment.Current.Preferences.GetOrDefault( section, key, default( T ) );
       }
+
+      public void SetSetting<T>( string section, string key, T value )
+      {
+         PluginEnvironment.Current.Preferences.Set( section, key, value );
+      }
    }
 }

+ 22 - 24
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationEndpointManager.cs

@@ -63,20 +63,20 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 
       public bool TryGetTranslation( UntranslatedText key, out string value )
       {
-         var unmodifiedKey = key.TranslatableText;
-         var result = _translations.TryGetValue( unmodifiedKey, out value );
+         var translatableText = key.TranslatableText;
+         var result = _translations.TryGetValue( translatableText, out value );
          if( result )
          {
             return result;
          }
 
-         var modifiedKey = key.TrimmedTranslatableText;
-         result = _translations.TryGetValue( modifiedKey, out value );
+         var trimmedTranslatableText = key.TrimmedTranslatableText;
+         result = _translations.TryGetValue( trimmedTranslatableText, out value );
          if( result )
          {
             // add an unmodifiedKey to the dictionary
             var unmodifiedValue = key.LeadingWhitespace + value + key.TrailingWhitespace;
-            AddTranslationToCache( unmodifiedKey, unmodifiedValue );
+            AddTranslationToCache( translatableText, unmodifiedValue );
 
             value = unmodifiedValue;
             return result;
@@ -96,17 +96,15 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 
       private void QueueNewTranslationForDisk( string key, string value )
       {
-         // FIXME: Implement
       }
 
       public void AddTranslationToCache( string key, string value )
       {
-         // UNRELEASED: Not included in current release
-         //if( !HasTranslated( key ) )
-         //{
-         //   AddTranslation( key, value );
-         //   QueueNewTranslationForDisk( key, value );
-         //}
+         if( !HasTranslated( key ) )
+         {
+            AddTranslation( key, value );
+            QueueNewTranslationForDisk( key, value );
+         }
       }
 
       public bool IsTranslatable( string text )
@@ -139,17 +137,20 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
                _unstartedJobs.Remove( key );
                Manager.UnstartedTranslations--;
 
-               var untranslatedText = job.Key.TrimmedTranslatableText;
-               if( CanTranslate( untranslatedText ) )
+               var unpreparedUntranslatedText = job.Key.TrimmedTranslatableText;
+               var untranslatedText = job.Key.PrepareUntranslatedText( unpreparedUntranslatedText );
+               if( CanTranslate( unpreparedUntranslatedText ) )
                {
                   jobs.Add( job );
                   untranslatedTexts.Add( untranslatedText );
                   _ongoingJobs[ key ] = job;
                   Manager.OngoingTranslations++;
+
+                  XuaLogger.Current.Debug( "Started: '" + unpreparedUntranslatedText + "'" );
                }
                else
                {
-                  XuaLogger.Current.Warn( $"Dequeued: '{untranslatedText}' because the current endpoint has already failed this translation 3 times." );
+                  XuaLogger.Current.Warn( $"Dequeued: '{unpreparedUntranslatedText}' because the current endpoint has already failed this translation 3 times." );
                   job.State = TranslationJobState.Failed;
                   job.ErrorMessage = "The endpoint failed to perform this translation 3 or more times.";
 
@@ -162,10 +163,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
                AvailableBatchOperations--;
                var jobsArray = jobs.ToArray();
 
-               foreach( var untranslatedText in untranslatedTexts )
-               {
-                  XuaLogger.Current.Debug( "Started: '" + untranslatedText + "'" );
-               }
                CoroutineHelper.Start(
                   Translate(
                      untranslatedTexts.ToArray(),
@@ -196,13 +193,14 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
             _unstartedJobs.Remove( key );
             Manager.UnstartedTranslations--;
 
-            var untranslatedText = job.Key.TrimmedTranslatableText;
-            if( CanTranslate( untranslatedText ) )
+            var unpreparedUntranslatedText = job.Key.TrimmedTranslatableText;
+            var untranslatedText = job.Key.PrepareUntranslatedText( unpreparedUntranslatedText );
+            if( CanTranslate( unpreparedUntranslatedText ) )
             {
                _ongoingJobs[ key ] = job;
                Manager.OngoingTranslations++;
 
-               XuaLogger.Current.Debug( "Started: '" + untranslatedText + "'" );
+               XuaLogger.Current.Debug( "Started: '" + unpreparedUntranslatedText + "'" );
                CoroutineHelper.Start(
                   Translate(
                      new[] { untranslatedText },
@@ -213,7 +211,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
             }
             else
             {
-               XuaLogger.Current.Warn( $"Dequeued: '{untranslatedText}' because the current endpoint has already failed this translation 3 times." );
+               XuaLogger.Current.Warn( $"Dequeued: '{unpreparedUntranslatedText}' because the current endpoint has already failed this translation 3 times." );
                job.State = TranslationJobState.Failed;
                job.ErrorMessage = "The endpoint failed to perform this translation 3 or more times.";
 
@@ -293,7 +291,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          var hasTranslation = !string.IsNullOrEmpty( translatedText );
          if( hasTranslation )
          {
-            translatedText = key.RepairTemplate( translatedText );
+            translatedText = key.FixTranslatedText( translatedText );
             translatedText = key.LeadingWhitespace + translatedText + key.TrailingWhitespace;
 
             if( Settings.Language == Settings.Romaji && Settings.RomajiPostProcessing != TextPostProcessing.None )

+ 23 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/IniFileExtensions.cs

@@ -8,6 +8,29 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {
    internal static class IniFileExtensions
    {
+      public static void Set<T>( this IniFile that, string section, string key, T value )
+      {
+         var typeOfT = typeof( T ).UnwrapNullable();
+         var iniSection = that[ section ];
+         var iniKey = iniSection[ key ];
+
+         if( value == null )
+         {
+            iniKey.Value = string.Empty;
+         }
+         else
+         {
+            if( typeOfT.IsEnum )
+            {
+               iniKey.Value = EnumHelper.GetNames( typeOfT, value );
+            }
+            else
+            {
+               iniKey.Value = Convert.ToString( value, CultureInfo.InvariantCulture );
+            }
+         }
+      }
+
       public static T GetOrDefault<T>( this IniFile that, string section, string key, T defaultValue )
       {
          var typeOfT = typeof( T ).UnwrapNullable();

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs

@@ -79,7 +79,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 
       public static TemplatedString TemplatizeByReplacements( this string str )
       {
-         if( Settings.Replacements == null || Settings.Replacements.Count == 0 ) return null;
+         if( Settings.Replacements.Count == 0 ) return null;
 
          var dict = new Dictionary<string, string>();
          char arg = 'A';

+ 60 - 52
src/XUnity.AutoTranslator.Plugin.Core/TemplatedString.cs

@@ -23,70 +23,78 @@ namespace XUnity.AutoTranslator.Plugin.Core
          return text;
       }
 
-      public string RepairTemplate( string translatedTemplate )
+      public string PrepareUntranslatedText( string untranslatedText )
       {
-         foreach( var argument in Arguments.Keys )
+         foreach( var kvp in Arguments )
+         {
+            var key = kvp.Key;
+            var translatorFriendlyKey = CreateTranslatorFriendlyKey( key );
+
+            untranslatedText = untranslatedText.Replace( key, translatorFriendlyKey );
+         }
+         return untranslatedText;
+      }
+
+      public string FixTranslatedText( string translatedText )
+      {
+         foreach( var kvp in Arguments )
+         {
+            var key = kvp.Key;
+            var translatorFriendlyKey = CreateTranslatorFriendlyKey( key );
+            translatedText = ReplaceApproximateMatches( translatedText, translatorFriendlyKey, key );
+         }
+         return translatedText;
+      }
+
+      public static string CreateTranslatorFriendlyKey( string key )
+      {
+         var c = key[ 2 ];
+         var translatorFriendlyKey = "ZM" + (char)( c + 2 ) + "Z";
+         return translatorFriendlyKey;
+      }
+
+      public static string ReplaceApproximateMatches( string translatedText, string translatorFriendlyKey, string key )
+      {
+         var cidx = 0;
+         var startIdx = 0;
+
+         for( int i = 0; i < translatedText.Length; i++ )
          {
-            if( !translatedTemplate.Contains( argument ) )
+            var c = translatedText[ i ];
+            if( c == ' ' || c == ' ' ) continue;
+
+            if( c == translatorFriendlyKey[ cidx ] )
             {
-               var permutations = CreatePermutations( argument );
-               foreach( var permutation in permutations )
+               if( cidx == 0 )
                {
-                  if( translatedTemplate.Contains( permutation ) )
-                  {
-                     translatedTemplate = translatedTemplate.Replace( permutation, argument );
-                     break;
-                  }
+                  startIdx = i;
                }
+
+               cidx++;
+            }
+            else
+            {
+               cidx = 0;
+               startIdx = 0;
             }
-         }
 
-         return translatedTemplate;
-      }
+            if( cidx == translatorFriendlyKey.Length )
+            {
+               int endIdx = i + 1;
 
-      public static string[] CreatePermutations( string argument )
-      {
-         var b0_1 = argument.Insert( 2, " " );   // {{ A}}
-         var b0_2 = argument.Insert( 3, " " );   // {{A }}
+               var lengthOfKey = endIdx - startIdx;
+               var diff = lengthOfKey - key.Length;
 
-         var b1 = argument.Substring( 1 ); // {A}}
-         var b1_1 = b1.Insert( 1, " " );   // { A}}
-         var b1_2 = b1.Insert( 2, " " );   // {A }}
+               translatedText = translatedText.Remove( startIdx, lengthOfKey ).Insert( startIdx, key );
 
-         var b2 = argument.Substring( 0, argument.Length - 1 ); // {{A}
-         var b2_1 = b2.Insert( 2, " " );   // {{ A}
-         var b2_2 = b2.Insert( 3, " " );   // {{A }
+               i -= diff;
 
-         var b3 = argument.Substring( 1, argument.Length - 2 ); // {A}
-         var b3_1 = b3.Insert( 1, " " );   // { A}
-         var b3_2 = b3.Insert( 2, " " );   // {A }
+               cidx = 0;
+               startIdx = 0;
+            }
+         }
 
-         return new string[]
-         {
-            b0_1,
-            b0_1,
-            b2,
-            b2_1,
-            b2_2,
-            b1,
-            b1_1,
-            b1_2,
-            b3,
-            b3_1,
-            b3_2,
-            b0_1.ToLower(),
-            b0_2.ToLower(),
-            argument.ToLower(),
-            b2.ToLower(),
-            b2_1.ToLower(),
-            b2_2.ToLower(),
-            b1.ToLower(),
-            b1_1.ToLower(),
-            b1_2.ToLower(),
-            b3.ToLower(),
-            b3_1.ToLower(),
-            b3_2.ToLower(),
-         };
+         return translatedText;
       }
    }
 }

+ 74 - 49
src/XUnity.AutoTranslator.Plugin.Core/TextTranslationCache.cs

@@ -55,12 +55,15 @@ namespace XUnity.AutoTranslator.Plugin.Core
                _defaultRegexes.Clear();
                _translations.Clear();
                _reverseTranslations.Clear();
+               Settings.Replacements.Clear();
 
                var mainTranslationFile = Settings.AutoTranslationsFilePath;
-               LoadTranslationsInFile( mainTranslationFile );
+               var substitutionFile = Settings.SubstitutionFilePath;
+               LoadTranslationsInFile( mainTranslationFile, false );
+               LoadTranslationsInFile( substitutionFile, true );
                foreach( var fullFileName in GetTranslationFiles().Reverse().Except( new[] { mainTranslationFile } ) )
                {
-                  LoadTranslationsInFile( fullFileName );
+                  LoadTranslationsInFile( fullFileName, false );
                }
             }
             var endTime = Time.realtimeSinceStartup;
@@ -72,56 +75,78 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      private void LoadTranslationsInFile( string fullFileName )
+      private void LoadTranslationsInFile( string fullFileName, bool isSubstitutionFile )
       {
-         if( File.Exists( fullFileName ) )
+         var fileExists = File.Exists( fullFileName );
+         if( fileExists || isSubstitutionFile )
          {
-            XuaLogger.Current.Debug( $"Loading texts: {fullFileName}." );
-
-            string[] translations = File.ReadAllLines( fullFileName, Encoding.UTF8 );
-            foreach( string translation in translations )
+            if( fileExists )
             {
-               for( int i = 0; i < TranslationSplitters.Length; i++ )
+               XuaLogger.Current.Debug( $"Loading texts: {fullFileName}." );
+
+               string[] translations = File.ReadAllLines( fullFileName, Encoding.UTF8 );
+               foreach( string translation in translations )
                {
-                  var splitter = TranslationSplitters[ i ];
-                  string[] kvp = translation.Split( splitter, StringSplitOptions.None );
-                  if( kvp.Length == 2 )
+                  for( int i = 0; i < TranslationSplitters.Length; i++ )
                   {
-                     string key = TextHelper.Decode( kvp[ 0 ] );
-                     string value = TextHelper.Decode( kvp[ 1 ] );
-
-                     if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) && IsTranslatable( key ) )
+                     var splitter = TranslationSplitters[ i ];
+                     string[] kvp = translation.Split( splitter, StringSplitOptions.None );
+                     if( kvp.Length == 2 )
                      {
-                        if( key.StartsWith( "r:" ) )
-                        {
-                           try
-                           {
-                              var regex = new RegexTranslation( key, value );
+                        string key = TextHelper.Decode( kvp[ 0 ] );
+                        string value = TextHelper.Decode( kvp[ 1 ] );
 
-                              AddTranslationRegex( regex );
-                           }
-                           catch( Exception e )
+                        if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) && IsTranslatable( key ) )
+                        {
+                           if( isSubstitutionFile )
                            {
-                              XuaLogger.Current.Warn( e, $"An error occurred while constructing regex translation: '{translation}'." );
+                              if( key != null && value != null )
+                              {
+                                 Settings.Replacements[ key ] = value;
+                              }
                            }
-                        }
-                        else
-                        {
-                           AddTranslation( key, value );
-
-                           // also add a modified version of the translation
-                           var ukey = new UntranslatedText( key, false, false );
-                           var uvalue = new UntranslatedText( value, false, false );
-                           if( ukey.TrimmedTranslatableText != key )
+                           else
                            {
-                              AddTranslation( ukey.TrimmedTranslatableText, uvalue.TrimmedTranslatableText );
+                              if( key.StartsWith( "r:" ) )
+                              {
+                                 try
+                                 {
+                                    var regex = new RegexTranslation( key, value );
+
+                                    AddTranslationRegex( regex );
+                                 }
+                                 catch( Exception e )
+                                 {
+                                    XuaLogger.Current.Warn( e, $"An error occurred while constructing regex translation: '{translation}'." );
+                                 }
+                              }
+                              else
+                              {
+                                 AddTranslation( key, value );
+
+                                 // also add a modified version of the translation
+                                 var ukey = new UntranslatedText( key, false, false );
+                                 var uvalue = new UntranslatedText( value, false, false );
+                                 if( ukey.TrimmedTranslatableText != key )
+                                 {
+                                    AddTranslation( ukey.TrimmedTranslatableText, uvalue.TrimmedTranslatableText );
+                                 }
+                              }
+                              break;
                            }
                         }
-                        break;
                      }
                   }
                }
             }
+            else if( isSubstitutionFile )
+            {
+               using( var stream = File.Create( fullFileName ) )
+               {
+                  stream.Write( new byte[] { 0xEF, 0xBB, 0xBF }, 0, 3 ); // UTF-8 BOM
+                  stream.Close();
+               }
+            }
          }
       }
 
@@ -243,24 +268,24 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
       internal bool TryGetTranslation( UntranslatedText key, bool allowRegex, out string value )
       {
-         var unmodifiedKey = key.TranslatableText;
-         var result = _translations.TryGetValue( unmodifiedKey, out value );
+         var translatableText = key.TranslatableText;
+         var result = _translations.TryGetValue( translatableText, out value );
          if( result )
          {
             return result;
          }
 
-         var modifiedKey = key.TrimmedTranslatableText;
-         if( modifiedKey != unmodifiedKey )
+         var trimmedTranslatableText = key.TrimmedTranslatableText;
+         if( trimmedTranslatableText != translatableText )
          {
-            result = _translations.TryGetValue( modifiedKey, out value );
+            result = _translations.TryGetValue( trimmedTranslatableText, out value );
             if( result )
             {
                // add an unmodifiedKey to the dictionary
                var unmodifiedValue = key.LeadingWhitespace + value + key.TrailingWhitespace;
 
                XuaLogger.Current.Info( $"Whitespace difference: '{key.TrimmedTranslatableText}' => '{value}'" );
-               AddTranslationToCache( unmodifiedKey, unmodifiedValue, Settings.CacheWhitespaceDifferences );
+               AddTranslationToCache( translatableText, unmodifiedValue, Settings.CacheWhitespaceDifferences );
 
                value = unmodifiedValue;
                return result;
@@ -276,12 +301,12 @@ namespace XUnity.AutoTranslator.Plugin.Core
                var regex = _defaultRegexes[ i ];
                try
                {
-                  var match = regex.CompiledRegex.Match( unmodifiedKey );
+                  var match = regex.CompiledRegex.Match( translatableText );
                   if( !match.Success ) continue;
 
-                  var translation = regex.CompiledRegex.Replace( unmodifiedKey, regex.Translation );
+                  var translation = regex.CompiledRegex.Replace( translatableText, regex.Translation );
 
-                  AddTranslationToCache( unmodifiedKey, translation, Settings.CacheRegexLookups ); // Would store it to file... Should we????
+                  AddTranslationToCache( translatableText, translation, Settings.CacheRegexLookups ); // Would store it to file... Should we????
 
                   value = translation;
                   found = true;
@@ -305,15 +330,15 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
          if( _staticTranslations.Count > 0 )
          {
-            if( _staticTranslations.TryGetValue( unmodifiedKey, out value ) )
+            if( _staticTranslations.TryGetValue( translatableText, out value ) )
             {
-               AddTranslationToCache( unmodifiedKey, value );
+               AddTranslationToCache( translatableText, value );
                return true;
             }
-            else if( _staticTranslations.TryGetValue( modifiedKey, out value ) )
+            else if( _staticTranslations.TryGetValue( trimmedTranslatableText, out value ) )
             {
                var unmodifiedValue = key.LeadingWhitespace + value + key.TrailingWhitespace;
-               AddTranslationToCache( unmodifiedKey, unmodifiedValue );
+               AddTranslationToCache( translatableText, unmodifiedValue );
 
                value = unmodifiedValue;
                return true;

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/UI/GUIUtil.cs

@@ -21,7 +21,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
       {
          richText = false,
          margin = new RectOffset( GUI.skin.label.margin.left, GUI.skin.label.margin.right, 0, 0 ),
-         padding = new RectOffset( GUI.skin.label.padding.left, GUI.skin.label.padding.right, 0, 0 )
+         padding = new RectOffset( GUI.skin.label.padding.left, GUI.skin.label.padding.right, 2, 3 )
       };
 
       public static readonly GUIStyle LabelCenter = new GUIStyle( GUI.skin.label )

+ 2 - 2
src/XUnity.AutoTranslator.Plugin.Core/UI/TranslationAggregatorOptionsWindow.cs

@@ -90,12 +90,12 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
 
          GUILayout.BeginHorizontal();
          GUILayout.Label( "Height" );
-         _viewModel.Height = GUILayout.HorizontalSlider( _viewModel.Height, 50, 300, GUILayout.MaxWidth( 250 ) );
+         _viewModel.Height = Mathf.Round( GUILayout.HorizontalSlider( _viewModel.Height, 50, 300, GUILayout.MaxWidth( 250 ) ) );
          GUILayout.EndHorizontal();
 
          GUILayout.BeginHorizontal();
          GUILayout.Label( "Width" );
-         _viewModel.Width = GUILayout.HorizontalSlider( _viewModel.Width, 200, 1000, GUILayout.MaxWidth( 250 ) );
+         _viewModel.Width = Mathf.Round( GUILayout.HorizontalSlider( _viewModel.Width, 200, 1000, GUILayout.MaxWidth( 250 ) ) );
          GUILayout.EndHorizontal();
 
          GUI.DragWindow();

+ 36 - 4
src/XUnity.AutoTranslator.Plugin.Core/UI/TranslationAggregatorViewModel.cs

@@ -4,24 +4,29 @@ using System.Linq;
 using UnityEngine;
 using XUnity.AutoTranslator.Plugin.Core.Configuration;
 using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
 
 namespace XUnity.AutoTranslator.Plugin.Core.UI
 {
    class TranslationAggregatorViewModel
    {
+      private DebounceFunction _saveHeightAndWidth;
       private LinkedList<AggregatedTranslationViewModel> _translations;
       private LinkedListNode<AggregatedTranslationViewModel> _current;
       private List<Translation> _translationsToAggregate = new List<Translation>();
       private HashSet<string> _textsToAggregate = new HashSet<string>();
       private float _lastUpdate = 0.0f;
+      private float _height;
+      private float _width;
 
       public TranslationAggregatorViewModel( TranslationManager translationManager )
       {
          _translations = new LinkedList<AggregatedTranslationViewModel>();
+         _saveHeightAndWidth = new DebounceFunction( 1, SaveHeightAndWidth );
 
          Manager = translationManager;
-         Height = 100; // TODO: Get from config
-         Width = 400; // TODO: Get from config
+         Height = Settings.Height;
+         Width = Settings.Width;
 
          AllTranslators = translationManager.AllEndpoints
             .Select( x => new TranslatorViewModel( x ) )
@@ -36,9 +41,31 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
 
       public bool IsShowingOptions { get; set; }
 
-      public float Height { get; set; }
+      public float Height
+      {
+         get { return _height; }
+         set
+         {
+            if( _height != value )
+            {
+               _height = value;
+               _saveHeightAndWidth.Execute();
+            }
+         }
+      }
 
-      public float Width { get; set; }
+      public float Width
+      {
+         get { return _width; }
+         set
+         {
+            if( _width != value )
+            {
+               _width = value;
+               _saveHeightAndWidth.Execute();
+            }
+         }
+      }
 
       public List<TranslatorViewModel> AvailableTranslators { get; }
 
@@ -48,6 +75,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
 
       public AggregatedTranslationViewModel Current => _current?.Value;
 
+      private void SaveHeightAndWidth()
+      {
+         Settings.SetTranslationAggregatorBounds( Width, Height );
+      }
+
       public void OnNewTranslationAdded( TextTranslationInfo info )
       {
          if( !_textsToAggregate.Contains( info.OriginalText ) )

+ 25 - 3
src/XUnity.AutoTranslator.Plugin.Core/UI/TranslatorViewModel.cs

@@ -1,17 +1,39 @@
-using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
 
 namespace XUnity.AutoTranslator.Plugin.Core.UI
 {
    class TranslatorViewModel
    {
+      private bool _isEnabled;
+
       public TranslatorViewModel( TranslationEndpointManager endpoint )
       {
          Endpoint = endpoint;
-         IsEnabled = false; // TODO: initialize from configuration...
+         IsEnabled = Settings.EnabledTranslators.Contains( endpoint.Endpoint.Id );
       }
 
       public TranslationEndpointManager Endpoint { get; set; }
 
-      public bool IsEnabled { get; set; }
+      public bool IsEnabled
+      {
+         get { return _isEnabled; }
+         set
+         {
+            if( _isEnabled != value )
+            {
+               _isEnabled = value;
+
+               if( _isEnabled )
+               {
+                  Settings.AddTranslator( Endpoint.Endpoint.Id );
+               }
+               else
+               {
+                  Settings.RemoveTranslator( Endpoint.Endpoint.Id );
+               }
+            }
+         }
+      }
    }
 }

+ 24 - 10
src/XUnity.AutoTranslator.Plugin.Core/UntranslatedText.cs

@@ -21,14 +21,16 @@ namespace XUnity.AutoTranslator.Plugin.Core
                text = TemplatedText.Template;
             }
          }
-         //else
-         //{
-         //   TemplatedText = text.TemplatizeByReplacements();
-         //   if( TemplatedText != null )
-         //   {
-         //      text = TemplatedText.Template;
-         //   }
-         //}
+         else
+         {
+            TemplatedText = text.TemplatizeByReplacements();
+            if( TemplatedText != null )
+            {
+               text = TemplatedText.Template;
+            }
+         }
+
+         TemplatedOriginalText = text;
 
          int i = 0;
          int firstNonWhitespace = 0;
@@ -204,6 +206,8 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
       public string OriginalText { get; }
 
+      public string TemplatedOriginalText { get; }
+
       public TemplatedString TemplatedText { get; }
 
       public string Untemplate( string text )
@@ -216,11 +220,21 @@ namespace XUnity.AutoTranslator.Plugin.Core
          return text;
       }
 
-      public string RepairTemplate( string text )
+      public string PrepareUntranslatedText( string text )
+      {
+         if( TemplatedText != null )
+         {
+            return TemplatedText.PrepareUntranslatedText( text );
+         }
+
+         return text;
+      }
+
+      public string FixTranslatedText( string text )
       {
          if( TemplatedText != null )
          {
-            return TemplatedText.RepairTemplate( text );
+            return TemplatedText.FixTranslatedText( text );
          }
 
          return text;

+ 3 - 2
src/XUnity.AutoTranslator.Plugin.Core/Utilities/CoroutineHelper.cs

@@ -1,5 +1,4 @@
-using System;
-using System.Collections;
+using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
@@ -10,5 +9,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Utilities
    internal static class CoroutineHelper
    {
       public static Coroutine Start( IEnumerator coroutine ) => AutoTranslationPlugin.Current.StartCoroutine( coroutine );
+
+      public static void Stop( Coroutine coroutine ) => AutoTranslationPlugin.Current.StopCoroutine( coroutine );
    }
 }

+ 38 - 0
src/XUnity.AutoTranslator.Plugin.Core/Utilities/DebounceFunction.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections;
+using UnityEngine;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Utilities
+{
+   internal class DebounceFunction
+   {
+      private readonly float _delaySeconds;
+      private readonly Action _callback;
+      private Coroutine _current;
+
+      public DebounceFunction( float delaySeconds, Action callback )
+      {
+         _delaySeconds = delaySeconds;
+         _callback = callback;
+      }
+
+      public void Execute()
+      {
+         if( _current != null )
+         {
+            CoroutineHelper.Stop( _current );
+         }
+
+         _current = CoroutineHelper.Start( Run() );
+      }
+
+      private IEnumerator Run()
+      {
+         yield return new WaitForSeconds( _delaySeconds );
+
+         _callback();
+
+         _current = null;
+      }
+   }
+}

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.5.0</Version>
+      <Version>3.6.0</Version>
    </PropertyGroup>
 
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.5.0</Version>
+      <Version>3.6.0</Version>
    </PropertyGroup>
 
    <ItemGroup>

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.5.0</Version>
+      <Version>3.6.0</Version>
    </PropertyGroup>
 
    <ItemGroup>

+ 1 - 0
src/XUnity.AutoTranslator.Setup.Build/XUnity.AutoTranslator.Setup.Build.csproj

@@ -16,6 +16,7 @@
     <ProjectReference Include="..\Translators\GoogleTranslateLegitimate\GoogleTranslateLegitimate.csproj" />
     <ProjectReference Include="..\Translators\GoogleTranslate\GoogleTranslate.csproj" />
     <ProjectReference Include="..\Translators\LecPowerTranslator15\LecPowerTranslator15.csproj" />
+    <ProjectReference Include="..\Translators\PapagoTranslate\PapagoTranslate.csproj" />
     <ProjectReference Include="..\Translators\ReverseTranslator\ReverseTranslator.csproj" />
     <ProjectReference Include="..\Translators\WatsonTranslate\WatsonTranslate.csproj" />
     <ProjectReference Include="..\Translators\YandexTranslate\YandexTranslate.csproj" />

+ 1 - 0
src/XUnity.AutoTranslator.Setup/Program.cs

@@ -52,6 +52,7 @@ namespace XUnity.AutoTranslator.Setup
          AddFile( Path.Combine( translatorsPath, "Lec.ExtProtocol.exe" ), Resources.Lec_ExtProtocol, true );
          AddFile( Path.Combine( translatorsPath, "WatsonTranslate.dll" ), Resources.WatsonTranslate, true );
          AddFile( Path.Combine( translatorsPath, "YandexTranslate.dll" ), Resources.YandexTranslate, true );
+         AddFile( Path.Combine( translatorsPath, "PapagoTranslate.dll" ), Resources.PapagoTranslate, true );
 
          foreach( var launcher in launchers )
          {

+ 10 - 0
src/XUnity.AutoTranslator.Setup/Properties/Resources.Designer.cs

@@ -210,6 +210,16 @@ namespace XUnity.AutoTranslator.Setup.Properties {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized resource of type System.Byte[].
+        /// </summary>
+        internal static byte[] PapagoTranslate {
+            get {
+                object obj = ResourceManager.GetObject("PapagoTranslate", resourceCulture);
+                return ((byte[])(obj));
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized resource of type System.Byte[].
         /// </summary>

+ 3 - 0
src/XUnity.AutoTranslator.Setup/Properties/Resources.resx

@@ -184,6 +184,9 @@
    <data name="YandexTranslate" type="System.Resources.ResXFileRef, System.Windows.Forms">
       <value>..\..\XUnity.AutoTranslator.Setup.Build\bin\net35\YandexTranslate.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
    </data>
+   <data name="PapagoTranslate" type="System.Resources.ResXFileRef, System.Windows.Forms">
+      <value>..\..\XUnity.AutoTranslator.Setup.Build\bin\net35\PapagoTranslate.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+   </data>
    <data name="Lec_ExtProtocol" type="System.Resources.ResXFileRef, System.Windows.Forms">
       <value>..\..\XUnity.AutoTranslator.Setup.Build-x86\bin\net35\Lec.ExtProtocol.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
    </data>

+ 1 - 1
src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj

@@ -4,7 +4,7 @@
       <OutputType>Exe</OutputType>
       <TargetFramework>net40</TargetFramework>
       <AssemblyName>SetupReiPatcherAndAutoTranslator</AssemblyName>
-      <Version>3.5.0</Version>
+      <Version>3.6.0</Version>
       <ApplicationIcon>icon.ico</ApplicationIcon>
       <Win32Resource />
    </PropertyGroup>