Jelajahi Sumber

general support for batching for ITranslateEndpoints

randoman 6 tahun lalu
induk
melakukan
f4613bce2d
21 mengubah file dengan 277 tambahan dan 221 penghapusan
  1. 10 0
      README.md
  2. 44 8
      src/Translators/GoogleTranslate/GoogleTranslateEndpoint.cs
  3. 39 3
      src/Translators/GoogleTranslateLegitimate/GoogleTranslateLegitimateEndpoint.cs
  4. 2 0
      src/Translators/ReverseTranslator/ReverseTranslatorEndpoint.cs
  5. 52 53
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs
  6. 0 71
      src/XUnity.AutoTranslator.Plugin.Core/Batching/TranslationBatch.cs
  7. 0 19
      src/XUnity.AutoTranslator.Plugin.Core/Batching/TranslationLineTracker.cs
  8. 0 2
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs
  9. 2 2
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ConfiguredEndpoint.cs
  10. 5 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ExtProtocol/ExtProtocolEndpoint.cs
  11. 5 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/HttpEndpoint.cs
  12. 7 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/HttpTranslationContext.cs
  13. 9 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/IHttpTranslationExtractionContext.cs
  14. 5 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ITranslateEndpoint.cs
  15. 7 34
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ITranslationContext.cs
  16. 45 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ITranslationContextBase.cs
  17. 25 9
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationContext.cs
  18. 9 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/IWwwTranslationExtractionContext.cs
  19. 5 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/WwwEndpoint.cs
  20. 6 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/WwwTranslationContext.cs
  21. 0 20
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/TranslationEndpointExtensions.cs

+ 10 - 0
README.md

@@ -459,6 +459,9 @@ 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#
+/// <summary>
+/// The interface that must be implemented by a translator.
+/// </summary>
 public interface ITranslateEndpoint
 {
    /// <summary>
@@ -477,6 +480,11 @@ public interface ITranslateEndpoint
    /// </summary>
    int MaxConcurrency { get; }
 
+   /// <summary>
+   /// Gets the maximum number of translations that can be served per translation request.
+   /// </summary>
+   int MaxTranslationsPerRequest { get; }
+
    /// <summary>
    /// Called during initialization. Use this to initialize plugin or throw exception if impossible.
    /// </summary>
@@ -539,6 +547,8 @@ public class ReverseTranslatorEndpoint : ITranslateEndpoint
 
    public int MaxConcurrency => 50;
 
+   public int MaxTranslationsPerRequest => 1;
+
    public void Initialize( IInitializationContext context )
    {
       _myConfig = context.GetOrCreateSetting( "Reverser", "MyConfig", true );

+ 44 - 8
src/Translators/GoogleTranslate/GoogleTranslateEndpoint.cs

@@ -29,7 +29,7 @@ namespace GoogleTranslate
       private static readonly string HttpsServicePointRomanizeTemplateUrl = "https://translate.googleapis.com/translate_a/single?client=webapp&sl={0}&tl=en&dt=rm&tk={1}&q={2}";
       private static readonly string HttpsTranslateUserSite = "https://translate.google.com";
       private static readonly string DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36";
-      private static readonly System.Random RandomNumbers = new System.Random();
+      private static readonly Random RandomNumbers = new Random();
 
       private static readonly string[] Accepts = new string[] { null, "*/*", "application/json" };
       private static readonly string[] AcceptLanguages = new string[] { null, "en-US,en;q=0.9", "en-US", "en" };
@@ -55,6 +55,8 @@ namespace GoogleTranslate
 
       public override string FriendlyName => "Google! Translate";
 
+      public override int MaxTranslationsPerRequest => 10;
+
       public override void Initialize( IInitializationContext context )
       {
          context.DisableCerfificateChecksFor( "translate.google.com", "translate.googleapis.com" );
@@ -80,6 +82,8 @@ namespace GoogleTranslate
 
       public override void OnCreateRequest( IHttpRequestCreationContext context )
       {
+         var allUntranslatedText = string.Join( "\n", context.UntranslatedTexts );
+
          XUnityWebRequest request;
          if( context.DestinationLanguage == "romaji" )
          {
@@ -87,8 +91,8 @@ namespace GoogleTranslate
                string.Format(
                   HttpsServicePointRomanizeTemplateUrl,
                   context.SourceLanguage,
-                  Tk( context.UntranslatedText ),
-                  Uri.EscapeDataString( context.UntranslatedText ) ) );
+                  Tk( allUntranslatedText ),
+                  Uri.EscapeDataString( allUntranslatedText ) ) );
          }
          else
          {
@@ -97,8 +101,8 @@ namespace GoogleTranslate
                   HttpsServicePointTranslateTemplateUrl,
                   context.SourceLanguage,
                   context.DestinationLanguage,
-                  Tk( context.UntranslatedText ),
-                  Uri.EscapeDataString( context.UntranslatedText ) ) );
+                  Tk( allUntranslatedText ),
+                  Uri.EscapeDataString( allUntranslatedText ) ) );
          }
 
          request.Cookies = _cookieContainer;
@@ -125,14 +129,46 @@ namespace GoogleTranslate
             var token = entry.AsArray[ dataIndex ].ToString();
             token = JsonHelper.Unescape( token.Substring( 1, token.Length - 2 ) );
 
-            if( !lineBuilder.EndsWithWhitespaceOrNewline() ) lineBuilder.Append( "\n" );
+            if( !lineBuilder.EndsWithWhitespaceOrNewline() ) lineBuilder.Append( '\n' );
 
             lineBuilder.Append( token );
          }
 
-         var translated = lineBuilder.ToString();
+         var allTranslation = lineBuilder.ToString();
+
+         if( context.UntranslatedTexts.Length == 1 )
+         {
+            context.Complete( allTranslation );
+         }
+         else
+         {
+            var translatedLines = allTranslation.Split( '\n' );
+            var translatedTexts = new List<string>();
+
+            int current = 0;
+            foreach( var untranslatedText in context.UntranslatedTexts )
+            {
+               var untranslatedLines = untranslatedText.Split( '\n' );
+               var untranslatedLinesCount = untranslatedLines.Length;
+               var translatedText = string.Empty;
+
+               for( int i = 0 ; i < untranslatedLinesCount ; i++ )
+               {
+                  if( current >= translatedLines.Length ) context.Fail( "Batch operation received incorrect number of translations." );
+
+                  var translatedLine = translatedLines[ current++ ];
+                  translatedText += translatedLine;
+
+                  if( i != untranslatedLinesCount - 1 ) translatedText += '\n';
+               }
 
-         context.Complete( translated );
+               translatedTexts.Add( translatedText );
+            }
+
+            if( current != translatedLines.Length ) context.Fail( "Batch operation received incorrect number of translations." );
+
+            context.Complete( translatedTexts.ToArray() );
+         }
       }
 
       private XUnityWebRequest CreateWebSiteRequest()

+ 39 - 3
src/Translators/GoogleTranslateLegitimate/GoogleTranslateLegitimateEndpoint.cs

@@ -28,6 +28,8 @@ namespace GoogleTranslateLegitimate
 
       public override string FriendlyName => "Google! Translate (Authenticated)";
 
+      public override int MaxTranslationsPerRequest => 10;
+
       public override void Initialize( IInitializationContext context )
       {
          _key = context.GetOrCreateSetting( "GoogleLegitimate", "GoogleAPIKey", "" );
@@ -42,9 +44,11 @@ namespace GoogleTranslateLegitimate
 
       public override void OnCreateRequest( IHttpRequestCreationContext context )
       {
+         var allUntranslatedText = string.Join( "\n", context.UntranslatedTexts );
+
          var b = new StringBuilder();
          b.Append( "{" );
-         b.Append( "\"q\":\"" ).Append( JsonHelper.Escape( context.UntranslatedText ) ).Append( "\"," );
+         b.Append( "\"q\":\"" ).Append( JsonHelper.Escape( allUntranslatedText ) ).Append( "\"," );
          b.Append( "\"target\":\"" ).Append( context.DestinationLanguage ).Append( "\"," );
          b.Append( "\"source\":\"" ).Append( context.SourceLanguage ).Append( "\"," );
          b.Append( "\"format\":\"text\"" );
@@ -75,9 +79,41 @@ namespace GoogleTranslateLegitimate
             lineBuilder.Append( token );
          }
 
-         var translated = lineBuilder.ToString();
+         var allTranslation = lineBuilder.ToString();
+
+         if( context.UntranslatedTexts.Length == 1 )
+         {
+            context.Complete( allTranslation );
+         }
+         else
+         {
+            var translatedLines = allTranslation.Split( '\n' );
+            var translatedTexts = new List<string>();
+
+            int current = 0;
+            foreach( var untranslatedText in context.UntranslatedTexts )
+            {
+               var untranslatedLines = untranslatedText.Split( '\n' );
+               var untranslatedLinesCount = untranslatedLines.Length;
+               var translatedText = string.Empty;
 
-         context.Complete( translated );
+               for( int i = 0 ; i < untranslatedLinesCount ; i++ )
+               {
+                  if( current >= translatedLines.Length ) context.Fail( "Batch operation received incorrect number of translations." );
+
+                  var translatedLine = translatedLines[ current++ ];
+                  translatedText += translatedLine;
+
+                  if( i != untranslatedLinesCount - 1 ) translatedText += '\n';
+               }
+
+               translatedTexts.Add( translatedText );
+            }
+
+            if( current != translatedLines.Length ) context.Fail( "Batch operation received incorrect number of translations." );
+
+            context.Complete( translatedTexts.ToArray() );
+         }
       }
    }
 }

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

@@ -17,6 +17,8 @@ namespace ReverseTranslator
 
       public int MaxConcurrency => 50;
 
+      public int MaxTranslationsPerRequest => 1;
+
       public void Initialize( IInitializationContext context )
       {
          _myConfig = context.GetOrCreateSetting( "Reverser", "MyConfig", true );

+ 52 - 53
src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs

@@ -24,7 +24,6 @@ using XUnity.AutoTranslator.Plugin.Core.Hooks.NGUI;
 using UnityEngine.SceneManagement;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Debugging;
-using XUnity.AutoTranslator.Plugin.Core.Batching;
 using Harmony;
 using XUnity.AutoTranslator.Plugin.Core.Parsing;
 using System.Diagnostics;
@@ -2095,14 +2094,16 @@ namespace XUnity.AutoTranslator.Plugin.Core
       {
          if( _endpoint == null ) return;
 
-         if( Settings.EnableBatching && _endpoint.Endpoint.SupportsLineSplitting() && !_batchLogicHasFailed && _unstartedJobs.Count > 1 && _availableBatchOperations > 0 )
+         if( Settings.EnableBatching && _endpoint.Endpoint.MaxTranslationsPerRequest > 1 && !_batchLogicHasFailed && _unstartedJobs.Count > 1 && _availableBatchOperations > 0 )
          {
             while( _unstartedJobs.Count > 0 && _availableBatchOperations > 0 )
             {
                if( _endpoint.IsBusy ) break;
 
-               var kvps = _unstartedJobs.Take( Settings.BatchSize ).ToList();
-               var batch = new TranslationBatch();
+               var kvps = _unstartedJobs.Take( _endpoint.Endpoint.MaxTranslationsPerRequest ).ToList();
+               //var batch = new TranslationBatch();
+               var untranslatedTexts = new List<string>();
+               var jobs = new List<TranslationJob>();
 
                foreach( var kvp in kvps )
                {
@@ -2112,23 +2113,28 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                   if( !job.AnyComponentsStillHasOriginalUntranslatedTextOrContextual() ) continue;
 
-                  batch.Add( job );
+                  jobs.Add( job );
+                  untranslatedTexts.Add( job.Key.GetDictionaryLookupKey() );
+
                   _ongoingJobs[ key ] = job;
                }
 
-               if( !batch.IsEmpty )
+               if( jobs.Count > 0 )
                {
                   _availableBatchOperations--;
+                  var jobsArray = jobs.ToArray();
 
-                  var untranslatedText = batch.GetFullTranslationKey();
-                  XuaLogger.Current.Debug( "Starting translation for: " + untranslatedText );
+                  foreach( var untranslatedText in untranslatedTexts )
+                  {
+                     XuaLogger.Current.Debug( "Starting translation for: " + untranslatedText );
+                  }
                   StartCoroutine(
                      _endpoint.Translate(
-                        untranslatedText,
+                        untranslatedTexts.ToArray(),
                         Settings.FromLanguage,
                         Settings.Language,
-                        translatedText => OnBatchTranslationCompleted( batch, translatedText ),
-                        ( msg, e ) => OnTranslationFailed( batch, msg, e ) ) );
+                        translatedText => OnBatchTranslationCompleted( jobsArray, translatedText ),
+                        ( msg, e ) => OnTranslationFailed( jobsArray, msg, e ) ) );
                }
             }
          }
@@ -2155,29 +2161,28 @@ namespace XUnity.AutoTranslator.Plugin.Core
                XuaLogger.Current.Debug( "Starting translation for: " + untranslatedText );
                StartCoroutine(
                   _endpoint.Translate(
-                     untranslatedText,
+                     new[] { untranslatedText },
                      Settings.FromLanguage,
                      Settings.Language,
                      translatedText => OnSingleTranslationCompleted( job, translatedText ),
-                     ( msg, e ) => OnTranslationFailed( job, msg, e ) ) );
+                     ( msg, e ) => OnTranslationFailed( new[] { job }, msg, e ) ) );
             }
          }
       }
 
-      private void OnBatchTranslationCompleted( TranslationBatch batch, string translatedTextBatch )
+      private void OnBatchTranslationCompleted( TranslationJob[] jobs, string[] translatedTexts )
       {
          _consecutiveErrors = 0;
-         XuaLogger.Current.Debug( $"Translation for '{batch.GetFullTranslationKey()}' succeded. Result: {translatedTextBatch}" );
 
-         var succeeded = batch.MatchWithTranslations( translatedTextBatch );
+         var succeeded = jobs.Length == translatedTexts.Length;
          if( succeeded )
          {
-            foreach( var tracker in batch.Trackers )
+            for( int i = 0 ; i < jobs.Length ; i++ )
             {
                Settings.TranslationCount++;
 
-               var job = tracker.Job;
-               var translatedText = tracker.RawTranslatedText;
+               var job = jobs[ i ];
+               var translatedText = translatedTexts[ i ];
                if( !string.IsNullOrEmpty( translatedText ) )
                {
                   if( Settings.ForceSplitTextAfterCharacters > 0 )
@@ -2193,20 +2198,23 @@ namespace XUnity.AutoTranslator.Plugin.Core
                AddTranslation( job.Key, job.TranslatedText );
                job.State = TranslationJobState.Succeeded;
                _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
+
+               XuaLogger.Current.Debug( $"Translation for '{job.Key.GetDictionaryLookupKey()}' succeded. Result: {translatedText}" );
             }
          }
          else
          {
             // might as well re-add all translation jobs, and never do this again!
             _batchLogicHasFailed = true;
-            foreach( var tracker in batch.Trackers )
+            for( int i = 0 ; i < jobs.Length ; i++ )
             {
                Settings.TranslationCount++;
+               var job = jobs[ i ];
 
-               var key = tracker.Job.Key.GetDictionaryLookupKey();
+               var key = job.Key.GetDictionaryLookupKey();
                if( !_unstartedJobs.ContainsKey( key ) )
                {
-                  _unstartedJobs[ key ] = tracker.Job;
+                  _unstartedJobs[ key ] = job;
                }
                _ongoingJobs.Remove( key );
             }
@@ -2229,8 +2237,10 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      private void OnSingleTranslationCompleted( TranslationJob job, string translatedText )
+      private void OnSingleTranslationCompleted( TranslationJob job, string[] translatedTextArray )
       {
+         var translatedText = translatedTextArray[ 0 ];
+
          Settings.TranslationCount++;
          XuaLogger.Current.Debug( $"Translation for '{job.Key.GetDictionaryLookupKey()}' succeded. Result: {translatedText}" );
 
@@ -2267,7 +2277,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      private void OnTranslationFailed( TranslationJob job, string error, Exception e )
+      private void OnTranslationFailed( TranslationJob[] jobs, string error, Exception e )
       {
          if( e == null )
          {
@@ -2278,45 +2288,34 @@ namespace XUnity.AutoTranslator.Plugin.Core
             XuaLogger.Current.Error( e, error );
          }
 
-         Settings.TranslationCount++; // counts as a translation
          _consecutiveErrors++;
 
-         job.State = TranslationJobState.Failed;
-         _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
-
-         if( !Settings.IsShutdown )
+         if( jobs.Length == 1 )
          {
-            if( _consecutiveErrors >= Settings.MaxErrors )
+            Settings.TranslationCount++; // counts as a translation
+            foreach( var job in jobs )
             {
-               Settings.IsShutdown = true;
-               XuaLogger.Current.Error( $"{Settings.MaxErrors} or more consecutive errors occurred. Shutting down plugin." );
-
-               _unstartedJobs.Clear();
-               _completedJobs.Clear();
-               _ongoingJobs.Clear();
+               job.State = TranslationJobState.Failed;
+               _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
             }
          }
-      }
-
-      private void OnTranslationFailed( TranslationBatch batch, string error, Exception e )
-      {
-         if( e == null )
-         {
-            XuaLogger.Current.Error( error );
-         }
          else
          {
-            XuaLogger.Current.Error( e, error );
-         }
+            _batchLogicHasFailed = true;
+            for( int i = 0 ; i < jobs.Length ; i++ )
+            {
+               Settings.TranslationCount++;
+               var job = jobs[ i ];
 
-         Settings.TranslationCount++; // counts as a translation
-         _consecutiveErrors++;
-         _batchLogicHasFailed = true;
+               var key = job.Key.GetDictionaryLookupKey();
+               if( !_unstartedJobs.ContainsKey( key ) )
+               {
+                  _unstartedJobs[ key ] = job;
+               }
+               _ongoingJobs.Remove( key );
+            }
 
-         foreach( var tracker in batch.Trackers )
-         {
-            tracker.Job.State = TranslationJobState.Failed;
-            _ongoingJobs.Remove( tracker.Job.Key.GetDictionaryLookupKey() );
+            XuaLogger.Current.Error( "A batch operation failed. Disabling batching and restarting failed jobs." );
          }
 
          if( !Settings.IsShutdown )

+ 0 - 71
src/XUnity.AutoTranslator.Plugin.Core/Batching/TranslationBatch.cs

@@ -1,71 +0,0 @@
-using System.Collections.Generic;
-using System.Text;
-
-namespace XUnity.AutoTranslator.Plugin.Core.Batching
-{
-   internal class TranslationBatch
-   {
-      public TranslationBatch()
-      {
-         Trackers = new List<TranslationLineTracker>();
-      }
-
-      public List<TranslationLineTracker> Trackers { get; private set; }
-
-      public bool IsEmpty => Trackers.Count == 0;
-
-      public int TotalLinesCount { get; set; }
-
-      public void Add( TranslationJob job )
-      {
-         var lines = new TranslationLineTracker( job );
-         Trackers.Add( lines );
-         TotalLinesCount += lines.LinesCount;
-      }
-
-      public bool MatchWithTranslations( string allTranslations )
-      {
-         var lines = allTranslations.Split( '\n' );
-
-         if( lines.Length != TotalLinesCount ) return false;
-
-         int current = 0;
-         foreach( var tracker in Trackers )
-         {
-            var builder = new StringBuilder( 32 );
-            for( int i = 0 ; i < tracker.LinesCount ; i++ )
-            {
-               var translation = lines[ current++ ];
-               builder.Append( translation );
-
-               // ADD NEW LINE IF NEEDED
-               if( !( i == tracker.LinesCount - 1 ) ) // if not last line
-               {
-                  builder.Append( '\n' );
-               }
-            }
-            var fullTranslation = builder.ToString();
-
-            tracker.RawTranslatedText = fullTranslation;
-         }
-
-         return true;
-      }
-
-      public string GetFullTranslationKey()
-      {
-         var builder = new StringBuilder();
-         for( int i = 0 ; i < Trackers.Count ; i++ )
-         {
-            var tracker = Trackers[ i ];
-            builder.Append( tracker.Job.Key.GetDictionaryLookupKey() );
-
-            if( !( i == Trackers.Count - 1 ) )
-            {
-               builder.Append( '\n' );
-            }
-         }
-         return builder.ToString();
-      }
-   }
-}

+ 0 - 19
src/XUnity.AutoTranslator.Plugin.Core/Batching/TranslationLineTracker.cs

@@ -1,19 +0,0 @@
-using System.Linq;
-
-namespace XUnity.AutoTranslator.Plugin.Core.Batching
-{
-   internal class TranslationLineTracker
-   {
-      public TranslationLineTracker( TranslationJob job )
-      {
-         Job = job;
-         LinesCount = job.Key.GetDictionaryLookupKey().Count( c => c == '\n' ) + 1;
-      }
-
-      public string RawTranslatedText { get; set; }
-
-      public TranslationJob Job { get; private set; }
-
-      public int LinesCount { get; private set; }
-   }
-}

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

@@ -41,8 +41,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static readonly int MaxSecondsAboveTranslationThreshold = 30;
       public static readonly int TranslationQueueWatchWindow = 6;
 
-      public static readonly int BatchSize = 10;
-
       // can be changed
       public static string ServiceEndpoint;
       public static string Language;

+ 2 - 2
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ConfiguredEndpoint.cs

@@ -20,9 +20,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 
       public bool IsBusy => _ongoingTranslations >= Endpoint.MaxConcurrency;
 
-      public IEnumerator Translate( string untranslatedText, string from, string to, Action<string> success, Action<string, Exception> failure )
+      public IEnumerator Translate( string[] untranslatedTexts, string from, string to, Action<string[]> success, Action<string, Exception> failure )
       {
-         var context = new TranslationContext( untranslatedText, from, to, success, failure );
+         var context = new TranslationContext( untranslatedTexts, from, to, success, failure );
          _ongoingTranslations++;
 
          bool ok = false;

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

@@ -44,6 +44,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.ExtProtocol
       /// </summary>
       public virtual int MaxConcurrency => 1;
 
+      /// <summary>
+      /// Gets the maximum number of translations that can be served per translation request.
+      /// </summary>
+      public int MaxTranslationsPerRequest => 1;
+
       /// <summary>
       /// Gets the path to the executable that should be communicated with.
       /// </summary>

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

@@ -26,6 +26,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
       /// </summary>
       public virtual int MaxConcurrency => 1;
 
+      /// <summary>
+      /// Gets the maximum number of translations that can be served per translation request.
+      /// </summary>
+      public virtual int MaxTranslationsPerRequest => 1;
+
       /// <summary>
       /// Called during initialization. Use this to initialize plugin or throw exception if impossible.
       /// </summary>

+ 7 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/HttpTranslationContext.cs

@@ -16,6 +16,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
       }
 
       public string UntranslatedText => _context.UntranslatedText;
+      public string[] UntranslatedTexts => _context.UntranslatedTexts;
       public string SourceLanguage => _context.SourceLanguage;
       public string DestinationLanguage => _context.DestinationLanguage;
       public XUnityWebResponse Response { get; internal set; }
@@ -31,6 +32,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
          _context.Fail( reason );
       }
 
+
       void IHttpRequestCreationContext.Complete( XUnityWebRequest request )
       {
          Request = request;
@@ -40,5 +42,10 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
       {
          _context.Complete( translatedText );
       }
+
+      void IHttpTranslationExtractionContext.Complete( string[] translatedTexts )
+      {
+         _context.Complete( translatedTexts );
+      }
    }
 }

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

@@ -12,5 +12,14 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
       /// </summary>
       /// <param name="translatedText"></param>
       void Complete( string translatedText );
+
+      /// <summary>
+      /// Completes the translation by providing the translated texts.
+      ///
+      /// The indices of the translations must match the indices of the
+      /// untranslated texts.
+      /// </summary>
+      /// <param name="translatedTexts"></param>
+      void Complete( string[] translatedTexts );
    }
 }

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

@@ -28,6 +28,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
       /// </summary>
       int MaxConcurrency { get; }
 
+      /// <summary>
+      /// Gets the maximum number of translations that can be served per translation request.
+      /// </summary>
+      int MaxTranslationsPerRequest { get; }
+
       /// <summary>
       /// Called during initialization. Use this to initialize plugin or throw exception if impossible.
       /// </summary>

+ 7 - 34
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ITranslationContext.cs

@@ -1,6 +1,4 @@
-using System;
-
-namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 {
    /// <summary>
    /// Interface used in the context of translating a text.
@@ -12,39 +10,14 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
       /// </summary>
       /// <param name="translatedText"></param>
       void Complete( string translatedText );
-   }
-
-   /// <summary>
-   /// Interface used in the context of translating a text.
-   /// </summary>
-   public interface ITranslationContextBase
-   {
-      /// <summary>
-      /// Gets the untranslated text.
-      /// </summary>
-      string UntranslatedText { get; }
-
-      /// <summary>
-      /// Gets the source language.
-      /// </summary>
-      string SourceLanguage { get; }
-
-      /// <summary>
-      /// Gets the destination language.
-      /// </summary>
-      string DestinationLanguage { get; }
-
-      /// <summary>
-      /// Fails the translation. Immediately throws an exception.
-      /// </summary>
-      /// <param name="reason"></param>
-      /// <param name="exception"></param>
-      void Fail( string reason, Exception exception );
 
       /// <summary>
-      /// Fails the translation. Immediately throws an exception.
+      /// Completes the translation by providing the translated texts.
+      ///
+      /// The indices of the translations must match the indices of the
+      /// untranslated texts.
       /// </summary>
-      /// <param name="reason"></param>
-      void Fail( string reason );
+      /// <param name="translatedTexts"></param>
+      void Complete( string[] translatedTexts );
    }
 }

+ 45 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ITranslationContextBase.cs

@@ -0,0 +1,45 @@
+using System;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
+{
+   /// <summary>
+   /// Interface used in the context of translating a text.
+   /// </summary>
+   public interface ITranslationContextBase
+   {
+      /// <summary>
+      /// Gets the first untranslated text.
+      /// </summary>
+      string UntranslatedText { get; }
+
+      /// <summary>
+      /// Gets all the untranslated texts. The number of texts
+      /// in array cannot be greater than the MaxTranslationsPerRequest
+      /// in the ITranslateEndpoint interface.
+      /// </summary>
+      string[] UntranslatedTexts { get; }
+
+      /// <summary>
+      /// Gets the source language.
+      /// </summary>
+      string SourceLanguage { get; }
+
+      /// <summary>
+      /// Gets the destination language.
+      /// </summary>
+      string DestinationLanguage { get; }
+
+      /// <summary>
+      /// Fails the translation. Immediately throws an exception.
+      /// </summary>
+      /// <param name="reason"></param>
+      /// <param name="exception"></param>
+      void Fail( string reason, Exception exception );
+
+      /// <summary>
+      /// Fails the translation. Immediately throws an exception.
+      /// </summary>
+      /// <param name="reason"></param>
+      void Fail( string reason );
+   }
+}

+ 25 - 9
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationContext.cs

@@ -4,17 +4,17 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 {
    internal class TranslationContext : ITranslationContext
    {
-      private Action<string> _complete;
+      private Action<string[]> _complete;
       private Action<string, Exception> _fail;
 
       public TranslationContext(
-         string untranslatedText,
+         string[] untranslatedTexts,
          string sourceLanguage,
          string destinationLanguage,
-         Action<string> complete,
+         Action<string[]> complete,
          Action<string, Exception> fail )
       {
-         UntranslatedText = untranslatedText;
+         UntranslatedTexts = untranslatedTexts;
          SourceLanguage = sourceLanguage;
          DestinationLanguage = destinationLanguage;
 
@@ -22,26 +22,42 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          _fail = fail;
       }
 
-      public string UntranslatedText { get; }
+      public string UntranslatedText => UntranslatedTexts[ 0 ];
+      public string[] UntranslatedTexts { get; }
       public string SourceLanguage { get; }
       public string DestinationLanguage { get; }
 
       internal bool IsDone { get; private set; }
 
+
       public void Complete( string translatedText )
+      {
+         Complete( new[] { translatedText } );
+      }
+
+      public void Complete( string[] translatedTexts )
       {
          if( IsDone ) return;
 
          try
          {
-            if( !string.IsNullOrEmpty( translatedText ) )
+            if( translatedTexts.Length == 0 )
             {
-               _complete( translatedText );
+               _fail( "Received empty translation from translator.", null );
+               return;
             }
-            else
+
+            for( int i = 0 ; i < translatedTexts.Length ; i++ )
             {
-               _fail( "Received empty translation from translator.", null );
+               var translatedText = translatedTexts[ 0 ];
+               if( string.IsNullOrEmpty( translatedText ) )
+               {
+                  _fail( "Received empty translation from translator.", null );
+                  return;
+               }
             }
+
+            _complete( translatedTexts );
          }
          finally
          {

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

@@ -15,5 +15,14 @@
       /// </summary>
       /// <param name="translatedText"></param>
       void Complete( string translatedText );
+
+      /// <summary>
+      /// Completes the translation by providing the translated texts.
+      ///
+      /// The indices of the translations must match the indices of the
+      /// untranslated texts.
+      /// </summary>
+      /// <param name="translatedTexts"></param>
+      void Complete( string[] translatedTexts );
    }
 }

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

@@ -37,6 +37,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
       /// </summary>
       public int MaxConcurrency => 1;
 
+      /// <summary>
+      /// Gets the maximum number of translations that can be served per translation request.
+      /// </summary>
+      public virtual int MaxTranslationsPerRequest => 1;
+
       /// <summary>
       /// Callback that can be overwritten that is called before any requests are sent out.
       /// </summary>

+ 6 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/WwwTranslationContext.cs

@@ -13,6 +13,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
          _context = context;
       }
 
+      public string[] UntranslatedTexts => _context.UntranslatedTexts;
       public string UntranslatedText => _context.UntranslatedText;
       public string SourceLanguage => _context.SourceLanguage;
       public string DestinationLanguage => _context.DestinationLanguage;
@@ -30,6 +31,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
          _context.Complete( translatedText );
       }
 
+      void IWwwTranslationExtractionContext.Complete( string[] translatedTexts )
+      {
+         _context.Complete( translatedTexts );
+      }
+
       public void Fail( string reason, Exception exception )
       {
          _context.Fail( reason, exception );

+ 0 - 20
src/XUnity.AutoTranslator.Plugin.Core/Extensions/TranslationEndpointExtensions.cs

@@ -1,20 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using XUnity.AutoTranslator.Plugin.Core.Constants;
-using XUnity.AutoTranslator.Plugin.Core.Endpoints;
-using XUnity.AutoTranslator.Plugin.Core.Endpoints.Http;
-using XUnity.AutoTranslator.Plugin.Core.Web;
-
-namespace XUnity.AutoTranslator.Plugin.Core.Extensions
-{
-   internal static class TranslationEndpointExtensions
-   {
-      public static bool SupportsLineSplitting( this ITranslateEndpoint endpoint )
-      {
-         return endpoint.Id == KnownTranslateEndpointNames.GoogleTranslate
-            || endpoint.Id == KnownTranslateEndpointNames.GoogleTranslateLegitimate;
-      }
-   }
-}