2 Revīzijas 90047e60b0 ... eed912b200

Autors SHA1 Ziņojums Datums
  randoman eed912b200 Version 3.3.0 6 gadi atpakaļ
  randoman e9d4dc11d3 work on EC fixes 6 gadi atpakaļ
27 mainītis faili ar 630 papildinājumiem un 255 dzēšanām
  1. 6 1
      CHANGELOG.md
  2. 8 8
      README.md
  3. 12 1
      XUnity.AutoTranslator.sln
  4. 1 1
      src/XUnity.AutoTranslator.Patcher/Patcher.cs
  5. 1 1
      src/XUnity.AutoTranslator.Plugin.BepIn-5x/XUnity.AutoTranslator.Plugin.BepIn-5x.csproj
  6. 1 1
      src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj
  7. 58 44
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs
  8. 0 2
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs
  9. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs
  10. 47 34
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationEndpointManager.cs
  11. 1 2
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/GameObjectExtensions.cs
  12. 18 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringBuilderExtensions.cs
  13. 4 58
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs
  14. 7 0
      src/XUnity.AutoTranslator.Plugin.Core/Properties/AssemblyInfo.cs
  15. 61 0
      src/XUnity.AutoTranslator.Plugin.Core/RegexTranslation.cs
  16. 110 22
      src/XUnity.AutoTranslator.Plugin.Core/TextTranslationCache.cs
  17. 2 2
      src/XUnity.AutoTranslator.Plugin.Core/TranslationJob.cs
  18. 0 71
      src/XUnity.AutoTranslator.Plugin.Core/TranslationKey.cs
  19. 187 0
      src/XUnity.AutoTranslator.Plugin.Core/UntranslatedText.cs
  20. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/WhitespaceHandlingStrategy.cs
  21. 1 2
      src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj
  22. 1 1
      src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj
  23. 1 1
      src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj
  24. 1 1
      src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj
  25. 19 0
      test/XUnity.AutoTranslator.Plugin.Core.Tests/RegexTranslationTests.cs
  26. 61 0
      test/XUnity.AutoTranslator.Plugin.Core.Tests/UntranslatedTextTests.cs
  27. 20 0
      test/XUnity.AutoTranslator.Plugin.Core.Tests/XUnity.AutoTranslator.Plugin.Core.Tests.csproj

+ 6 - 1
CHANGELOG.md

@@ -1,4 +1,9 @@
-### 3.2.0
+### 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'
+
+### 3.2.0
  * FEATURE - BepInEx 5.x plugin support
  * CHANGE - Restructured large portions of the internal code to support more features going forward
  * BUG FIX - Interacting with UI now blocks input to game

+ 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.

+ 12 - 1
XUnity.AutoTranslator.sln

@@ -83,7 +83,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.RuntimeHooker.Consol
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.RuntimeHooker.Benchmark", "test\XUnity.RuntimeHooker.Benchmark\XUnity.RuntimeHooker.Benchmark.csproj", "{E2F50278-9134-4DC8-9C50-4C0A52063A89}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnity.AutoTranslator.Plugin.BepIn-5x", "src\XUnity.AutoTranslator.Plugin.BepIn-5x\XUnity.AutoTranslator.Plugin.BepIn-5x.csproj", "{ADCCF172-7D31-42C6-B9D4-1779EAC8B403}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.BepIn-5x", "src\XUnity.AutoTranslator.Plugin.BepIn-5x\XUnity.AutoTranslator.Plugin.BepIn-5x.csproj", "{ADCCF172-7D31-42C6-B9D4-1779EAC8B403}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnity.AutoTranslator.Plugin.Core.Tests", "test\XUnity.AutoTranslator.Plugin.Core.Tests\XUnity.AutoTranslator.Plugin.Core.Tests.csproj", "{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -301,6 +303,14 @@ Global
 		{ADCCF172-7D31-42C6-B9D4-1779EAC8B403}.Release|Any CPU.Build.0 = Release|Any CPU
 		{ADCCF172-7D31-42C6-B9D4-1779EAC8B403}.Release|x86.ActiveCfg = Release|Any CPU
 		{ADCCF172-7D31-42C6-B9D4-1779EAC8B403}.Release|x86.Build.0 = Release|Any CPU
+		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Debug|x86.Build.0 = Debug|Any CPU
+		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Release|Any CPU.Build.0 = Release|Any CPU
+		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Release|x86.ActiveCfg = Release|Any CPU
+		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -333,6 +343,7 @@ Global
 		{20E57B16-F3C7-4D80-88FC-09D6C5A4CCC3} = {2A4A3DDF-338C-40C0-8E26-2A810BAAADD6}
 		{E2F50278-9134-4DC8-9C50-4C0A52063A89} = {2A4A3DDF-338C-40C0-8E26-2A810BAAADD6}
 		{ADCCF172-7D31-42C6-B9D4-1779EAC8B403} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD} = {2A4A3DDF-338C-40C0-8E26-2A810BAAADD6}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {EE803FED-4447-4D19-B3D6-88C56E8DFCCA}

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

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

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

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <TargetFramework>net35</TargetFramework>
-    <Version>3.2.0</Version>
+    <Version>3.3.0</Version>
   </PropertyGroup>
 
   <ItemGroup>

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

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.2.0</Version>
+      <Version>3.3.0</Version>
    </PropertyGroup>
 
    <ItemGroup>

+ 58 - 44
src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs

@@ -253,7 +253,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
          if( TranslationManager.CurrentEndpoint != endpoint )
          {
             TranslationManager.CurrentEndpoint = endpoint;
-            
+
             if( TranslationManager.CurrentEndpoint != null )
             {
                if( !Settings.IsShutdown )
@@ -371,12 +371,12 @@ namespace XUnity.AutoTranslator.Plugin.Core
          TextureCache.LoadTranslationFiles();
       }
 
-      private void CreateTranslationJobFor( TranslationEndpointManager endpoint, object ui, TranslationKey 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.GetDictionaryLookupKey() );
+            SpamChecker.PerformChecks( key.TrimmedText );
          }
       }
 
@@ -429,14 +429,14 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      private void QueueNewUntranslatedForClipboard( TranslationKey key )
+      private void QueueNewUntranslatedForClipboard( UntranslatedText key )
       {
          if( Settings.CopyToClipboard && Features.SupportsClipboard )
          {
-            if( !_textsToCopyToClipboard.Contains( key.RelevantText ) )
+            if( !_textsToCopyToClipboard.Contains( key.TrimmedText ) )
             {
-               _textsToCopyToClipboard.Add( key.RelevantText );
-               _textsToCopyToClipboardOrdered.Add( key.RelevantText );
+               _textsToCopyToClipboard.Add( key.TrimmedText );
+               _textsToCopyToClipboardOrdered.Add( key.TrimmedText );
 
                _clipboardUpdated = Time.realtimeSinceStartup;
             }
@@ -585,7 +585,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                // NGUI only behaves if you set the text after the resize behaviour
                ui.SetText( text );
-
+               
                info?.ResetScrollIn( ui );
 
                if( TranslationAggregatorWindow != null && info != null && !ui.IsSpammingComponent() )
@@ -935,18 +935,21 @@ namespace XUnity.AutoTranslator.Plugin.Core
       private string TranslateImmediate( object ui, string text, TextTranslationInfo info, bool ignoreComponentState )
       {
          // Get the trimmed text
-         text = ( text ?? ui.GetText() ).TrimIfConfigured();
+         string originalText = text;
+
+         text = text ?? ui.GetText();
 
          if( !string.IsNullOrEmpty( text ) && TextCache.IsTranslatable( text ) && ShouldTranslateTextComponent( ui, ignoreComponentState ) && !IsCurrentlySetting( info ) )
          {
-            info?.Reset( text );
+            info?.Reset( originalText );
 
             //var textKey = new TranslationKey( ui, text, !ui.SupportsStabilization(), false );
-            var textKey = new TranslationKey( ui, text, ui.IsSpammingComponent(), false );
+            var isSpammer = ui.IsSpammingComponent();
+            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;
-            if( TextCache.TryGetTranslation( textKey, out translation ) )
+            if( TextCache.TryGetTranslation( textKey, false, out translation ) )
             {
                if( !string.IsNullOrEmpty( translation ) )
                {
@@ -985,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 = new TranslationKey( 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;
@@ -1065,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 );
                }
@@ -1103,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 ) )
@@ -1116,12 +1108,11 @@ 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 = new TranslationKey( 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;
-            if( TextCache.TryGetTranslation( textKey, out translation ) )
+            if( TextCache.TryGetTranslation( textKey, !isSpammer, out translation ) )
             {
                if( context == null && !isSpammer )
                {
@@ -1206,18 +1197,17 @@ namespace XUnity.AutoTranslator.Plugin.Core
                               _ongoingOperations.Remove( ui );
 
                               originalText = stabilizedText;
-                              stabilizedText = stabilizedText.TrimIfConfigured();
 
                               if( !string.IsNullOrEmpty( stabilizedText ) && TextCache.IsTranslatable( stabilizedText ) )
                               {
-                                 var stabilizedTextKey = new TranslationKey( ui, stabilizedText, false );
+                                 var stabilizedTextKey = GetCacheKey( ui, stabilizedText, false, false );
 
                                  QueueNewUntranslatedForClipboard( stabilizedTextKey );
 
                                  info?.Reset( originalText );
 
                                  // once the text has stabilized, attempt to look it up
-                                 if( TextCache.TryGetTranslation( stabilizedTextKey, out translation ) )
+                                 if( TextCache.TryGetTranslation( stabilizedTextKey, true, out translation ) )
                                  {
                                     if( !string.IsNullOrEmpty( translation ) )
                                     {
@@ -1311,7 +1301,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
                                  // once the text has stabilized, attempt to look it up
                                  if( !Settings.IsShutdown && !endpoint.HasFailedDueToConsecutiveErrors )
                                  {
-                                    if( !TextCache.TryGetTranslation( textKey, out translation ) )
+                                    if( !TextCache.TryGetTranslation( textKey, true, out translation ) )
                                     {
                                        CreateTranslationJobFor( endpoint, ui, textKey, null, context );
                                     }
@@ -1319,7 +1309,6 @@ namespace XUnity.AutoTranslator.Plugin.Core
                               }
                            } ) );
                   }
-
                }
             }
          }
@@ -1339,7 +1328,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
             if( !string.IsNullOrEmpty( untranslatedTextPart ) && TextCache.IsTranslatable( untranslatedTextPart ) && IsBelowMaxLength( untranslatedTextPart ) )
             {
                string partTranslation;
-               if( TextCache.TryGetTranslation( untranslatedTextPart, out partTranslation ) )
+               if( TextCache.TryGetTranslation( new UntranslatedText( untranslatedTextPart, false, false ), false, out partTranslation ) )
                {
                   translations.Add( variableName, partTranslation );
                }
@@ -1406,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( TranslationKey textKey, float delay, Action onTextStabilized, Action onFailed = null )
+      private IEnumerator WaitForTextStablization( UntranslatedText textKey, float delay, Action onTextStabilized, Action onFailed = null )
       {
-         var text = textKey.GetDictionaryLookupKey();
+         var text = textKey.TrimmedText;
 
          if( !_immediatelyTranslating.Contains( text ) )
          {
@@ -1510,7 +1499,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
             {
                ConnectionTrackingWebClient.CheckServicePoints();
             }
-            
+
             if( Input.anyKey )
             {
                var isAltPressed = Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt );
@@ -1676,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 );
@@ -1699,7 +1688,7 @@ 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();
+               var text = component.GetText();
                if( text == job.Key.OriginalText )
                {
                   var info = component.GetOrCreateTextTranslationInfo();
@@ -1723,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>();
 
@@ -1787,6 +1776,14 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
+      private static UntranslatedText GetCacheKey( object ui, string originalText, bool templatizeByNumbers, bool neverRemoveWhitespace )
+      {
+         var removeInternalWhitespace = !neverRemoveWhitespace
+            && ( ( Settings.IgnoreWhitespaceInDialogue && originalText.Length > Settings.MinDialogueChars ) || ( Settings.IgnoreWhitespaceInNGUI && ui.IsNGUI() ) );
+
+         return new UntranslatedText( originalText, templatizeByNumbers, removeInternalWhitespace );
+      }
+
       private void ReloadTranslations()
       {
          LoadTranslations();
@@ -1802,10 +1799,11 @@ namespace XUnity.AutoTranslator.Plugin.Core
                   if( component.gameObject?.activeSelf ?? false )
                   {
                      var tti = kvp.Value as TextTranslationInfo;
-                     if( tti != null && !string.IsNullOrEmpty( tti.OriginalText ) )
+                     var originalText = tti.OriginalText;
+                     if( tti != null && !string.IsNullOrEmpty( originalText ) )
                      {
-                        var key = new TranslationKey( kvp.Key, tti.OriginalText, false );
-                        if( TextCache.TryGetTranslation( key, out string translatedText ) && !string.IsNullOrEmpty( translatedText ) )
+                        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
                         }
@@ -2056,7 +2054,23 @@ namespace XUnity.AutoTranslator.Plugin.Core
          if( obj != null )
          {
             var layer = LayerMask.LayerToName( obj.layer );
-            var components = string.Join( ", ", obj.GetComponents<Component>().Select( x => x?.GetType()?.Name ).Where( x => x != null ).ToArray() );
+            var components = string.Join( ", ", obj.GetComponents<Component>().Select( x =>
+            {
+               string output = null;
+               var type = x?.GetType();
+               if( type != null )
+               {
+                  output = type.Name;
+
+                  var text = x.GetText();
+                  if( !string.IsNullOrEmpty( text ) )
+                  {
+                     output += " (" + text + ")";
+                  }
+               }
+
+               return output;
+            } ).Where( x => x != null ).ToArray() );
             var line = string.Format( "{0,-50} {1,100}",
                identation + obj.name + " [" + layer + "]",
                components );

+ 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 );

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

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

+ 47 - 34
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( TranslationKey key, out string value )
+      public bool TryGetTranslation( UntranslatedText key, out string value )
       {
-         return TryGetTranslation( key.GetDictionaryLookupKey(), 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( TranslationKey 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.GetDictionaryLookupKey();
+               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.GetDictionaryLookupKey();
+            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.GetDictionaryLookupKey() );
+               RemoveOngoingTranslation( job.Key );
 
-               XuaLogger.Current.Info( $"Completed: '{job.Key.GetDictionaryLookupKey()}' => '{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.GetDictionaryLookupKey();
+               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.GetDictionaryLookupKey() );
+         RemoveOngoingTranslation( job.Key );
 
-         XuaLogger.Current.Info( $"Completed: '{job.Key.GetDictionaryLookupKey()}' => '{job.TranslatedText}'" );
+         XuaLogger.Current.Info( $"Completed: '{job.Key.TrimmedText}' => '{job.TranslatedText}'" );
 
          Manager.InvokeJobCompleted( job );
       }
 
-      private string PostProcessTranslation( TranslationKey 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 )
             {
@@ -317,13 +332,13 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          {
             foreach( var job in jobs )
             {
-               var untranslatedText = job.Key.GetDictionaryLookupKey();
+               var key = job.Key;
                job.State = TranslationJobState.Failed;
                job.ErrorMessage = error;
 
-               RemoveOngoingTranslation( untranslatedText );
+               RemoveOngoingTranslation( key );
 
-               RegisterTranslationFailureFor( untranslatedText );
+               RegisterTranslationFailureFor( key.TrimmedText );
 
                Manager.InvokeJobFailed( job );
             }
@@ -340,7 +355,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
             {
                var job = jobs[ i ];
 
-               var key = job.Key.GetDictionaryLookupKey();
+               var key = job.Key;
                AddUnstartedJob( key, job );
                RemoveOngoingTranslation( key );
             }
@@ -370,11 +385,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          XuaLogger.Current.Info( "Re-enabled batching." );
       }
 
-      public bool EnqueueTranslation( object ui, TranslationKey key, TranslationResult translationResult, ParserTranslationContext context )
+      public bool EnqueueTranslation( object ui, UntranslatedText key, TranslationResult translationResult, ParserTranslationContext context )
       {
-         var lookupKey = key.GetDictionaryLookupKey();
-
-         var added = AssociateWithExistingJobIfPossible( ui, lookupKey, translationResult, context );
+         var added = AssociateWithExistingJobIfPossible( ui, key, translationResult, context );
          if( added )
          {
             return false;
@@ -390,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;
@@ -398,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 ) )
          {
@@ -424,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 ) )
          {
@@ -443,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 ) )
          {
@@ -463,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.";
 

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

@@ -9,7 +9,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
    internal static class GameObjectExtensions
    {
       private static GameObject[] _objects = new GameObject[ 128 ];
-      private static readonly string DummyName = "Dummy";
       private static readonly string XuaIgnore = "XUAIGNORE";
 
       public static Component GetFirstComponentInSelfOrAncestor( this GameObject go, Type type )
@@ -57,7 +56,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 
       public static bool HasIgnoredName( this GameObject go )
       {
-         return go.name.EndsWith( DummyName ) || go.name.Contains( XuaIgnore ) || go.transform?.parent?.name.EndsWith( DummyName ) == true;
+         return go.name.Contains( XuaIgnore );
       }
    }
 }

+ 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;
+      }
    }
 }

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

@@ -60,8 +60,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
       };
       private static readonly HashSet<char> InvalidFileNameChars = new HashSet<char>( Path.GetInvalidFileNameChars() );
 
-      private static readonly char[] NewlinesCharacters = new char[] { '\r', '\n' };
+      private static readonly string[] NewlinesCharacters = new string[] { "\r\n", "\n" };
       private static readonly char[] WhitespacesAndNewlines = new char[] { '\r', '\n', ' ', ' ' };
+      private static readonly char[] Spaces = new char[] { ' ', ' ' };
 
       public static string SanitizeForFileSystem( this string path )
       {
@@ -83,7 +84,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          StringBuilder carg = null;
          char arg = 'A';
 
-         for( int i = 0 ; i < str.Length ; i++ )
+         for( int i = 0; i < str.Length; i++ )
          {
             var c = str[ i ];
             if( isNumber )
@@ -200,67 +201,12 @@ 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();
-         }
-         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.RemoveEmptyEntries );
-            var lastLine = lines.Length - 1;
-            for( int i = 0 ; i < lines.Length ; i++ )
-            {
-               var line = lines[ i ].Trim( WhitespacesAndNewlines );
-               for( int j = 0 ; j < line.Length ; j++ )
-               {
-                  var c = line[ j ];
-                  builder.Append( c );
-               }
-
-               // 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 );
          if( len < prefix.Length ) return false;
 
-         for( int i = 0 ; i < len ; i++ )
+         for( int i = 0; i < len; i++ )
          {
             if( str[ i ] != prefix[ i ] ) return false;
          }

+ 7 - 0
src/XUnity.AutoTranslator.Plugin.Core/Properties/AssemblyInfo.cs

@@ -0,0 +1,7 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+[assembly: InternalsVisibleTo( "XUnity.AutoTranslator.Plugin.Core.Tests" )]

+ 61 - 0
src/XUnity.AutoTranslator.Plugin.Core/RegexTranslation.cs

@@ -0,0 +1,61 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace XUnity.AutoTranslator.Plugin.Core
+{
+   class RegexTranslation
+   {
+      public RegexTranslation( string key, string value )
+      {
+         // remove r:
+         if( key.StartsWith( "r:" ) )
+         {
+            key = key.Substring( 2, key.Length - 2 );
+         }
+
+         var startIdx = key.IndexOf( '"' ) + 1;
+         if( startIdx == -1 )
+         {
+            // take entire string
+         }
+         else
+         {
+            var endIdx = key.LastIndexOf( '"', key.Length - 1 );
+            if( endIdx != startIdx )
+            {
+               key = key.Substring( startIdx, endIdx - startIdx );
+            }
+         }
+
+         // remove r:
+         if( value.StartsWith( "r:" ) )
+         {
+            value = value.Substring( 2, value.Length - 2 );
+         }
+
+         startIdx = value.IndexOf( '"' ) + 1;
+         if( startIdx == -1 )
+         {
+            // take entire string
+         }
+         else
+         {
+            var endIdx = value.LastIndexOf( '"', value.Length - 1 );
+            if( endIdx != startIdx )
+            {
+               value = value.Substring( startIdx, endIdx - startIdx );
+            }
+         }
+
+         CompiledRegex = new Regex( key );
+         Original = key;
+         Translation = value;
+      }
+
+      public Regex CompiledRegex { get; set; }
+
+      public string Original { get; set; }
+
+      public string Translation { get; set; }
+   }
+}

+ 110 - 22
src/XUnity.AutoTranslator.Plugin.Core/TextTranslationCache.cs

@@ -21,6 +21,9 @@ namespace XUnity.AutoTranslator.Plugin.Core
       private Dictionary<string, string> _translations = new Dictionary<string, string>();
       private Dictionary<string, string> _reverseTranslations = new Dictionary<string, string>();
 
+      private List<RegexTranslation> _defaultRegexes = new List<RegexTranslation>();
+      private HashSet<string> _registeredRegexes = new HashSet<string>();
+
       /// <summary>
       /// These are the new translations that has not yet been persisted to the file system.
       /// </summary>
@@ -48,6 +51,9 @@ namespace XUnity.AutoTranslator.Plugin.Core
                Directory.CreateDirectory( Path.Combine( PluginEnvironment.Current.TranslationPath, Settings.TranslationDirectory ).Parameterize() );
                Directory.CreateDirectory( Path.GetDirectoryName( Settings.AutoTranslationsFilePath ) );
 
+               _registeredRegexes.Clear();
+               _defaultRegexes.Clear();
+
                var mainTranslationFile = Settings.AutoTranslationsFilePath;
                LoadTranslationsInFile( mainTranslationFile );
                foreach( var fullFileName in GetTranslationFiles().Reverse().Except( new[] { mainTranslationFile } ) )
@@ -56,7 +62,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
                }
             }
             var endTime = Time.realtimeSinceStartup;
-            XuaLogger.Current.Info( $"Loaded text files (took {Math.Round( endTime - startTime, 2 )} seconds)" );
+            XuaLogger.Current.Info( $"Loaded text files ({_translations.Count} translations and {_defaultRegexes.Count} regex translations) (took {Math.Round( endTime - startTime, 2 )} seconds)" );
          }
          catch( Exception e )
          {
@@ -79,12 +85,36 @@ 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 ) )
                      {
-                        AddTranslation( key, value );
+                        if( key.StartsWith( "r:" ) )
+                        {
+                           try
+                           {
+                              var regex = new RegexTranslation( key, value );
+
+                              AddTranslationRegex( regex );
+                           }
+                           catch( Exception e )
+                           {
+                              XuaLogger.Current.Warn( e, $"An error occurred while constructing regex translation: '{translation}'." );
+                           }
+                        }
+                        else
+                        {
+                           AddTranslation( key, value );
+
+                           // also add a modified version of the translation
+                           var ukey = new UntranslatedText( key, false, false );
+                           var uvalue = new UntranslatedText( value, false, false );
+                           if( ukey.TrimmedText != key )
+                           {
+                              AddTranslation( ukey.TrimmedText, uvalue.TrimmedText );
+                           }
+                        }
                         break;
                      }
                   }
@@ -111,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 ) )
                      {
@@ -148,6 +178,19 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
+      private void AddTranslationRegex( RegexTranslation regex )
+      {
+         if( !_registeredRegexes.Contains( regex.Original ) )
+         {
+            _registeredRegexes.Add( regex.Original );
+            _defaultRegexes.Add( regex );
+         }
+         //else
+         //{
+         //   XuaLogger.Current.Warn( $"Could not register translation regex '{regex.Original}' because it has already been registered." );
+         //}
+      }
+
       private bool HasTranslated( string key )
       {
          return _translations.ContainsKey( key );
@@ -172,41 +215,86 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      internal void AddTranslationToCache( TranslationKey key, string value )
-      {
-         AddTranslationToCache( key.GetDictionaryLookupKey(), 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( TranslationKey key, out string value )
+      internal bool TryGetTranslation( UntranslatedText key, bool allowRegex, out string value )
       {
-         return TryGetTranslation( key.GetDictionaryLookupKey(), out value );
-      }
+         var unmodifiedKey = key.TranslatableText;
+         var result = _translations.TryGetValue( unmodifiedKey, out value );
+         if( result )
+         {
+            return result;
+         }
 
-      internal bool TryGetTranslation( string key, 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;
          }
-         else if( _staticTranslations.Count > 0 )
+
+         if( allowRegex )
          {
-            if( _staticTranslations.TryGetValue( key, out value ) )
+            bool found = false;
+
+            var len = _defaultRegexes.Count;
+            for( int i = 0; i < len; i++ )
+            {
+               var regex = _defaultRegexes[ i ];
+               var match = regex.CompiledRegex.Match( unmodifiedKey );
+               if( !match.Success ) continue;
+
+               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 lookup: '{key.TrimmedText}' => '{value}'" );
+               break;
+            }
+
+            if( found )
             {
-               QueueNewTranslationForDisk( key, value );
-               AddTranslation( key, value );
                return true;
             }
          }
+
+         if( _staticTranslations.Count > 0 )
+         {
+            if( _staticTranslations.TryGetValue( unmodifiedKey, out 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;
+            }
+         }
+
          return result;
       }
 

+ 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, TranslationKey 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 TranslationKey Key { get; private set; }
+      public UntranslatedText Key { get; private set; }
 
       public string TranslatedText { get; set; }
 

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

@@ -1,71 +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 struct TranslationKey
-   {
-      public TranslationKey( object ui, string key, bool templatizeByNumbers, bool neverRemoveWhitespace = false )
-      {
-         OriginalText = key;
-
-         if( !neverRemoveWhitespace
-            && ( ( Settings.IgnoreWhitespaceInDialogue && key.Length > Settings.MinDialogueChars ) || ( Settings.IgnoreWhitespaceInNGUI && ui.IsNGUI() ) ) )
-         {
-            RelevantText = key.RemoveWhitespaceAndNewlines();
-         }
-         else
-         {
-            RelevantText = key;
-         }
-
-         if( templatizeByNumbers )
-         {
-            TemplatedText = RelevantText.TemplatizeByNumbers();
-         }
-         else
-         {
-            TemplatedText = null;
-         }
-      }
-
-      public TemplatedString TemplatedText { get; }
-
-      public string RelevantText { get; }
-
-      public string OriginalText { get; set; }
-
-      public string GetDictionaryLookupKey()
-      {
-         if( TemplatedText != null )
-         {
-            return TemplatedText.Template;
-         }
-         return RelevantText;
-      }
-
-      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;
-      }
-   }
-}

+ 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();
+      }
+   }
+}

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

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

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

@@ -2,8 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.2.0</Version>
-      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+      <Version>3.3.0</Version>
    </PropertyGroup>
 
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

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

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.2.0</Version>
+      <Version>3.3.0</Version>
    </PropertyGroup>
 
    <ItemGroup>

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

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.2.0</Version>
+      <Version>3.3.0</Version>
    </PropertyGroup>
 
    <ItemGroup>

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

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

+ 19 - 0
test/XUnity.AutoTranslator.Plugin.Core.Tests/RegexTranslationTests.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Tests
+{
+   public class RegexTranslationTests
+   {
+      [Theory( DisplayName = "Can_Create_Regex" )]
+      [InlineData( "r:\"^タイプ([0-90-9]+)$\"", "r:\"Type $1\"" )]
+      public void Can_Create_Regex( string key, string value )
+      {
+         var regex = new RegexTranslation( key, value );
+      }
+   }
+}

+ 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 );
+      }
+   }
+}

+ 20 - 0
test/XUnity.AutoTranslator.Plugin.Core.Tests/XUnity.AutoTranslator.Plugin.Core.Tests.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net471</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
+    <PackageReference Include="xunit" Version="2.4.1" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+    </PackageReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\XUnity.AutoTranslator.Plugin.Core\XUnity.AutoTranslator.Plugin.Core.csproj" />
+  </ItemGroup>
+
+</Project>