فهرست منبع

Version 3.3.0

 * FEATURE - Support 'TARC' regex formatting in translation files
 * FEATURE - Much improved handling of whitespace and newlines. Option 'TrimAllText' removed and options for 'WhitespaceRemovalStrategy' changed
 * BUG FIX - Allow hooking of text with components named 'Dummy'
randoman 6 سال پیش
والد
کامیت
eed912b200

+ 2 - 2
CHANGELOG.md

@@ -1,6 +1,6 @@
 ### 3.3.0
- * FEATURE - Support TARC regex formatting in translation files
- * MISC - The text trimming process will now maintain all prepended newlines regardless of the translator used
+ * FEATURE - Support 'TARC' regex formatting in translation files
+ * FEATURE - Much improved handling of whitespace and newlines. Option 'TrimAllText' removed and options for 'WhitespaceRemovalStrategy' changed
  * BUG FIX - Allow hooking of text with components named 'Dummy'
 
 ### 3.2.0

+ 8 - 8
README.md

@@ -29,7 +29,7 @@ From version 3.0.0 it is possible to implement custom translators. See [this sec
 
 ## Plugin Frameworks
 The mod can be installed into the following Plugin Managers:
- * [BepInEx](https://github.com/bbepis/BepInEx) (preferred approach)
+ * [BepInEx](https://github.com/bbepis/BepInEx) (recommended approach)
  * [IPA](https://github.com/Eusth/IPA)
  * UnityInjector
 
@@ -249,10 +249,9 @@ CopyToClipboard=False            ;Whether or not to copy hooked texts to clipboa
 MaxClipboardCopyCharacters=450   ;Max number of characters to hook to clipboard at a time
 EnableUIResizing=True            ;Whether or not the plugin should provide a "best attempt" at resizing UI components upon translation. Only work for NGUI
 EnableBatching=True              ;Indicates whether batching of translations should be enabled for supported endpoints
-TrimAllText=True                 ;Indicates whether spaces in front and behind translation candidates should be removed before translation
 UseStaticTranslations=True       ;Indicates whether or not to use translations from the included static translation cache
 OverrideFont=                    ;Overrides the fonts used for texts when updating text components. NOTE: Only works for UGUI
-WhitespaceRemovalStrategy=TrimPerNewline ;Indicates how whitespace/newline removal should be handled before attempting translation. Can be ["TrimPerNewline", "AllOccurrences"]
+WhitespaceRemovalStrategy=TrimPerNewline ;Indicates how whitespace/newline removal should be handled before attempting translation. Can be ["TrimPerNewline", "None"]
 ResizeUILineSpacingScale=        ;A decimal value that the default line spacing should be scaled by during UI resizing, for example: 0.80. NOTE: Only works for UGUI
 ForceUIResizing=True             ;Indicates whether the UI resize behavior should be applied to all UI components regardless of them being translated.
 IgnoreTextStartingWith=\u180e;   ;Indicates that the plugin should ignore any strings starting with certain characters. This is a list seperated by ';'.
@@ -318,17 +317,18 @@ When it comes to automated translations, proper whitespace handling can really m
  * `IgnoreWhitespaceInNGUI`
  * `MinDialogueChars`
  * `ForceSplitTextAfterCharacters`
- * `TrimAllText`
  * `WhitespaceRemovalStrategy`
 
-The first thing the plugin does when it discovers a new text is trim any whitespace, if `TrimAllText` is configured. This does not include newlines or "special" whitespace such as japanese whitespace.
+The plugin first determines whether or not it should perform a special whitespace removal operation. How it removes the whitespace is based on the parameter `WhitespaceRemomvalStrategy`. The default value of this parameter is recommended. The other option 'None' may cause poor translations.
 
-After this initial trimming, the plugin determines whether or not it should perform a special whitespace removal operation. How it removes the whitespace is based on the parameter `WhitespaceRemomvalStrategy`. The default value of this parameter is recommended. The other value (AllOccurrences) is only kept for legacy reasons.
-
-It determine whether or not to perform this operation based on the parameters `IgnoreWhitespaceInDialogue`, `IgnoreWhitespaceInNGUI` and `MinDialogueChars`:
+It determines whether or not to perform this operation based on the parameters `IgnoreWhitespaceInDialogue`, `IgnoreWhitespaceInNGUI` and `MinDialogueChars`:
  * `IgnoreWhitespaceInDialogue`: If the text is longer than `MinDialogueChars`, whitespace is removed.
  * `IgnoreWhitespaceInNGUI`: If the text comes from an NGUI component, whitespace is removed.
 
+It is worth mentioning if the same whitespace character is repeating in the untranslated text, it will not be removed because it is likely used for formatting.
+
+All leading and trailing whitespace before the actual text is preserved in the translation as well. But the plugin will still be capable of reading a translation from the translation file, even if the leading/trailing whitespace does not match up exactly.
+
 After the text has been translated by the configured service, `ForceSplitTextAfterCharacters` is used to determine if the plugin should force the result into multiple lines after a certain number of characters.
 
 The main reason that this type of handling can make or break a translation really comes down to whether or not whitespace is removed from the source text before sending it to the endpoint. Most endpoints (such as GoogleTranslate) consider text on multiple lines seperately, which can often result in terrible translation if an unnecessary newline is included.

+ 26 - 59
src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs

@@ -371,12 +371,12 @@ namespace XUnity.AutoTranslator.Plugin.Core
          TextureCache.LoadTranslationFiles();
       }
 
-      private void CreateTranslationJobFor( TranslationEndpointManager endpoint, object ui, UntranslatedTextInfo key, TranslationResult translationResult, ParserTranslationContext context )
+      private void CreateTranslationJobFor( TranslationEndpointManager endpoint, object ui, UntranslatedText key, TranslationResult translationResult, ParserTranslationContext context )
       {
          var added = endpoint.EnqueueTranslation( ui, key, translationResult, context );
          if( added && translationResult == null )
          {
-            SpamChecker.PerformChecks( key.GetUntranslatedText() );
+            SpamChecker.PerformChecks( key.TrimmedText );
          }
       }
 
@@ -429,14 +429,14 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      private void QueueNewUntranslatedForClipboard( UntranslatedTextInfo key )
+      private void QueueNewUntranslatedForClipboard( UntranslatedText key )
       {
          if( Settings.CopyToClipboard && Features.SupportsClipboard )
          {
-            if( !_textsToCopyToClipboard.Contains( key.UntranslatedText ) )
+            if( !_textsToCopyToClipboard.Contains( key.TrimmedText ) )
             {
-               _textsToCopyToClipboard.Add( key.UntranslatedText );
-               _textsToCopyToClipboardOrdered.Add( key.UntranslatedText );
+               _textsToCopyToClipboard.Add( key.TrimmedText );
+               _textsToCopyToClipboardOrdered.Add( key.TrimmedText );
 
                _clipboardUpdated = Time.realtimeSinceStartup;
             }
@@ -937,7 +937,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
          // Get the trimmed text
          string originalText = text;
 
-         text = ( text ?? ui.GetText() ).TrimIfConfigured();
+         text = text ?? ui.GetText();
 
          if( !string.IsNullOrEmpty( text ) && TextCache.IsTranslatable( text ) && ShouldTranslateTextComponent( ui, ignoreComponentState ) && !IsCurrentlySetting( info ) )
          {
@@ -945,7 +945,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
             //var textKey = new TranslationKey( ui, text, !ui.SupportsStabilization(), false );
             var isSpammer = ui.IsSpammingComponent();
-            var textKey = GetUntranslatedTextInfo( ui, text, isSpammer, false );
+            var textKey = GetCacheKey( ui, text, isSpammer, false );
 
             // if we already have translation loaded in our _translatios dictionary, simply load it and set text
             string translation;
@@ -988,16 +988,10 @@ namespace XUnity.AutoTranslator.Plugin.Core
       {
          var result = new TranslationResult();
 
-         if( context == null )
-         {
-            // Get the trimmed text
-            text = text.TrimIfConfigured();
-         }
-
          // Ensure that we actually want to translate this text and its owning UI element.
          if( !string.IsNullOrEmpty( text ) && endpoint.IsTranslatable( text ) && IsBelowMaxLength( text ) )
          {
-            var textKey = GetUntranslatedTextInfo( null, text, false, context != null );
+            var textKey = GetCacheKey( null, text, false, context != null );
 
             // if we already have translation loaded in our _translatios dictionary, simply load it and set text
             string translation;
@@ -1068,7 +1062,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
             if( !string.IsNullOrEmpty( untranslatedTextPart ) && endpoint.IsTranslatable( untranslatedTextPart ) && IsBelowMaxLength( untranslatedTextPart ) )
             {
                string partTranslation;
-               if( endpoint.TryGetTranslation( untranslatedTextPart, out partTranslation ) )
+               if( endpoint.TryGetTranslation( new UntranslatedText( untranslatedTextPart, false, false ), out partTranslation ) )
                {
                   translations.Add( variableName, partTranslation );
                }
@@ -1106,11 +1100,6 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
          // make sure text exists
          var originalText = text;
-         if( context == null )
-         {
-            // Get the trimmed text
-            text = text.TrimIfConfigured();
-         }
 
          // Ensure that we actually want to translate this text and its owning UI element. 
          if( !string.IsNullOrEmpty( text ) && TextCache.IsTranslatable( text ) && ShouldTranslateTextComponent( ui, ignoreComponentState ) && !IsCurrentlySetting( info ) )
@@ -1119,8 +1108,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
             var isSpammer = ui.IsSpammingComponent();
             if( isSpammer && !IsBelowMaxLength( text ) ) return null; // avoid templating long strings every frame for IMGUI, important!
 
-            //var textKey = new TranslationKey( ui, text, !supportsStabilization, context != null );
-            var textKey = GetUntranslatedTextInfo( ui, text, isSpammer, context != null );
+            var textKey = GetCacheKey( ui, text, isSpammer, context != null );
 
             // if we already have translation loaded in our _translatios dictionary, simply load it and set text
             string translation;
@@ -1209,11 +1197,10 @@ namespace XUnity.AutoTranslator.Plugin.Core
                               _ongoingOperations.Remove( ui );
 
                               originalText = stabilizedText;
-                              stabilizedText = stabilizedText.TrimIfConfigured();
 
                               if( !string.IsNullOrEmpty( stabilizedText ) && TextCache.IsTranslatable( stabilizedText ) )
                               {
-                                 var stabilizedTextKey = GetUntranslatedTextInfo( ui, stabilizedText, false, false );
+                                 var stabilizedTextKey = GetCacheKey( ui, stabilizedText, false, false );
 
                                  QueueNewUntranslatedForClipboard( stabilizedTextKey );
 
@@ -1322,7 +1309,6 @@ namespace XUnity.AutoTranslator.Plugin.Core
                               }
                            } ) );
                   }
-
                }
             }
          }
@@ -1342,7 +1328,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
             if( !string.IsNullOrEmpty( untranslatedTextPart ) && TextCache.IsTranslatable( untranslatedTextPart ) && IsBelowMaxLength( untranslatedTextPart ) )
             {
                string partTranslation;
-               if( TextCache.TryGetTranslation( untranslatedTextPart, false, out partTranslation ) )
+               if( TextCache.TryGetTranslation( new UntranslatedText( untranslatedTextPart, false, false ), false, out partTranslation ) )
                {
                   translations.Add( variableName, partTranslation );
                }
@@ -1409,9 +1395,9 @@ namespace XUnity.AutoTranslator.Plugin.Core
       /// for global text, where the component cannot tell us if the text
       /// has changed itself.
       /// </summary>
-      private IEnumerator WaitForTextStablization( UntranslatedTextInfo textKey, float delay, Action onTextStabilized, Action onFailed = null )
+      private IEnumerator WaitForTextStablization( UntranslatedText textKey, float delay, Action onTextStabilized, Action onFailed = null )
       {
-         var text = textKey.GetUntranslatedText();
+         var text = textKey.TrimmedText;
 
          if( !_immediatelyTranslating.Contains( text ) )
          {
@@ -1679,7 +1665,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
       {
          if( job.SaveResultGlobally )
          {
-            TextCache.AddTranslationToCache( job.Key, job.TranslatedText );
+            TextCache.AddTranslationToCache( job.Key.TranslatableText, job.TranslatedText );
          }
 
          job.Endpoint.AddTranslationToCache( job.Key, job.TranslatedText );
@@ -1702,8 +1688,8 @@ namespace XUnity.AutoTranslator.Plugin.Core
             // update the original text, but only if it has not been chaanged already for some reason (could be other translator plugin or game itself)
             try
             {
-               var text = component.GetText().TrimIfConfigured();
-               if( text == job.Key.TrimmedOriginalText )
+               var text = component.GetText();
+               if( text == job.Key.OriginalText )
                {
                   var info = component.GetOrCreateTextTranslationInfo();
                   if( !string.IsNullOrEmpty( job.TranslatedText ) )
@@ -1726,7 +1712,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
             {
                try
                {
-                  var text = context.Component.GetText().TrimIfConfigured();
+                  var text = context.Component.GetText();
                   var result = context.Result;
                   Dictionary<string, string> translations = new Dictionary<string, string>();
 
@@ -1790,31 +1776,12 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      private static string GetCacheKey( object ui, string trimmedOriginalText, bool neverRemoveWhitespace )
-      {
-         if( !neverRemoveWhitespace
-            && ( ( Settings.IgnoreWhitespaceInDialogue && trimmedOriginalText.Length > Settings.MinDialogueChars ) || ( Settings.IgnoreWhitespaceInNGUI && ui.IsNGUI() ) ) )
-         {
-            return trimmedOriginalText.RemoveWhitespaceAndNewlines();
-         }
-         else
-         {
-            return trimmedOriginalText;
-         }
-      }
-
-      private static UntranslatedTextInfo GetUntranslatedTextInfo( object ui, string trimmedOriginalText, bool templatizeByNumbers, bool neverRemoveWhitespace )
+      private static UntranslatedText GetCacheKey( object ui, string originalText, bool templatizeByNumbers, bool neverRemoveWhitespace )
       {
-         var untranslatedTextKey = GetCacheKey( ui, trimmedOriginalText, neverRemoveWhitespace );
-         var untranslatedText = untranslatedTextKey.TrimLeadingNewlines( out int count );
-
-         TemplatedString templatedText = null;
-         if( templatizeByNumbers )
-         {
-            templatedText = untranslatedText.TemplatizeByNumbers();
-         }
+         var removeInternalWhitespace = !neverRemoveWhitespace
+            && ( ( Settings.IgnoreWhitespaceInDialogue && originalText.Length > Settings.MinDialogueChars ) || ( Settings.IgnoreWhitespaceInNGUI && ui.IsNGUI() ) );
 
-         return new UntranslatedTextInfo( trimmedOriginalText, untranslatedTextKey, untranslatedText, count, templatedText );
+         return new UntranslatedText( originalText, templatizeByNumbers, removeInternalWhitespace );
       }
 
       private void ReloadTranslations()
@@ -1832,10 +1799,10 @@ namespace XUnity.AutoTranslator.Plugin.Core
                   if( component.gameObject?.activeSelf ?? false )
                   {
                      var tti = kvp.Value as TextTranslationInfo;
-                     var trimmedOriginalText = tti.OriginalText.TrimIfConfigured();
-                     if( tti != null && !string.IsNullOrEmpty( trimmedOriginalText ) )
+                     var originalText = tti.OriginalText;
+                     if( tti != null && !string.IsNullOrEmpty( originalText ) )
                      {
-                        var key = GetCacheKey( kvp.Key, trimmedOriginalText, false );
+                        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

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

@@ -73,7 +73,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static bool EnableMigrations;
       public static string MigrationsTag;
       public static bool EnableBatching;
-      public static bool TrimAllText;
       public static bool EnableUIResizing;
       public static bool UseStaticTranslations;
       public static string OverrideFont;
@@ -132,7 +131,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
             MaxClipboardCopyCharacters = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "MaxClipboardCopyCharacters", 1000 );
             EnableUIResizing = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "EnableUIResizing", true );
             EnableBatching = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "EnableBatching", true );
-            TrimAllText = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "TrimAllText", ClrTypes.AdvEngine == null );
             UseStaticTranslations = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "UseStaticTranslations", true );
             OverrideFont = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "OverrideFont", string.Empty );
             ResizeUILineSpacingScale = PluginEnvironment.Current.Preferences.GetOrDefault<float?>( "Behaviour", "ResizeUILineSpacingScale", null );

+ 47 - 39
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationEndpointManager.cs

@@ -13,8 +13,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
    internal class TranslationEndpointManager
    {
       private Dictionary<string, byte> _failedTranslations;
-      private Dictionary<string, TranslationJob> _unstartedJobs;
-      private Dictionary<string, TranslationJob> _ongoingJobs;
+      private Dictionary<UntranslatedText, TranslationJob> _unstartedJobs;
+      private Dictionary<UntranslatedText, TranslationJob> _ongoingJobs;
 
       private int _ongoingTranslations;
 
@@ -29,8 +29,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          _ongoingTranslations = 0;
 
          _failedTranslations = new Dictionary<string, byte>();
-         _unstartedJobs = new Dictionary<string, TranslationJob>();
-         _ongoingJobs = new Dictionary<string, TranslationJob>();
+         _unstartedJobs = new Dictionary<UntranslatedText, TranslationJob>();
+         _ongoingJobs = new Dictionary<UntranslatedText, TranslationJob>();
 
          _translations = new Dictionary<string, string>();
          _reverseTranslations = new Dictionary<string, string>();
@@ -61,14 +61,28 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 
       public bool HasFailedDueToConsecutiveErrors => ConsecutiveErrors >= Settings.MaxErrors;
 
-      public bool TryGetTranslation( UntranslatedTextInfo key, out string value )
+      public bool TryGetTranslation( UntranslatedText key, out string value )
       {
-         return TryGetTranslation( key.GetCacheKey(), out value );
-      }
+         var unmodifiedKey = key.TranslatableText;
+         var result = _translations.TryGetValue( unmodifiedKey, out value );
+         if( result )
+         {
+            return result;
+         }
 
-      public bool TryGetTranslation( string key, out string value )
-      {
-         return _translations.TryGetValue( key, out value );
+         var modifiedKey = key.TrimmedText;
+         result = _translations.TryGetValue( modifiedKey, out value );
+         if( result )
+         {
+            // add an unmodifiedKey to the dictionary
+            var unmodifiedValue = key.LeadingWhitespace + value + key.TrailingWhitespace;
+            AddTranslationToCache( unmodifiedKey, unmodifiedValue );
+
+            value = unmodifiedValue;
+            return result;
+         }
+
+         return result;
       }
 
       private void AddTranslation( string key, string value )
@@ -82,7 +96,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          // FIXME: Implement
       }
 
-      public void AddTranslationToCache( UntranslatedTextInfo key, string value )
+      public void AddTranslationToCache( UntranslatedText key, string value )
       {
          // UNRELEASED: Not included in current release
          //AddTranslationToCache( key.GetDictionaryLookupKey(), value );
@@ -128,7 +142,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
                _unstartedJobs.Remove( key );
                Manager.UnstartedTranslations--;
 
-               var untranslatedText = job.Key.GetUntranslatedText();
+               var untranslatedText = job.Key.TrimmedText;
                if( CanTranslate( untranslatedText ) )
                {
                   jobs.Add( job );
@@ -185,7 +199,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
             _unstartedJobs.Remove( key );
             Manager.UnstartedTranslations--;
 
-            var untranslatedText = job.Key.GetUntranslatedText();
+            var untranslatedText = job.Key.TrimmedText;
             if( CanTranslate( untranslatedText ) )
             {
                _ongoingJobs[ key ] = job;
@@ -233,9 +247,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
                job.TranslatedText = PostProcessTranslation( job.Key, translatedText );
                job.State = TranslationJobState.Succeeded;
 
-               RemoveOngoingTranslation( job.Key.GetUntranslatedText() );
+               RemoveOngoingTranslation( job.Key );
 
-               XuaLogger.Current.Info( $"Completed: '{job.Key.GetUntranslatedText()}' => '{job.TranslatedText}'" );
+               XuaLogger.Current.Info( $"Completed: '{job.Key.TrimmedText}' => '{job.TranslatedText}'" );
 
                Manager.InvokeJobCompleted( job );
             }
@@ -252,7 +266,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
             {
                var job = jobs[ i ];
 
-               var key = job.Key.GetUntranslatedText();
+               var key = job.Key;
                AddUnstartedJob( key, job );
                RemoveOngoingTranslation( key );
             }
@@ -270,19 +284,20 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          job.TranslatedText = PostProcessTranslation( job.Key, translatedText );
          job.State = TranslationJobState.Succeeded;
 
-         RemoveOngoingTranslation( job.Key.GetUntranslatedText() );
+         RemoveOngoingTranslation( job.Key );
 
-         XuaLogger.Current.Info( $"Completed: '{job.Key.GetUntranslatedText()}' => '{job.TranslatedText}'" );
+         XuaLogger.Current.Info( $"Completed: '{job.Key.TrimmedText}' => '{job.TranslatedText}'" );
 
          Manager.InvokeJobCompleted( job );
       }
 
-      private string PostProcessTranslation( UntranslatedTextInfo key, string translatedText )
+      private string PostProcessTranslation( UntranslatedText key, string translatedText )
       {
          var hasTranslation = !string.IsNullOrEmpty( translatedText );
          if( hasTranslation )
          {
             translatedText = key.RepairTemplate( translatedText );
+            translatedText = key.LeadingWhitespace + translatedText + key.TrailingWhitespace;
 
             if( Settings.Language == Settings.Romaji && Settings.RomajiPostProcessing != TextPostProcessing.None )
             {
@@ -297,11 +312,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
             {
                translatedText = translatedText.SplitToLines( Settings.ForceSplitTextAfterCharacters, '\n', ' ', ' ' );
             }
-
-            if( key.PrependedNewlines > 0 && key.TemplatedText == null )
-            {
-               translatedText = new string( '\n', key.PrependedNewlines ) + translatedText;
-            }
          }
 
          return translatedText;
@@ -322,13 +332,13 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          {
             foreach( var job in jobs )
             {
-               var untranslatedText = job.Key.GetUntranslatedText();
+               var key = job.Key;
                job.State = TranslationJobState.Failed;
                job.ErrorMessage = error;
 
-               RemoveOngoingTranslation( untranslatedText );
+               RemoveOngoingTranslation( key );
 
-               RegisterTranslationFailureFor( untranslatedText );
+               RegisterTranslationFailureFor( key.TrimmedText );
 
                Manager.InvokeJobFailed( job );
             }
@@ -345,7 +355,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
             {
                var job = jobs[ i ];
 
-               var key = job.Key.GetUntranslatedText();
+               var key = job.Key;
                AddUnstartedJob( key, job );
                RemoveOngoingTranslation( key );
             }
@@ -375,11 +385,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          XuaLogger.Current.Info( "Re-enabled batching." );
       }
 
-      public bool EnqueueTranslation( object ui, UntranslatedTextInfo key, TranslationResult translationResult, ParserTranslationContext context )
+      public bool EnqueueTranslation( object ui, UntranslatedText key, TranslationResult translationResult, ParserTranslationContext context )
       {
-         var lookupKey = key.GetUntranslatedText();
-
-         var added = AssociateWithExistingJobIfPossible( ui, lookupKey, translationResult, context );
+         var added = AssociateWithExistingJobIfPossible( ui, key, translationResult, context );
          if( added )
          {
             return false;
@@ -395,7 +403,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
                var endpoint = endpoints[ i ];
                if( endpoint == this ) continue;
 
-               added = endpoint.AssociateWithExistingJobIfPossible( ui, lookupKey, translationResult, context );
+               added = endpoint.AssociateWithExistingJobIfPossible( ui, key, translationResult, context );
                if( added )
                {
                   return false;
@@ -403,16 +411,16 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
             }
          }
 
-         XuaLogger.Current.Debug( "Queued: '" + lookupKey + "'" );
+         XuaLogger.Current.Debug( "Queued: '" + key.TrimmedText + "'" );
 
          var saveResultGlobally = checkOtherEndpoints;
          var newJob = new TranslationJob( this, key, saveResultGlobally );
          newJob.Associate( ui, translationResult, context );
 
-         return AddUnstartedJob( lookupKey, newJob );
+         return AddUnstartedJob( key, newJob );
       }
 
-      private bool AssociateWithExistingJobIfPossible( object ui, string key, TranslationResult translationResult, ParserTranslationContext context )
+      private bool AssociateWithExistingJobIfPossible( object ui, UntranslatedText key, TranslationResult translationResult, ParserTranslationContext context )
       {
          if( _unstartedJobs.TryGetValue( key, out TranslationJob unstartedJob ) )
          {
@@ -429,7 +437,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          return false;
       }
 
-      private bool AddUnstartedJob( string key, TranslationJob job )
+      private bool AddUnstartedJob( UntranslatedText key, TranslationJob job )
       {
          if( !_unstartedJobs.ContainsKey( key ) )
          {
@@ -448,7 +456,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          return false;
       }
 
-      private void RemoveOngoingTranslation( string key )
+      private void RemoveOngoingTranslation( UntranslatedText key )
       {
          if( _ongoingJobs.Remove( key ) )
          {
@@ -468,7 +476,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 
          foreach( var job in unstartedJobs )
          {
-            XuaLogger.Current.Warn( $"Dequeued: '{job.Key}'" );
+            XuaLogger.Current.Warn( $"Dequeued: '{job.Key.TrimmedText}'" );
             job.Value.State = TranslationJobState.Failed;
             job.Value.ErrorMessage = "Translation failed because all jobs on endpoint was cleared.";
 

+ 18 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringBuilderExtensions.cs

@@ -20,5 +20,23 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          var lastChar = builder[ builder.Length - 1 ];
          return char.IsWhiteSpace( lastChar ) || lastChar == '\n';
       }
+
+      internal static StringBuilder Reverse( this StringBuilder text )
+      {
+         if( text.Length > 1 )
+         {
+            int pivotPos = text.Length / 2;
+            for( int i = 0; i < pivotPos; i++ )
+            {
+               int iRight = text.Length - ( i + 1 );
+               char rightChar = text[ i ];
+               char leftChar = text[ iRight ];
+               text[ i ] = leftChar;
+               text[ iRight ] = rightChar;
+            }
+         }
+
+         return text;
+      }
    }
 }

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

@@ -201,90 +201,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          return sb.ToString();
       }
 
-      public static string TrimIfConfigured( this string text )
-      {
-         if( text == null ) return text;
-
-         if( Settings.TrimAllText )
-         {
-            return text.Trim( Spaces ).TrimEnd( WhitespacesAndNewlines );
-         }
-         return text;
-      }
-
-      public static string TrimLeadingNewlines( this string text, out int newlineCount )
-      {
-         int i = 0;
-         int count = 0;
-         while( i < text.Length && char.IsWhiteSpace( text[ i ] ) )
-         {
-            if( i < text.Length && text[ i ] == '\n' )
-            {
-               count++;
-            }
-
-            i++;
-         }
-         newlineCount = count;
-
-         text = text.Substring( i, text.Length - i );
-         return text;
-      }
-
-      public static string RemoveWhitespaceAndNewlines( this string text )
-      {
-         var builder = new StringBuilder( text.Length );
-         if( Settings.WhitespaceRemovalStrategy == WhitespaceHandlingStrategy.AllOccurrences )
-         {
-            for( int i = 0; i < text.Length; i++ )
-            {
-               var c = text[ i ];
-               switch( c )
-               {
-                  case '\n':
-                  case '\r':
-                  case ' ':
-                  case ' ':
-                     break;
-                  default:
-                     builder.Append( c );
-                     break;
-               }
-            }
-         }
-         else // if( Settings.WhitespaceHandlingStrategy == WhitespaceHandlingStrategy.TrimPerNewline )
-         {
-            var lines = text.Split( NewlinesCharacters, StringSplitOptions.None );
-            var lastLine = lines.Length - 1;
-            bool hasAddedText = false;
-            for( int i = 0; i < lines.Length; i++ )
-            {
-               var line = lines[ i ].Trim( WhitespacesAndNewlines );
-               if( line != string.Empty )
-               {
-                  for( int j = 0; j < line.Length; j++ )
-                  {
-                     hasAddedText = true;
-
-                     var c = line[ j ];
-                     builder.Append( c );
-                  }
-               }
-               else if( !hasAddedText )
-               {
-                  builder.Append( "\n" );
-               }
-
-               // do we need to add a space when merging lines?
-               if( Settings.UsesWhitespaceBetweenWords && i != lastLine ) // en, ru, ko?
-               {
-                  builder.Append( ' ' );
-               }
-            }
-         }
-         return builder.ToString();
-      }
-
       public static bool StartsWithStrict( this string str, string prefix )
       {
          var len = Math.Min( str.Length, prefix.Length );

+ 47 - 25
src/XUnity.AutoTranslator.Plugin.Core/TextTranslationCache.cs

@@ -85,7 +85,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
                   string[] kvp = translation.Split( splitter, StringSplitOptions.None );
                   if( kvp.Length == 2 )
                   {
-                     string key = TextHelper.Decode( kvp[ 0 ].TrimIfConfigured() );
+                     string key = TextHelper.Decode( kvp[ 0 ] );
                      string value = TextHelper.Decode( kvp[ 1 ] );
 
                      if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) && IsTranslatable( key ) )
@@ -106,6 +106,14 @@ namespace XUnity.AutoTranslator.Plugin.Core
                         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.TrimmedText != key )
+                           {
+                              AddTranslation( ukey.TrimmedText, uvalue.TrimmedText );
+                           }
                         }
                         break;
                      }
@@ -133,8 +141,8 @@ namespace XUnity.AutoTranslator.Plugin.Core
                   string[] kvp = translation.Split( splitter, StringSplitOptions.None );
                   if( kvp.Length >= 2 )
                   {
-                     string key = TextHelper.Decode( kvp[ 0 ].TrimIfConfigured() );
-                     string value = TextHelper.Decode( kvp[ 1 ].TrimIfConfigured() );
+                     string key = TextHelper.Decode( kvp[ 0 ] );
+                     string value = TextHelper.Decode( kvp[ 1 ] );
 
                      if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) )
                      {
@@ -207,30 +215,38 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      internal void AddTranslationToCache( UntranslatedTextInfo key, string value )
-      {
-         AddTranslationToCache( key.GetCacheKey(), value );
-      }
-
-      internal void AddTranslationToCache( string key, string value )
+      internal void AddTranslationToCache( string key, string value, bool persistToDisk = true )
       {
          if( !HasTranslated( key ) )
          {
             AddTranslation( key, value );
-            QueueNewTranslationForDisk( key, value );
+            if( persistToDisk )
+            {
+               QueueNewTranslationForDisk( key, value );
+            }
          }
       }
 
-      internal bool TryGetTranslation( UntranslatedTextInfo key, bool allowRegex, out string value )
+      internal bool TryGetTranslation( UntranslatedText key, bool allowRegex, out string value )
       {
-         return TryGetTranslation( key.GetCacheKey(), allowRegex, out value );
-      }
+         var unmodifiedKey = key.TranslatableText;
+         var result = _translations.TryGetValue( unmodifiedKey, out value );
+         if( result )
+         {
+            return result;
+         }
 
-      internal bool TryGetTranslation( string key, bool allowRegex, out string value )
-      {
-         var result = _translations.TryGetValue( key, out value );
+         var modifiedKey = key.TrimmedText;
+         result = _translations.TryGetValue( modifiedKey, out value );
          if( result )
          {
+            // add an unmodifiedKey to the dictionary
+            var unmodifiedValue = key.LeadingWhitespace + value + key.TrailingWhitespace;
+
+            XuaLogger.Current.Info( $"Whitespace difference: '{key.TrimmedText}' => '{value}'" );
+            AddTranslationToCache( unmodifiedKey, unmodifiedValue, false );
+
+            value = unmodifiedValue;
             return result;
          }
 
@@ -242,18 +258,17 @@ namespace XUnity.AutoTranslator.Plugin.Core
             for( int i = 0; i < len; i++ )
             {
                var regex = _defaultRegexes[ i ];
-               var match = regex.CompiledRegex.Match( key );
+               var match = regex.CompiledRegex.Match( unmodifiedKey );
                if( !match.Success ) continue;
 
-               var translation = regex.CompiledRegex.Replace( key, regex.Translation );
-               
-               //AddTranslation( key, translation );
-               AddTranslationToCache( key, translation ); // Would store it to file... Should we????
+               var translation = regex.CompiledRegex.Replace( unmodifiedKey, regex.Translation );
+
+               AddTranslationToCache( unmodifiedKey, translation, false ); // Would store it to file... Should we????
 
                value = translation;
                found = true;
 
-               XuaLogger.Current.Info( $"Regex translation: '{key}' => '{value}'" );
+               XuaLogger.Current.Info( $"Regex lookup: '{key.TrimmedText}' => '{value}'" );
                break;
             }
 
@@ -265,10 +280,17 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
          if( _staticTranslations.Count > 0 )
          {
-            if( _staticTranslations.TryGetValue( key, out value ) )
+            if( _staticTranslations.TryGetValue( unmodifiedKey, out value ) )
             {
-               QueueNewTranslationForDisk( key, value );
-               AddTranslation( key, value );
+               AddTranslationToCache( unmodifiedKey, value );
+               return true;
+            }
+            else if( _staticTranslations.TryGetValue( modifiedKey, out value ) )
+            {
+               var unmodifiedValue = key.LeadingWhitespace + value + key.TrailingWhitespace;
+               AddTranslationToCache( unmodifiedKey, unmodifiedValue );
+
+               value = unmodifiedValue;
                return true;
             }
          }

+ 2 - 2
src/XUnity.AutoTranslator.Plugin.Core/TranslationJob.cs

@@ -11,7 +11,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
 {
    internal class TranslationJob
    {
-      public TranslationJob( TranslationEndpointManager endpoint, UntranslatedTextInfo key, bool saveResult )
+      public TranslationJob( TranslationEndpointManager endpoint, UntranslatedText key, bool saveResult )
       {
          Endpoint = endpoint;
          Key = key;
@@ -32,7 +32,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
       public HashSet<TranslationResult> TranslationResults { get; private set; }
 
-      public UntranslatedTextInfo Key { get; private set; }
+      public UntranslatedText Key { get; private set; }
 
       public string TranslatedText { get; set; }
 

+ 187 - 0
src/XUnity.AutoTranslator.Plugin.Core/UntranslatedText.cs

@@ -0,0 +1,187 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+
+namespace XUnity.AutoTranslator.Plugin.Core
+{
+   class UntranslatedText
+   {
+      public UntranslatedText( string text, bool templatizeByNumbers, bool removeInternalWhitespace )
+      {
+         OriginalText = text;
+
+         if( templatizeByNumbers )
+         {
+            TemplatedText = text.TemplatizeByNumbers();
+            if( TemplatedText != null )
+            {
+               text = TemplatedText.Template;
+            }
+         }
+
+         int i = 0;
+         int firstNonWhitespace = 0;
+         int lastNonWhitespace = 0;
+
+         StringBuilder leadingBuilder = null;
+         while( i < text.Length && char.IsWhiteSpace( text[ i ] ) )
+         {
+            if( leadingBuilder == null ) leadingBuilder = new StringBuilder( 64 );
+
+            leadingBuilder.Append( text[ i ] );
+            i++;
+         }
+         firstNonWhitespace = i;
+
+         if( firstNonWhitespace != 0 )
+         {
+            LeadingWhitespace = leadingBuilder?.ToString();
+         }
+
+         i = text.Length - 1;
+         StringBuilder trailingBuilder = leadingBuilder;
+         if( trailingBuilder != null ) trailingBuilder.Length = 0;
+
+         while( i > -1 && char.IsWhiteSpace( text[ i ] ) )
+         {
+            if( trailingBuilder == null ) trailingBuilder = new StringBuilder( 64 );
+
+            trailingBuilder.Append( text[ i ] );
+            i--;
+         }
+         lastNonWhitespace = i;
+
+         if( lastNonWhitespace != text.Length - 1 )
+         {
+            TrailingWhitespace = trailingBuilder?.Reverse().ToString();
+         }
+
+         // trim internals of 'text'
+         if( removeInternalWhitespace && Settings.WhitespaceRemovalStrategy == WhitespaceHandlingStrategy.TrimPerNewline )
+         {
+            StringBuilder builder = trailingBuilder;
+            if( builder != null ) builder.Length = 0;
+            else if( builder == null ) builder = new StringBuilder( 64 );
+
+            if( LeadingWhitespace != null )
+            {
+               builder.Append( LeadingWhitespace );
+            }
+
+            char currentWhitespaceChar = default( char );
+            bool addedCurrentWhitespace = false;
+            for( i = firstNonWhitespace; i <= lastNonWhitespace; i++ )
+            {
+               var c = text[ i ];
+               if( !char.IsWhiteSpace( c ) )
+               {
+                  builder.Append( c );
+
+                  currentWhitespaceChar = default( char );
+               }
+               else
+               {
+                  // keep repeating whitespace
+                  if( c == currentWhitespaceChar )
+                  {
+                     if( !addedCurrentWhitespace )
+                     {
+                        builder.Append( currentWhitespaceChar );
+                        addedCurrentWhitespace = true;
+                     }
+
+                     builder.Append( c );
+                  }
+                  else
+                  {
+                     addedCurrentWhitespace = false;
+                     currentWhitespaceChar = c;
+                  }
+               }
+
+               if( Settings.UsesWhitespaceBetweenWords && ( c == '\n' || c == '\r' ) )
+               {
+                  if( builder.Length > 0 && builder[ builder.Length - 1 ] != ' ' )
+                  {
+                     builder.Append( ' ' );
+                  }
+
+                  var nextI = i + 1;
+                  if( nextI < lastNonWhitespace && text[ nextI ] == '\n' )
+                  {
+                     i++;
+                  }
+               }
+            }
+
+            if( TrailingWhitespace != null )
+            {
+               builder.Append( TrailingWhitespace );
+            }
+
+            TranslatableText = builder.ToString();
+         }
+         else
+         {
+            TranslatableText = text;
+         }
+
+         int leadingWhitespaceCount = LeadingWhitespace != null ? LeadingWhitespace.Length : 0;
+         int trailingWhitespaceCount = TrailingWhitespace != null ? TrailingWhitespace.Length : 0;
+
+         if( leadingWhitespaceCount > 0 || trailingWhitespaceCount > 0 )
+         {
+            TrimmedText = TranslatableText.Substring( leadingWhitespaceCount, TranslatableText.Length - trailingWhitespaceCount - leadingWhitespaceCount );
+         }
+         else
+         {
+            TrimmedText = TranslatableText;
+         }
+      }
+
+      public string LeadingWhitespace { get; }
+
+      public string TrailingWhitespace { get; }
+
+      public string TrimmedText { get; }
+
+      public string TranslatableText { get; }
+
+      public string OriginalText { get; }
+
+      public TemplatedString TemplatedText { get; }
+
+      public string Untemplate( string text )
+      {
+         if( TemplatedText != null )
+         {
+            return TemplatedText.Untemplate( text );
+         }
+
+         return text;
+      }
+
+      public string RepairTemplate( string text )
+      {
+         if( TemplatedText != null )
+         {
+            return TemplatedText.RepairTemplate( text );
+         }
+
+         return text;
+      }
+
+      public override bool Equals( object obj )
+      {
+         return obj is UntranslatedText ut && TranslatableText == ut.TranslatableText;
+      }
+
+      public override int GetHashCode()
+      {
+         return TranslatableText.GetHashCode();
+      }
+   }
+}

+ 0 - 73
src/XUnity.AutoTranslator.Plugin.Core/UntranslatedTextInfo.cs

@@ -1,73 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using XUnity.AutoTranslator.Plugin.Core.Configuration;
-using XUnity.AutoTranslator.Plugin.Core.Extensions;
-
-namespace XUnity.AutoTranslator.Plugin.Core
-{
-   internal class UntranslatedTextInfo
-   {
-      public UntranslatedTextInfo( string trimmedOriginalText, string untranslatedTextKey, string untranslatedText, int prependedNewlineCount, TemplatedString templatedText )
-      {
-         TrimmedOriginalText = trimmedOriginalText;
-         UntranslatedText = untranslatedText;
-         UntranslatedTextKey = untranslatedTextKey;
-         PrependedNewlines = prependedNewlineCount;
-         TemplatedText = templatedText;
-      }
-
-      public int PrependedNewlines { get; set; }
-
-      public TemplatedString TemplatedText { get; }
-
-      public string UntranslatedTextKey { get; }
-
-      public string UntranslatedText { get; }
-
-      public string TrimmedOriginalText { get; }
-
-      public string GetUntranslatedText()
-      {
-         // Should NOT contain prepended newlines (problem with template text?)
-
-         if( TemplatedText != null )
-         {
-            return TemplatedText.Template;
-         }
-         return UntranslatedText;
-      }
-
-      public string GetCacheKey()
-      {
-         // Should contain prepended newlines (problem with template text?)
-
-         if( TemplatedText != null )
-         {
-            return TemplatedText.Template;
-         }
-         return UntranslatedTextKey;
-      }
-
-      public string Untemplate( string text )
-      {
-         if( TemplatedText != null )
-         {
-            return TemplatedText.Untemplate( text );
-         }
-
-         return text;
-      }
-
-      public string RepairTemplate( string text )
-      {
-         if( TemplatedText != null )
-         {
-            return TemplatedText.RepairTemplate( text );
-         }
-
-         return text;
-      }
-   }
-}

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/WhitespaceHandlingStrategy.cs

@@ -8,6 +8,6 @@ namespace XUnity.AutoTranslator.Plugin.Core
    internal enum WhitespaceHandlingStrategy
    {
       TrimPerNewline,
-      AllOccurrences
+      None
    }
 }

+ 0 - 25
test/XUnity.AutoTranslator.Plugin.Core.Tests/StringExtensionTests.cs

@@ -1,25 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Xunit;
-using XUnity.AutoTranslator.Plugin.Core.Extensions;
-
-namespace XUnity.AutoTranslator.Plugin.Core.Tests
-{
-   public class StringExtensionTests
-   {
-      [Theory( DisplayName = "Can_Trim_Leading_Newlines" )]
-      [InlineData( "\r\n \r\nHello", "Hello", 2 )]
-      [InlineData( "\r\n \r\nHello\n", "Hello\n", 2 )]
-      [InlineData( "\r\r\r\r\n \n Hello", "Hello", 2 )]
-      public void Can_Trim_Leading_Newlines( string input, string expectedOutput, int expectedNewlineCount )
-      {
-         var output = input.TrimLeadingNewlines( out int newlineCount );
-
-         Assert.Equal( output, expectedOutput );
-         Assert.Equal( expectedNewlineCount, newlineCount );
-      }
-   }
-}

+ 61 - 0
test/XUnity.AutoTranslator.Plugin.Core.Tests/UntranslatedTextTests.cs

@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Tests
+{
+   public class UntranslatedTextTests
+   {
+      [Theory( DisplayName = "Can_Trim_Surrounding_Whitespace" )]
+      [InlineData( "\r\n \r\nHello", "Hello", "\r\n \r\n", null )]
+      [InlineData( "\r\n \r\nHello\n", "Hello", "\r\n \r\n", "\n" )]
+      [InlineData( "\r\r\r\r\n \n Hello  \r\n", "Hello", "\r\r\r\r\n \n ", "  \r\n" )]
+      public void Can_Trim_Surrounding_Whitespace( string input, string expectedTrimmedText, string expectedLeadingWhitespace, string expectedTrailingWhitespace )
+      {
+         var untranslatedText = new UntranslatedText( input, false, false );
+
+         Assert.Equal( input, untranslatedText.TranslatableText );
+         Assert.Equal( expectedTrimmedText, untranslatedText.TrimmedText );
+         Assert.Equal( expectedLeadingWhitespace, untranslatedText.LeadingWhitespace );
+         Assert.Equal( expectedTrailingWhitespace, untranslatedText.TrailingWhitespace );
+      }
+
+      [Theory( DisplayName = "Can_Trim_Internal_Whitespace" )]
+      [InlineData( "Hel lo", "Hello" )]
+      [InlineData( "Hel\r\n lo", "Hello" )]
+      public void Can_Trim_Internal_Whitespace( string input, string expectedTrimmedText )
+      {
+         var untranslatedText = new UntranslatedText( input, false, true );
+         
+         Assert.Equal( expectedTrimmedText, untranslatedText.TrimmedText );
+      }
+
+      [Theory( DisplayName = "Can_Trim_Internal_And_Surrounding_Whitespace" )]
+      [InlineData( "\r\n \r\nHe llo", "Hello", "\r\n \r\n", null )]
+      [InlineData( "\r\n \r\nHel\r\nlo\n", "Hello", "\r\n \r\n", "\n" )]
+      [InlineData( "\r\r\r\r\n \n Hell\no  \r\n", "Hello", "\r\r\r\r\n \n ", "  \r\n" )]
+      public void Can_Trim_Internal_And_Surrounding_Whitespace( string input, string expectedTrimmedText, string expectedLeadingWhitespace, string expectedTrailingWhitespace )
+      {
+         var untranslatedText = new UntranslatedText( input, false, true );
+
+         Assert.Equal( expectedTrimmedText, untranslatedText.TrimmedText );
+         Assert.Equal( expectedLeadingWhitespace, untranslatedText.LeadingWhitespace );
+         Assert.Equal( expectedTrailingWhitespace, untranslatedText.TrailingWhitespace );
+      }
+
+      [Theory( DisplayName = "Can_Trim_Internal_And_Surrounding_Whitespace_And_Template" )]
+      [InlineData( "\r\n \r\nFPS: 60.53", "FPS:{{A}}", "\r\n \r\n", null )]
+      public void Can_Trim_Internal_And_Surrounding_Whitespace_And_Template( string input, string expectedTrimmedText, string expectedLeadingWhitespace, string expectedTrailingWhitespace )
+      {
+         var untranslatedText = new UntranslatedText( input, true, true );
+
+         Assert.Equal( expectedTrimmedText, untranslatedText.TrimmedText );
+         Assert.Equal( expectedLeadingWhitespace, untranslatedText.LeadingWhitespace );
+         Assert.Equal( expectedTrailingWhitespace, untranslatedText.TrailingWhitespace );
+      }
+   }
+}