فهرست منبع

doc update, UI update, ext protocol update

randoman 6 سال پیش
والد
کامیت
ee176f3bb5

+ 1 - 0
CHANGELOG.md

@@ -6,6 +6,7 @@
  * FEATURE - Enable custom implementations of translators that can be loaded dynamically
  * FEATURE - Removed support for Excite translate because it only support the 'WWW' API in Unity due to missing TLS1.2 implementation
  * FEATURE - Updated Watson translate to v3
+ * FEATURE - Support for 'romaji' as output language. Only google supports this at the  moment
  * MISC - {GameExeName} variable can now be used in configuration of directories and files
  * MISC - Changed the way the 'Custom' endpoint works. See README for more info
  * MISC - Added new configuration 'GameLogTextPaths' to enable special handling of text components that text is being appended to continuously (requires export knowledge to setup)

+ 126 - 14
README.md

@@ -442,13 +442,50 @@ This approach requires version 2.15.0 or later!
 ## Implementing a Translator
 Since version 3.0.0, you can now also implement your own translators.
 
+In order to do so, all you have to do is implement the following interface, build the assembly and place the generated DLL in the `Translators` folder.
+
+```C#
+public interface ITranslateEndpoint
+{
+   /// <summary>
+   /// Gets the id of the ITranslateEndpoint that is used as a configuration parameter.
+   /// </summary>
+   string Id { get; }
+
+   /// <summary>
+   /// Gets a friendly name that can be displayed to the user representing the plugin.
+   /// </summary>
+   string FriendlyName { get; }
+
+   /// <summary>
+   /// Gets the maximum concurrency for the endpoint. This specifies how many times "Translate"
+   /// can be called before it returns.
+   /// </summary>
+   int MaxConcurrency { get; }
+
+   /// <summary>
+   /// Called during initialization. Use this to initialize plugin or throw exception if impossible.
+   /// </summary>
+   void Initialize( IInitializationContext context );
+
+   /// <summary>
+   /// Attempt to translated the provided untranslated text. Will be used in a "coroutine",
+   /// so it can be implemented in an asynchronous fashion.
+   /// </summary>
+   IEnumerator Translate( ITranslationContext context );
+}
+```
+
+Often an implementation of this interface will access an external web service. If this is the case, you do not need to implement the entire interface yourself. Instead you can rely on a base class in the `XUnity.AutoTranslator.Plugin.Core` assembly. But more on this later.
+
 ### Important Notes on Implementing a Translator based on an Online Service
 Whenever you implement a translator based on an online service, it is important to not use it in an abusive way. For example by:
  * Establishing a large number of connections to it
  * Performing web scraping instead of using an available API
+ * *This is especially important if the service is not authenticated*
 
 With that in mind, consider the following:
- * The `WWW` class in Unity establishes a new TCP connection on each request you make, making it extremely poor at this kind of job. Especially if SSL (https) is involved because it has to do the entire handshake procedure each time.
+ * The `WWW` class in Unity establishes a new TCP connection on each request you make, making it extremely poor at this kind of job. Especially if SSL (https) is involved because it has to do the entire handshake procedure each time. Yuck.
  * The `UnityWebRequest` class in Unity does not exist in most games, because they use an old engine, so it is not a good choice either.
  * The `WebClient` class from .NET is capable of using persistent connections (it does so by default), but has its own problems with SSL. The version of Mono used in most Unity games rejects all certificates by default making all HTTPS connections fail. This, however, can be remedied during the initialization phase of the translator (see examples below). Another shortcoming of this API is the fact that the runtime will never release the TCP connections it has used until the process ends. The API also integrates terribly with Unity because callbacks return on a background thread.
  * The `WebRequest` class from .NET is essentially the same as WebClient.
@@ -456,7 +493,7 @@ With that in mind, consider the following:
 
 None of these are therefore an ideal solution.
 
-To remedy this, I have made a class `XUnityWebClient`, which is based on Mono's version of WebClient. However, it adds the following features:
+To remedy this, the plugin implements a class `XUnityWebClient`, which is based on Mono's version of WebClient. However, it adds the following features:
  * Enables integration with Unity by returning result classes that can be 'yielded'.
  * Properly closes connections that has not been used for 50 seconds.
 
@@ -466,29 +503,32 @@ I recommend using this class, or in case that cannot be used, falling back to th
 Follow these steps:
  * Start a new project in Visual Studio 2017 or later. (Be a good boy and choose the "Class Library (.NET Standard)" template. After choosing that, edit the generated .csproj file and change the TargetFramework element to 'net35')
  * Add a reference to the XUnity.AutoTranslator.Plugin.Core.dll
- * Add a reference to UnityEngine.dll
- * Add a reference to ExIni.dll
+ * Add a reference to UnityEngine.dll (Consider using an old version of this assembly (if `Translators` exists in the Managed folder, it is not an old version!))
  * Create a new class that either:
    * Implements the `ITranslateEndpoint` interface
    * Inherits from the `HttpEndpoint` class
+   * Inherits from the `WwwEndpoint` class
+   * Inherits from the `ExtProtocolEndpoint` class
 
-Here's an example that simply reverses the text:
+Here's an example that simply reverses the text and also reads some configuration from the configuration file the plugin uses:
 
 ```C#
-public class ReverseTranslator : ITranslateEndpoint
+public class ReverseTranslatorEndpoint : ITranslateEndpoint
 {
-   public string Id => "Reverser";
+   private bool _myConfig;
+
+   public string Id => "ReverseTranslator";
 
    public string FriendlyName => "Reverser";
 
    public int MaxConcurrency => 50;
 
-   public void Initialize( InitializationContext context )
+   public void Initialize( IInitializationContext context )
    {
-
+      _myConfig = context.GetOrCreateSetting( "Reverser", "MyConfig", true );
    }
 
-   public IEnumerator Translate( TranslationContext context )
+   public IEnumerator Translate( ITranslationContext context )
    {
       var reversedText = new string( context.UntranslatedText.Reverse().ToArray() );
       context.Complete( reversedText );
@@ -498,12 +538,84 @@ public class ReverseTranslator : ITranslateEndpoint
 }
 ```
 
-Here's a more real example of one of the existing endpoints based on the HttpEndpoint:
+Arguably, this is not a particularly interesting example, but it illustrates the basic principles of what must be done in order to implement a Translator.
+
+Let's take a look at a more advanced example that accesses the web:
+
+```C#
+internal class YandexTranslateEndpoint : HttpEndpoint
+{
+   private static readonly string HttpsServicePointTemplateUrl = "https://translate.yandex.net/api/v1.5/tr.json/translate?key={3}&text={2}&lang={0}-{1}&format=plain";
+
+   private string _key;
+
+   public override string Id => "YandexTranslate";
+
+   public override string FriendlyName => "Yandex Translate";
+
+   public override void Initialize( IInitializationContext context )
+   {
+      _key = context.GetOrCreateSetting( "Yandex", "YandexAPIKey", "" );
+      context.EnableSslFor( "translate.yandex.net" );
+
+      // if the plugin cannot be enabled, simply throw so the user cannot select the plugin
+      if( string.IsNullOrEmpty( _key ) ) throw new Exception( "The YandexTranslate endpoint requires an API key which has not been provided." );
+      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 override void OnCreateRequest( IHttpRequestCreationContext context )
+   {
+      var request = new XUnityWebRequest(
+         string.Format(
+            HttpsServicePointTemplateUrl,
+            context.SourceLanguage,
+            context.DestinationLanguage,
+            WWW.EscapeURL( context.UntranslatedText ),
+            _key ) );
+
+      request.Headers[ HttpRequestHeader.UserAgent ] = string.IsNullOrEmpty( AutoTranslatorSettings.UserAgent ) ? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.183 Safari/537.36 Vivaldi/1.96.1147.55" : AutoTranslatorSettings.UserAgent;
+      request.Headers[ HttpRequestHeader.Accept ] = "*/*";
+      request.Headers[ HttpRequestHeader.AcceptCharset ] = "UTF-8";
+
+      context.Complete( request );
+   }
+
+   public override void OnExtractTranslation( IHttpTranslationExtractionContext context )
+   {
+      var data = context.Response.Data;
+      var obj = JSON.Parse( data );
+      var lineBuilder = new StringBuilder( data.Length );
+
+      var code = obj.AsObject[ "code" ].ToString();
+
+      if( code == "200" )
+      {
+         var token = obj.AsObject[ "text" ].ToString();
+         token = token.Substring( 2, token.Length - 4 ).UnescapeJson();
+
+         if( string.IsNullOrEmpty( token ) ) return; 
+
+         if( !lineBuilder.EndsWithWhitespaceOrNewline() ) lineBuilder.Append( "\n" );
+         lineBuilder.Append( token );
+
+         var translated = lineBuilder.ToString();
+
+         context.Complete( translated );
+      }
+   }
+}
+```
 
-...
+This plugin extends from `HttpEndpoint`. Let's look at the three methods it overrides:
+ * `Initialize` is used to read the API key the user has configured. In addition it calls `context.EnableSslFor( "translate.yandex.net" )` in order to disable the certificate check for this specific hostname. If this is neglected, SSL will fail in most versions of Unity. Finally, it throws an exception if the plugin cannot be used with the specified configuration.
+ * `OnCreateRequest` is used to construct the `XUnityWebRequest` object that will be sent to the external endpoint. The call to `context.Complete( request )` specifies the request to use.
+ * `OnExtractTranslation` is used to extract the text from the response returned from the web server.
 
-As you can see, the `XUnityWebClient` class is not even used. We simply return a request object that the `HttpEndpoint` will use internally to perform the request.
+As you can see, the `XUnityWebClient` class is not even used. We simply specify a request object that the `HttpEndpoint` will use internally to perform the request.
 
-After implementing the class, simply build the project and place the generated DLL file in the "Translators" directory (you may have to create it) of the plugin folder. That's it.
+After implementing the class, simply build the project and place the generated DLL file in the "Translators" directory of the plugin folder. That's it.
 
+As mentioned earlier, you  can also use the abstract class `WwwEndpoint` to implement roughly the same thing. However, I do not recommend doing so, unless it is an authenticated service.
 
+Another way to implement a translator is to implement the `ExtProtocolEndpoint` class. This can be used to delegate the actual translation logic to an external process. Currently there is no documentation on this, but you can take a look at the LEC implementation, which uses it.

+ 7 - 5
src/Translators/Lec.ExtProtocol/Program.cs

@@ -26,12 +26,14 @@ namespace Lec.ExtProtocol
 
             using( var translator = new LecTranslationLibrary( dllPath ) )
             {
-               while( true )
+               using( var stdout = Console.OpenStandardOutput() )
+               using( var writer = new StreamWriter( stdout ) )
+               using( var stdin = Console.OpenStandardInput() )
+               using( var reader = new StreamReader( stdin ) )
                {
-                  using( var stdout = Console.OpenStandardOutput() )
-                  using( var writer = new StreamWriter( stdout ) )
-                  using( var stdin = Console.OpenStandardInput() )
-                  using( var reader = new StreamReader( stdin ) )
+                  writer.AutoFlush = true;
+
+                  while( true )
                   {
                      var receivedPayload = reader.ReadLine();
                      if( string.IsNullOrEmpty( receivedPayload ) ) return;

+ 2 - 0
src/Translators/LecPowerTranslator15/LecPowerTranslator15Endpoint.cs

@@ -15,6 +15,8 @@ namespace LecPowerTranslator15
 
       public override string FriendlyName => "LEC Power Translator 15";
 
+      public override int MaxConcurrency => 15;
+
       public override void Initialize( IInitializationContext context )
       {
          var pathToLec = context.GetOrCreateSetting( "LecPowerTranslator15", "InstallationPath", "" );

+ 4 - 2
src/Translators/ReverseTranslator/ReverseTranslatorEndpoint.cs

@@ -9,7 +9,9 @@ namespace ReverseTranslator
 {
    public class ReverseTranslatorEndpoint : ITranslateEndpoint
    {
-      public string Id => "Reverser";
+      private bool _myConfig;
+
+      public string Id => "ReverseTranslator";
 
       public string FriendlyName => "Reverser";
 
@@ -17,7 +19,7 @@ namespace ReverseTranslator
 
       public void Initialize( IInitializationContext context )
       {
-
+         _myConfig = context.GetOrCreateSetting( "Reverser", "MyConfig", true );
       }
 
       public IEnumerator Translate( ITranslationContext context )

+ 5 - 11
src/Translators/YandexTranslate/YandexTranslateEndpoint.cs

@@ -26,17 +26,13 @@ namespace YandexTranslate
 
       public override string FriendlyName => "Yandex Translate";
 
-      public YandexTranslateEndpoint()
-      {
-      }
-
       public override void Initialize( IInitializationContext context )
       {
          _key = context.GetOrCreateSetting( "Yandex", "YandexAPIKey", "" );
-         if( string.IsNullOrEmpty( _key ) ) throw new Exception( "The YandexTranslate endpoint requires an API key which has not been provided." );
-
          context.EnableSslFor( "translate.yandex.net" );
-         
+
+         // if the plugin cannot be enabled, simply throw so the user cannot select the plugin
+         if( string.IsNullOrEmpty( _key ) ) throw new Exception( "The YandexTranslate endpoint requires an API key which has not been provided." );
          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." );
       }
@@ -70,10 +66,8 @@ namespace YandexTranslate
          {
             var token = obj.AsObject[ "text" ].ToString();
             token = token.Substring( 2, token.Length - 4 ).UnescapeJson();
-            if( string.IsNullOrEmpty( token ) )
-            {
-               return;
-            }
+
+            if( string.IsNullOrEmpty( token ) ) return; 
 
             if( !lineBuilder.EndsWithWhitespaceOrNewline() ) lineBuilder.Append( "\n" );
             lineBuilder.Append( token );

+ 10 - 5
src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs

@@ -296,11 +296,16 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
       private void OnEndpointSelected( ConfiguredEndpoint endpoint )
       {
-         _endpoint = endpoint;
-         if( Settings.IsShutdown && !Settings.IsShutdownFatal )
+         if( _endpoint != endpoint )
          {
-            RebootPlugin();
-            ManualHook();
+            _endpoint = endpoint;
+            if( Settings.IsShutdown && !Settings.IsShutdownFatal )
+            {
+               RebootPlugin();
+               ManualHook();
+            }
+
+            Settings.SetEndpoint( _endpoint.Endpoint.Id );
          }
       }
 
@@ -2268,7 +2273,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
          Settings.TranslationCount++; // counts as a translation
          _consecutiveErrors++;
          _batchLogicHasFailed = true;
-         
+
          foreach( var tracker in batch.Trackers )
          {
             tracker.Job.State = TranslationJobState.Failed;

+ 7 - 0
src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs

@@ -93,6 +93,13 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static bool CopyToClipboard;
       public static int MaxClipboardCopyCharacters;
 
+      public static void SetEndpoint( string id )
+      {
+         ServiceEndpoint = id;
+         Config.Current.Preferences[ "Service" ][ "Endpoint" ].Value = id;
+         Config.Current.SaveConfig();
+      }
+
       public static void Configure()
       {
          try

+ 171 - 75
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ExtProtocol/ExtProtocolEndpoint.cs

@@ -14,10 +14,18 @@ using XUnity.AutoTranslator.Plugin.ExtProtocol;
 namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.ExtProtocol
 {
 
-   public abstract class ExtProtocolEndpoint : ITranslateEndpoint, IDisposable
+   public abstract class ExtProtocolEndpoint : MonoBehaviour, ITranslateEndpoint, IDisposable
    {
+      private readonly Dictionary<Guid, StreamReaderResult> _pendingRequests = new Dictionary<Guid, StreamReaderResult>();
+      private readonly object _sync = new object();
+
       private bool _disposed = false;
       private Process _process;
+      private Thread _thread;
+      private bool _startedThread;
+      private bool _initializing;
+      private bool _failed;
+
       protected string _exePath;
       protected string _arguments;
 
@@ -25,126 +33,211 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.ExtProtocol
 
       public abstract string FriendlyName { get; }
 
+      public virtual int MaxConcurrency => 1;
+
       public abstract void Initialize( IInitializationContext context );
 
-      public int MaxConcurrency => 1;
+      private void EnsureInitialized()
+      {
+         if( !_startedThread )
+         {
+            _startedThread = true;
+            _initializing = true;
 
-      public IEnumerator Translate( ITranslationContext context )
+            _thread = new Thread( ReaderLoop );
+            _thread.IsBackground = true;
+            _thread.Start();
+         }
+      }
+
+      private void ReaderLoop( object state )
       {
-         var result = new StreamReaderResult();
          try
          {
-            ThreadPool.QueueUserWorkItem( state =>
+            if( _process == null )
             {
-               try
-               {
-                  if( _process == null )
-                  {
-                     _process = new Process();
-                     _process.StartInfo.FileName = _exePath;
-                     _process.StartInfo.Arguments = _arguments;
-                     _process.EnableRaisingEvents = false;
-                     _process.StartInfo.UseShellExecute = false;
-                     _process.StartInfo.CreateNoWindow = true;
-                     _process.StartInfo.RedirectStandardInput = true;
-                     _process.StartInfo.RedirectStandardOutput = true;
-                     _process.StartInfo.RedirectStandardError = true;
-                     _process.Start();
-
-                     // wait a second...
-                     _process.WaitForExit( 2500 );
-                  }
-
-                  if( _process.HasExited )
-                  {
-                     result.SetCompleted( null, "The translation process exited. Likely due to invalid path to installation." );
-                     return;
-                  }
+               _process = new Process();
+               _process.StartInfo.FileName = _exePath;
+               _process.StartInfo.Arguments = _arguments;
+               _process.EnableRaisingEvents = false;
+               _process.StartInfo.UseShellExecute = false;
+               _process.StartInfo.CreateNoWindow = true;
+               _process.StartInfo.RedirectStandardInput = true;
+               _process.StartInfo.RedirectStandardOutput = true;
+               _process.StartInfo.RedirectStandardError = true;
+               _process.Start();
 
-                  var request = new TranslationRequest
-                  {
-                     Id = Guid.NewGuid(),
-                     SourceLanguage = context.SourceLanguage,
-                     DestinationLanguage = context.DestinationLanguage,
-                     UntranslatedText = context.UntranslatedText
-                  };
-                  var payload = ExtProtocolConvert.Encode( request );
-                  _process.StandardInput.WriteLine( payload );
-
-                  var returnedPayload = _process.StandardOutput.ReadLine();
-                  var response = ExtProtocolConvert.Decode( returnedPayload );
-
-                  HandleProtocolMessage( result, response );
-               }
-               catch( Exception e )
-               {
-                  result.SetCompleted( null, e.Message );
-               }
-            } );
+               // wait a second...
+               _process.WaitForExit( 2500 );
+            }
 
-            // yield-wait for completion
-            if( Features.SupportsCustomYieldInstruction )
+            if( _process.HasExited )
             {
-               yield return result;
+               return;
             }
-            else
+            _initializing = false;
+
+            while( !_disposed )
             {
-               while( !result.IsCompleted )
-               {
-                  yield return new WaitForSeconds( 0.2f );
-               }
+               var returnedPayload = _process.StandardOutput.ReadLine();
+               var response = ExtProtocolConvert.Decode( returnedPayload );
+               HandleProtocolMessage( response );
             }
+         }
+         catch( Exception e )
+         {
+            _failed = true;
+            _initializing = false;
 
-            try
+            XuaLogger.Current.Error( e, "Error occurred while reading standard output from external process." );
+         }
+      }
+
+      void Update()
+      {
+         if( Time.frameCount % 30 == 0 )
+         {
+            lock( _sync )
             {
-               if( result.Succeeded )
+               var time = Time.realtimeSinceStartup;
+
+               List<Guid> idsToRemove = null;
+               foreach( var kvp in _pendingRequests )
                {
-                  context.Complete( result.Result );
+                  var elapsed = time - kvp.Value.StartTime;
+                  if( elapsed > 60 )
+                  {
+                     if( idsToRemove == null )
+                     {
+                        idsToRemove = new List<Guid>();
+                     }
+
+                     idsToRemove.Add( kvp.Key );
+                     kvp.Value.SetCompleted( null, "Request timed out." );
+                  }
                }
-               else
+
+               if( idsToRemove != null )
                {
-                  context.Fail( "Error occurred while retrieving translation." + Environment.NewLine + result.Error, null );
+                  foreach( var id in idsToRemove )
+                  {
+                     _pendingRequests.Remove( id );
+                  }
                }
             }
-            catch( Exception e )
+         }
+      }
+
+      public IEnumerator Translate( ITranslationContext context )
+      {
+         EnsureInitialized();
+
+         while( _initializing && !_failed )
+         {
+            yield return new WaitForSeconds( 0.2f );
+         }
+
+         if( _failed )
+         {
+            context.Fail( "Translator failed.", null );
+            yield break;
+         }
+
+         var result = new StreamReaderResult();
+         var id = Guid.NewGuid();
+
+         lock( _sync )
+         {
+            _pendingRequests[ id ] = result;
+         }
+
+         try
+         {
+            var request = new TranslationRequest
+            {
+               Id = id,
+               SourceLanguage = context.SourceLanguage,
+               DestinationLanguage = context.DestinationLanguage,
+               UntranslatedText = context.UntranslatedText
+            };
+            var payload = ExtProtocolConvert.Encode( request );
+
+            _process.StandardInput.WriteLine( payload );
+         }
+         catch( Exception e )
+         {
+            result.SetCompleted( null, e.Message );
+         }
+
+         // yield-wait for completion
+         if( Features.SupportsCustomYieldInstruction )
+         {
+            yield return result;
+         }
+         else
+         {
+            while( !result.IsCompleted )
             {
-               context.Fail( "Error occurred while retrieving translation.", e );
+               yield return new WaitForSeconds( 0.2f );
             }
          }
-         finally
+
+         if( result.Succeeded )
+         {
+            context.Complete( result.Result );
+         }
+         else
          {
-            result = null;
+            context.Fail( "Error occurred while retrieving translation." + Environment.NewLine + result.Error, null );
          }
       }
 
-      private static void HandleProtocolMessage( StreamReaderResult result, ProtocolMessage message )
+      private void HandleProtocolMessage( ProtocolMessage message )
       {
          switch( message )
          {
             case TranslationResponse translationResponse:
-               HandleTranslationResponse( result, translationResponse );
+               HandleTranslationResponse( translationResponse );
                break;
             case TranslationError translationResponse:
-               HandleTranslationError( result, translationResponse );
+               HandleTranslationError( translationResponse );
                break;
             default:
-               result.SetCompleted( null, "Received invalid response." );
                break;
          }
       }
 
-      private static void HandleTranslationResponse( StreamReaderResult result, TranslationResponse message )
+      private void HandleTranslationResponse( TranslationResponse message )
       {
-         result.SetCompleted( message.TranslatedText, null );
+         lock( _sync )
+         {
+            if( _pendingRequests.TryGetValue( message.Id, out var result ) )
+            {
+               result.SetCompleted( message.TranslatedText, null );
+               _pendingRequests.Remove( message.Id );
+            }
+         }
       }
 
-      private static void HandleTranslationError( StreamReaderResult result, TranslationError message )
+      private void HandleTranslationError( TranslationError message )
       {
-         result.SetCompleted( null, message.Reason );
+         lock( _sync )
+         {
+            if( _pendingRequests.TryGetValue( message.Id, out var result ) )
+            {
+               result.SetCompleted( null, message.Reason );
+               _pendingRequests.Remove( message.Id );
+            }
+         }
       }
 
       private class StreamReaderResult : CustomYieldInstructionShim
       {
+         public StreamReaderResult()
+         {
+            StartTime = Time.realtimeSinceStartup;
+         }
+
          public void SetCompleted( string result, string error )
          {
             IsCompleted = true;
@@ -154,6 +247,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.ExtProtocol
 
          public override bool keepWaiting => !IsCompleted;
 
+         public float StartTime { get; set; }
+
          public string Result { get; set; }
 
          public string Error { get; set; }
@@ -171,10 +266,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.ExtProtocol
          {
             if( disposing )
             {
-               if(_process != null )
+               if( _process != null )
                {
                   _process.Kill();
                   _process.Dispose();
+                  _thread.Abort();
                }
             }
 

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/HttpEndpoint.cs

@@ -11,7 +11,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
 
       public abstract string FriendlyName { get; }
 
-      public int MaxConcurrency => 1;
+      public virtual int MaxConcurrency => 1;
 
       public abstract void Initialize( IInitializationContext context );
 

+ 3 - 3
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ITranslateEndpoint.cs

@@ -10,7 +10,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
    public interface ITranslateEndpoint
    {
       /// <summary>
-      /// Gets the id of the IKnownEndpoint that is used as a configuration parameter.
+      /// Gets the id of the ITranslateEndpoint that is used as a configuration parameter.
       /// </summary>
       string Id { get; }
 
@@ -31,8 +31,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
       void Initialize( IInitializationContext context );
 
       /// <summary>
-      /// Attempt to translated the provided untranslated text. Will be used in a "coroutine", so it can be implemented
-      /// in an async fashion.
+      /// Attempt to translated the provided untranslated text. Will be used in a "coroutine",
+      /// so it can be implemented in an asynchronous fashion.
       /// </summary>
       IEnumerator Translate( ITranslationContext context );
    }

+ 23 - 5
src/XUnity.AutoTranslator.Plugin.Core/UI/XuaWindow.cs

@@ -57,10 +57,17 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
             IsShown = false;
          }
 
+
+         var halfSpacing = GUIUtil.ComponentSpacing / 2;
+
+         // GROUP
+         var groupHeight = ( GUIUtil.RowHeight * _toggles.Count ) + ( GUIUtil.ComponentSpacing * ( _toggles.Count ) ) - halfSpacing;
+         GUI.Box( GUIUtil.R( halfSpacing, posy, WindowWidth - GUIUtil.ComponentSpacing, groupHeight ), "" );
+
          foreach( var vm in _toggles )
          {
             var previousValue = vm.IsToggled();
-            var newValue = GUI.Toggle( GUIUtil.R( col1x, posy, col12, GUIUtil.RowHeight ), previousValue, vm.Text );
+            var newValue = GUI.Toggle( GUIUtil.R( col1x, posy + 3, col12, GUIUtil.RowHeight - 3 ), previousValue, vm.Text );
             if( previousValue != newValue )
             {
                vm.OnToggled();
@@ -68,13 +75,18 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
             posy += GUIUtil.RowHeight + GUIUtil.ComponentSpacing;
          }
 
-         GUI.Label( GUIUtil.R( col1x, posy, col12, GUIUtil.LabelHeight ), "---- Command Panel ----", GUIUtil.LabelCenter );
-         posy += GUIUtil.RowHeight + GUIUtil.ComponentSpacing;
-
          const int buttonsPerRow = 3;
          const int buttonWidth = ( col12 - ( GUIUtil.ComponentSpacing * ( buttonsPerRow - 1 ) ) ) / buttonsPerRow;
          var rows = _commandButtons.Count / buttonsPerRow;
          if( _commandButtons.Count % 3 != 0 ) rows++;
+
+         // GROUP
+         groupHeight = GUIUtil.LabelHeight + ( GUIUtil.RowHeight * rows ) + ( GUIUtil.ComponentSpacing * ( rows + 1 ) ) - halfSpacing;
+         GUI.Box( GUIUtil.R( halfSpacing, posy, WindowWidth - GUIUtil.ComponentSpacing, groupHeight ), "" );
+
+         GUI.Label( GUIUtil.R( col1x, posy, col12, GUIUtil.LabelHeight ), "---- Command Panel ----", GUIUtil.LabelCenter );
+         posy += GUIUtil.RowHeight + GUIUtil.ComponentSpacing;
+
          for( int row = 0 ; row < rows ; row++ )
          {
             for( int col = 0 ; col < buttonsPerRow ; col++ )
@@ -96,7 +108,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
             posy += GUIUtil.RowHeight + GUIUtil.ComponentSpacing;
          }
 
-
+         // GROUP
+         groupHeight = GUIUtil.LabelHeight + ( GUIUtil.RowHeight * 1 ) + ( GUIUtil.ComponentSpacing * ( 2 ) ) - halfSpacing;
+         GUI.Box( GUIUtil.R( halfSpacing, posy, WindowWidth - GUIUtil.ComponentSpacing, groupHeight ), "" );
 
          GUI.Label( GUIUtil.R( col1x, posy, col12, GUIUtil.LabelHeight ), "---- Select a Translator ----", GUIUtil.LabelCenter );
          posy += GUIUtil.RowHeight + GUIUtil.ComponentSpacing;
@@ -105,6 +119,10 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
          int endpointDropdownPosy = posy;
          posy += GUIUtil.RowHeight + GUIUtil.ComponentSpacing;
 
+         // GROUP
+         groupHeight = GUIUtil.LabelHeight + ( GUIUtil.RowHeight * _labels.Count ) + ( GUIUtil.ComponentSpacing * ( _labels.Count + 1 ) ) - halfSpacing;
+         GUI.Box( GUIUtil.R( halfSpacing, posy, WindowWidth - GUIUtil.ComponentSpacing, groupHeight ), "" );
+
          GUI.Label( GUIUtil.R( col1x, posy, col12, GUIUtil.LabelHeight ), "---- Status ----", GUIUtil.LabelCenter );
          posy += GUIUtil.RowHeight + GUIUtil.ComponentSpacing;