1
0

33 Коммитууд 5919ba3167 ... 11108f0565

Эзэн SHA1 Мессеж Огноо
  randoman 11108f0565 Version 3.0.0 6 жил өмнө
  randoman 31c2ef5bbe Version 3.0.0 6 жил өмнө
  randoman 4a155a2b75 renamed RomajiPostProcessing to TextPostProcessing 6 жил өмнө
  randoman eed766835b minor spelling fix in README 6 жил өмнө
  randoman a78e951f0b readme update 6 жил өмнө
  randoman 8a66769056 formatting 6 жил өмнө
  randoman 5caf7c5e5e bing fix 6 жил өмнө
  randoman a05c978ec8 romaji post processing 6 жил өмнө
  randoman 262ec32b81 spelling error 6 жил өмнө
  randoman 7483e63ba9 minor fix to LEC 6 жил өмнө
  randoman d111aa30ab logic to handle same error consistently occurring towards a specific endpoint 6 жил өмнө
  randoman 1a8158402d ExtProtocol improvement (multiline) and bug fix 6 жил өмнө
  randoman 89527a23e0 initial endpoint selection logic fix 6 жил өмнө
  randoman afd1fcb002 user agents fix, super fix watson google legit batching 6 жил өмнө
  randoman 38f9fe0f98 fixed project build dependencies 6 жил өмнө
  randoman 9f235d9a12 enable build in all configurations 6 жил өмнө
  randoman 2ac151ff9b bing batching, yandex simplify, google fixes 6 жил өмнө
  randoman f4613bce2d general support for batching for ITranslateEndpoints 6 жил өмнө
  randoman cd7293bb4f removing duplicate interface implementations 6 жил өмнө
  randoman 519d1dd9cf "unfixed" yandex translate 6 жил өмнө
  randoman 037aadea4b xml documentation for public api 6 жил өмнө
  randoman c19bc569d0 UI error handling, config error handling 6 жил өмнө
  randoman 262a37f53b improved API, language checks, improved UI 6 жил өмнө
  randoman ee176f3bb5 doc update, UI update, ext protocol update 6 жил өмнө
  randoman 60e1c87841 improved public interface 6 жил өмнө
  randoman 18276e367f moved endpoints implementations out of main assembly 6 жил өмнө
  randoman d4ab0f3e50 renamed translator assemblies 6 жил өмнө
  randoman b739c414aa improved various interfaces 6 жил өмнө
  randoman 1090a4fc1a project config fixes 6 жил өмнө
  randoman a5bc0f6411 bumped version, finalized dynamic plugins 6 жил өмнө
  randoman abb5b9093b splitting out translator projects 6 жил өмнө
  randoman eb2819ca04 work on designing HttpEndpoint interface 6 жил өмнө
  randoman f44009d6e7 Fixed public API 6 жил өмнө
100 өөрчлөгдсөн 4675 нэмэгдсэн , 1162 устгасан
  1. 16 1
      CHANGELOG.md
  2. 250 22
      README.md
  3. 117 3
      XUnity.AutoTranslator.sln
  4. 15 0
      src/Translators/BaiduTranslate/BaiduTranslate.csproj
  5. 100 0
      src/Translators/BaiduTranslate/BaiduTranslateEndpoint.cs
  6. 15 0
      src/Translators/BingTranslate/BingTranslate.csproj
  7. 255 0
      src/Translators/BingTranslate/BingTranslateEndpoint.cs
  8. 15 0
      src/Translators/BingTranslateLegitimate/BingTranslateLegitimate.csproj
  9. 110 0
      src/Translators/BingTranslateLegitimate/BingTranslateLegitimateEndpoint.cs
  10. 15 0
      src/Translators/CustomTranslate/CustomTranslate.csproj
  11. 57 0
      src/Translators/CustomTranslate/CustomTranslateEndpoint.cs
  12. 15 0
      src/Translators/GoogleTranslate/GoogleTranslate.csproj
  13. 369 0
      src/Translators/GoogleTranslate/GoogleTranslateEndpoint.cs
  14. 15 0
      src/Translators/GoogleTranslateLegitimate/GoogleTranslateLegitimate.csproj
  15. 81 0
      src/Translators/GoogleTranslateLegitimate/GoogleTranslateLegitimateEndpoint.cs
  16. 17 0
      src/Translators/Lec.ExtProtocol/Kernel32.cs
  17. 26 0
      src/Translators/Lec.ExtProtocol/Lec.ExtProtocol.csproj
  18. 159 0
      src/Translators/Lec.ExtProtocol/LecTranslationLibrary.cs
  19. 72 0
      src/Translators/Lec.ExtProtocol/Program.cs
  20. 7 0
      src/Translators/Lec.ExtProtocol/Properties/launchSettings.json
  21. 53 0
      src/Translators/Lec.ExtProtocol/UnmanagedLibraryLoader.cs
  22. 64 0
      src/Translators/Lec.ExtProtocol/UnmanagedTranslationLibrary.cs
  23. 21 0
      src/Translators/LecPowerTranslator15/LecPowerTranslator15.csproj
  24. 49 0
      src/Translators/LecPowerTranslator15/LecPowerTranslator15Endpoint.cs
  25. 11 0
      src/Translators/ReverseTranslator/ReverseTranslator.csproj
  26. 35 0
      src/Translators/ReverseTranslator/ReverseTranslatorEndpoint.cs
  27. 15 0
      src/Translators/WatsonTranslate/WatsonTranslate.csproj
  28. 92 0
      src/Translators/WatsonTranslate/WatsonTranslateEndpoint.cs
  29. 15 0
      src/Translators/YandexTranslate/YandexTranslate.csproj
  30. 72 0
      src/Translators/YandexTranslate/YandexTranslateEndpoint.cs
  31. 1 1
      src/XUnity.AutoTranslator.Patcher/Patcher.cs
  32. 1 1
      src/XUnity.AutoTranslator.Patcher/XUnity.AutoTranslator.Patcher.csproj
  33. 7 14
      src/XUnity.AutoTranslator.Plugin.BepIn/AutoTranslatorPlugin.cs
  34. 1 1
      src/XUnity.AutoTranslator.Plugin.BepIn/BepInLogger.cs
  35. 2 2
      src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj
  36. 26 0
      src/XUnity.AutoTranslator.Plugin.Core/ApplicationInformation.cs
  37. 380 131
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs
  38. 25 0
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslatorSettings.cs
  39. 19 0
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslatorState.cs
  40. 0 71
      src/XUnity.AutoTranslator.Plugin.Core/Batching/TranslationBatch.cs
  41. 0 19
      src/XUnity.AutoTranslator.Plugin.Core/Batching/TranslationLineTracker.cs
  42. 0 13
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Config.cs
  43. 0 19
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/IConfiguration.cs
  44. 0 92
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/IniKeyExtensions.cs
  45. 81 126
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs
  46. 2 2
      src/XUnity.AutoTranslator.Plugin.Core/ConsoleLogger.cs
  47. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Constants/ClrTypes.cs
  48. 0 28
      src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownEndpointNames.cs
  49. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownEvents.cs
  50. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownPlugins.cs
  51. 23 0
      src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownTranslateEndpointNames.cs
  52. 13 1
      src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs
  53. 35 0
      src/XUnity.AutoTranslator.Plugin.Core/Constants/UserAgents.cs
  54. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Debugging/DebugConsole.cs
  55. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Debugging/Kernel32.cs
  56. 3 3
      src/XUnity.AutoTranslator.Plugin.Core/DefaultPluginEnvironment.cs
  57. 49 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/AssemblyLoader.cs
  58. 92 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ConfiguredEndpoint.cs
  59. 280 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ExtProtocol/ExtProtocolEndpoint.cs
  60. 32 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ExtProtocol/ProtocolTransactionHandle.cs
  61. 104 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/HttpEndpoint.cs
  62. 51 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/HttpTranslationContext.cs
  63. 16 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/IHttpRequestCreationContext.cs
  64. 20 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/IHttpResponseInspectionContext.cs
  65. 11 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/IHttpTranslationContext.cs
  66. 25 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/IHttpTranslationExtractionContext.cs
  67. 48 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/IInitializationContext.cs
  68. 47 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ITranslateEndpoint.cs
  69. 23 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ITranslationContext.cs
  70. 45 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ITranslationContextBase.cs
  71. 53 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/InitializationContext.cs
  72. 66 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/KnownTranslateEndpoints.cs
  73. 134 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationContext.cs
  74. 14 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/IWwwRequestCreationContext.cs
  75. 11 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/IWwwTranslationContext.cs
  76. 28 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/IWwwTranslationExtractionContext.cs
  77. 129 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/WwwEndpoint.cs
  78. 59 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/WwwRequestInfo.cs
  79. 49 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/WwwTranslationContext.cs
  80. 0 162
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/ComponentExtensions.cs
  81. 20 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/EnumerableExtensions.cs
  82. 24 1
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/GameObjectExtensions.cs
  83. 2 2
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/HarmonyInstanceExtensions.cs
  84. 82 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/IniFileExtensions.cs
  85. 9 1
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringBuilderExtensions.cs
  86. 2 141
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs
  87. 132 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextComponentExtensions.cs
  88. 59 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextureComponentExtensions.cs
  89. 21 1
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextureExtensions.cs
  90. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/TypeExtensions.cs
  91. 48 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/UILabelExtensions.cs
  92. 13 4
      src/XUnity.AutoTranslator.Plugin.Core/Features.cs
  93. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Fonts/FontCache.cs
  94. 13 60
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/HooksSetup.cs
  95. 22 21
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/IMGUIHooks.cs
  96. 25 174
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/ImageHooks.cs
  97. 5 5
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/NGUIHooks.cs
  98. 9 8
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextGetterCompatHooks.cs
  99. 19 19
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshProHooks.cs
  100. 5 6
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/UGUIHooks.cs

+ 16 - 1
CHANGELOG.md

@@ -1,4 +1,19 @@
-### 2.18.0
+### 3.0.0
+ * FEATURE - UI to control plugin more conveniently (press ALT + 0 (that's a zero))
+ * FEATURE - Dynamic selection of translator during game session
+ * FEATURE - Support BingTranslate API
+ * FEATURE - Support LEC Offline Power Translator 15
+ * FEATURE - Enable custom implementations of translators
+ * FEATURE - Removed support for Excite translate because it only support the 'WWW' API in Unity due to missing Tls1.2 support
+ * FEATURE - Updated Watson translate to v3
+ * FEATURE - Support for 'romaji' as output language. Only google supports this at the moment
+ * FEATURE - Batching support for all endpoints where the API supports it
+ * BUG FIX - Too many small fixes to mention
+ * MISC - {GameExeName} variable can now be used in configuration of directories and files
+ * MISC - Changed the way the 'Custom' endpoint works. See README for more info
+ * MISC - Added new configuration 'GameLogTextPaths' to enable special handling of text components that text is being appended to continuously (requires export knowledge to setup)
+
+### 2.18.0
  * FEATURE - Text Getter Compatibility Mode. Fools the game into thinking that it is not actually translated
  * FEATURE - Textures - Live2D component support
  * FEATURE - Textures - SpriteRenderer component support

+ 250 - 22
README.md

@@ -13,6 +13,7 @@
  * [Regarding Redistribution](#regarding-redistribution)
  * [Texture Translation](#texture-translation)
  * [Integrating with Auto Translator](#integrating-with-auto-translator)
+ * [Implementing a Translator](#implementing-a-translator)
  
 ## Introduction
 This is a plugin that is capable of using various online translators to provide on-the-fly translations for various Unity-based games.
@@ -23,30 +24,38 @@ Oh, and it is also capable of doing some basic image loading/dumping. Go to the
 
 If you intend on redistributing this plugin as part of a translation suite for a game, please read [this section](#regarding-redistribution).
 
+From version 3.0.0 it is possible to implement custom translators. See [this section](#implementing-a-translator) for more info.
+
 ## Translators
-The supported online translators are:
+The supported translators are:
  * [GoogleTranslate](https://anonym.to/?https://translate.google.com/), based on the online Google translation service. Does not require authentication.
    * No limitations, but unstable.
  * [GoogleTranslateLegitimate](https://anonym.to/?https://cloud.google.com/translate/), based on the Google cloud translation API. Requires an API key.
    * Provides trial period of 1 year with $300 credits. Enough for 15 million characters translations.
+ * [BingTranslate](https://anonym.to/?https://www.bing.com/translator), based on the online Bing translation service. Does not require authentication.
+   * No limitations, but unstable.
  * [BingTranslateLegitimate](https://anonym.to/?https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-info-overview), based on the Azure text translation. Requires an API key.
    * Free up to 2 million characters per month.
  * [BaiduTranslate](https://anonym.to/?https://fanyi.baidu.com/), based on Baidu translation service. Requires AppId and AppSecret.
    * Not sure on quotas on this one.
  * [YandexTranslate](https://anonym.to/?https://tech.yandex.com/translate/), based on the Yandex translation service. Requires an API key.
    * Free up to 1 million characters per day, but max 10 million characters per month.
- * [WatsonTranslate](https://anonym.to/?https://cloud.ibm.com/apidocs/language-translator), based on IBM's Watson. Requires a URL, username and password.
+ * [WatsonTranslate](https://anonym.to/?https://cloud.ibm.com/apidocs/language-translator), based on IBM's Watson. Requires a URL and an API key.
    * Free up to 1 million characters per month.
- * [ExciteTranslate](https://anonym.to/?https://www.excite.co.jp/world/english_japanese/), based on the Excite online translation service. Does not require authentication.
-   * No limitations, but unstable. Also very slow.
- * Custom. Alternatively you can also specify any custom HTTP url that can be used as a translation endpoint (GET request). This must use the query parameters "from", "to" and "text" and return only a string with the result (try HTTP without SSL first, as unity-mono often has issues with SSL).
-   * Example Endpoint=http://my-custom-translation-service.net/translate
+ * LecPowerTranslator15, based on LEC's Power Translator. Does not require authentication, but does require the software installed.
+   * No limitations.
+ * CustomTranslate. Alternatively you can also specify any custom HTTP url that can be used as a translation endpoint (GET request). This must use the query parameters "from", "to" and "text" and return only a string with the result (try HTTP without SSL first, as unity-mono often has issues with SSL).
+   * Example Configuration:
+     * Endpoint=CustomTranslate
+     * [Custom]
+     * Url=http://my-custom-translation-service.net/translate
    * Example Request: GET http://my-custom-translation-service.net/translate?from=ja&to=en&text=こんにちは
    * Example Response (only body): Hello
-   * If someone is willing to implement it, this could provide support for offline translators such as ATLAS or LEC as well, since it enables the scenario where you simply run a local web server that can serve translation requests.
  
 **Do note that if you use any of the online translators that does not require some form of authentication, that this plugin may break at any time.**
 
+Since 3.0.0, you can also implement your own translators. To do so, follow the instruction [here](#implementing-a-translator).
+
 ### About Authenticated Translators
 If you decide to use an authenticated service *do not ever share your key or secret*. If you do so by accident, you should revoke it immediately. Most, if not all services provides an option for this.
 
@@ -90,19 +99,19 @@ Installation instructions for all methods can be found below.
 Additionally it can be installed without a dependency on a plugin manager through ReiPatcher. However, this approach is not recommended if you use one of the above mentioned Plugin Managers!
 
 ## Configuration
-The default configuration file, looks as such (2.6.0+):
+The default configuration file, looks as such:
 
 ```ini
 [Service]
-Endpoint=GoogleTranslate         ;Endpoint to use. Can be ["GoogleTranslate", "GoogleTranslateLegitimate", "BingTranslateLegitimate", "BaiduTranslate", "YandexTranslate", "WatsonTranslate", "ExciteTranslate", "*any custom http endpoint*", ""]. If empty, it simply means: Only use cached translations
+Endpoint=GoogleTranslate         ;Endpoint to use. Can be ["GoogleTranslate", "GoogleTranslateLegitimate", "BingTranslate", "BingTranslateLegitimate", "BaiduTranslate", "YandexTranslate", "WatsonTranslate", "LecPowerTranslator15", "CustomTranslate", ""]. If empty, it simply means: Only use cached translations. Additional translators can also be used if downloaded externally.
 
 [General]
 Language=en                      ;The language to translate into
 FromLanguage=ja                  ;The original language of the game
 
 [Files]
-Directory=Translation                                          ;Directory to search for cached translation files
-OutputFile=Translation\_AutoGeneratedTranslations.{lang}.txt   ;File to insert generated translations into
+Directory=Translation                                          ;Directory to search for cached translation files. Can use placeholder: {GameExeName}
+OutputFile=Translation\_AutoGeneratedTranslations.{lang}.txt   ;File to insert generated translations into. Can use placeholders: {GameExeName}, {lang}
 
 [TextFrameworks]
 EnableUGUI=True                  ;Enable or disable UGUI translation
@@ -131,9 +140,11 @@ ResizeUILineSpacingScale=        ;A decimal value that the default line spacing
 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 ';'.
 TextGetterCompatibilityMode=False ;Indicates whether or not to enable "Text Getter Compatibility Mode". Should only be enabled if required by the game. 
+GameLogTextPaths=                ;Indicates specific paths for game objects that the game uses as "log components", where it continuously appends or prepends text to. Requires expert knowledge to setup. This is a list seperated by ';'.
+RomajiPostProcessing=RemoveAllDiacritics;RemoveApostrophes ;Indicates what type of post processing to do on 'translated' romaji texts. This can be important in certain games because the font used does not support various diacritics properly. This is a list seperated by ';'. Possible values: ["RemoveAllDiacritics", "ReplaceMacronWithCircumflex", "RemoveApostrophes"]
 
 [Texture]
-TextureDirectory=Translation\Texture ;Directory to dump textures to, and root of directories to load images from
+TextureDirectory=Translation\Texture ;Directory to dump textures to, and root of directories to load images from. Can use placeholder: {GameExeName}
 EnableTextureTranslation=False   ;Indicates whether the plugin will attempt to replace in-game images with those from the TextureDirectory directory
 EnableTextureDumping=False       ;Indicates whether the plugin will dump texture it is capable of replacing to the TextureDirectory. Has significant performance impact
 EnableTextureToggling=False      ;Indicates whether or not toggling the translation with the ALT+T hotkey will also affect textures. Not guaranteed to work for all textures. Has significant performance impact
@@ -159,9 +170,14 @@ BaiduAppSecret=                  ;OPTIONAL, needed if BaiduTranslate is configur
 YandexAPIKey=                    ;OPTIONAL, needed if YandexTranslate is configured
 
 [Watson]
-WatsonAPIUrl=                    ;OPTIONAL, needed if WatsonTranslate is configured
-WatsonAPIUsername=               ;OPTIONAL, needed if WatsonTranslate is configured
-WatsonAPIPassword=               ;OPTIONAL, needed if WatsonTranslate is configured
+Url=                             ;OPTIONAL, needed if WatsonTranslate is configured
+Key=                             ;OPTIONAL, needed if WatsonTranslate is configured
+
+[Custom]
+Url=                             ;Optional, needed if CustomTranslated is configured
+
+[LecPowerTranslator15]
+InstallationPath=                ;Optional, needed if LecPowerTranslator15 is configured
 
 [Debug]
 EnablePrintHierarchy=False       ;Used for debugging
@@ -209,10 +225,18 @@ Resizing of a UI component does not refer to changing of it's dimensions, but ra
 
 #### Reducing Translation Requests
 The following aims at reducing the number of requests send to the translation endpoint:
- * `EnableBatching`: Batches several translation requests into a single with supported endpoints (Only GoogleTranslate and GoogleTranslateLegitimate at the moment)
+ * `EnableBatching`: Batches several translation requests into a single with supported endpoints.
  * `UseStaticTranslations`: Enables usage of internal lookup dictionary of various english-to-japanese terms.
  * `MaxCharactersPerTranslation`: Specifies the maximum length of a text to translate. Any texts longer than this is ignored by the plugin. Cannot be greater than 500.
 
+#### Romaji 'translation'
+One of the possible values as output `Language` is 'romaji'. If you choose this as language, you will find that games often has problems showing the translations because the font does not understand the special characters used, for example the [macron diacritic](https://en.wikipedia.org/wiki/Macron_(diacritic)).
+
+To rememdy this, post processing can be applied to translations when 'romaji' is chosen as `Language`. This is done through the option `RomajiPostProcessing`. This option is a ';'-seperated list of values:
+ * `RemoveAllDiacritics`: Remove all diacritics from the translated text
+ * `ReplaceMacronWithCircumflex`: Replaces the macron diacritic with a circumflex.
+ * `RemoveApostrophes`: Some translators might decide to include apostrophes after the 'n'-character. Applying this option removes those.
+
 #### Other Options
  * `TextGetterCompatibilityMode`: This mode fools the game into thinking that the text displayed is not translated. This is required if the game uses text displayed to the user to determine what logic to execute. You can easily determine if this is required if you can see the functionality works fine if you toggle the translation off (hotkey: ALT+T).
  * `IgnoreTextStartingWith`: Disable translation for any texts starting with values in this ';-separated' setting. The [default value](https://www.charbase.com/180e-unicode-mongolian-vowel-separator) is an invisible character that takes up no space.
@@ -221,6 +245,7 @@ The following aims at reducing the number of requests send to the translation en
 
 ## Key Mapping
 The following key inputs are mapped:
+ * ALT + 0: Toggle XUnity AutoTranslator UI. (That's a zero, not an O)
  * ALT + T: Alternate between translated and untranslated versions of all texts provided by this plugin.
  * ALT + D: Dump untranslated texts (if no endpoint is configured)
  * ALT + R: Reload translation files. Useful if you change the text and texture files on the fly. Not guaranteed to work for all textures.
@@ -240,8 +265,10 @@ REQUIRES: [BepInEx plugin manager](https://github.com/BepInEx/BepInEx) (follow i
 The file structure should like like this:
 ```
 {GameDirectory}/BepInEx/XUnity.AutoTranslator.Plugin.Core.dll
-{GameDirectory}/BepInEx/XUnity.AutoTranslator.Plugin.Core.BepInEx.dll
+{GameDirectory}/BepInEx/XUnity.AutoTranslator.Plugin.BepInEx.dll
+{GameDirectory}/BepInEx/XUnity.AutoTranslator.Plugin.ExtProtocol.dll
 {GameDirectory}/BepInEx/ExIni.dll
+{GameDirectory}/BepInEx/Translators/{Translator}.dll
 {GameDirectory}/BepInEx/Translation/AnyTranslationFile.txt (these files will be auto generated by plugin!)
 ```
 
@@ -254,9 +281,11 @@ REQUIRES: [IPA plugin manager](https://github.com/Eusth/IPA) (follow its install
 The file structure should like like this
 ```
 {GameDirectory}/Plugins/XUnity.AutoTranslator.Plugin.Core.dll
-{GameDirectory}/Plugins/XUnity.AutoTranslator.Plugin.Core.IPA.dll
+{GameDirectory}/Plugins/XUnity.AutoTranslator.Plugin.IPA.dll
+{GameDirectory}/Plugins/XUnity.AutoTranslator.Plugin.ExtProtocol.dll
 {GameDirectory}/Plugins/0Harmony.dll
 {GameDirectory}/Plugins/ExIni.dll
+{GameDirectory}/Plugins/Translators/{Translator}.dll
 {GameDirectory}/Plugins/Translation/AnyTranslationFile.txt (these files will be auto generated by plugin!)
  ```
 
@@ -269,9 +298,11 @@ REQUIRES: UnityInjector (follow its installation instructions first!).
 The file structure should like like this
 ```
 {GameDirectory}/UnityInjector/XUnity.AutoTranslator.Plugin.Core.dll
-{GameDirectory}/UnityInjector/XUnity.AutoTranslator.Plugin.Core.UnityInjector.dll
+{GameDirectory}/UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.dll
+{GameDirectory}/UnityInjector/XUnity.AutoTranslator.Plugin.ExtProtocol.dll
 {GameDirectory}/UnityInjector/0Harmony.dll
 {GameDirectory}/UnityInjector/ExIni.dll
+{GameDirectory}/UnityInjector/Translators/{Translator}.dll
 {GameDirectory}/UnityInjector/Translation/AnyTranslationFile.txt (these files will be auto generated by plugin!)
  ```
  
@@ -281,7 +312,7 @@ REQUIRES: Nothing, ReiPatcher is provided by this download.
  1. Download XUnity.AutoTranslator-ReiPatcher-{VERSION}.zip from [releases](../../releases).
  2. Extract directly into the game directory, such that "SetupReiPatcherAndAutoTranslator.exe" is placed alongside other exe files.
  3. Execute "SetupReiPatcherAndAutoTranslator.exe". This will setup up ReiPatcher correctly.
- 4. Execute the shortcut {GameExeName}.lnk that was created besides existing executables. This will patch and launch the game.
+ 4. Execute the shortcut {GameExeName} (Patch and Run).lnk that was created besides existing executables. This will patch and launch the game.
  5. From now on you can launch the game from the {GameExeName}.exe instead.
 
 The file structure should like like this
@@ -296,9 +327,11 @@ The file structure should like like this
 {GameDirectory}/ReiPatcher/ReiPatcher.exe
 {GameDirectory}/{GameExeName}_Data/Managed/ReiPatcher.exe
 {GameDirectory}/{GameExeName}_Data/Managed/XUnity.AutoTranslator.Plugin.Core.dll
+{GameDirectory}/{GameExeName}_Data/Managed/XUnity.AutoTranslator.Plugin.ExtProtocol.dll
 {GameDirectory}/{GameExeName}_Data/Managed/0Harmony.dll
 {GameDirectory}/{GameExeName}_Data/Managed/ExIni.dll
-{GameDirectory}/AutoTranslator/AnyTranslationFile.txt (these files will be auto generated by plugin!)
+{GameDirectory}/AutoTranslator/Translators/{Translator}.dll
+{GameDirectory}/AutoTranslator/Translation/AnyTranslationFile.txt (these files will be auto generated by plugin!)
  ```
 
 ## Translating Mods
@@ -354,7 +387,7 @@ TextureHashGenerationStrategy=FromImageName
 This is done through a hash-value that is stored in square brackets in each image file name, like this: `file_name [0223B639A2-6E698E9272].png`. This configuration specifies how these hash-values are generated:
  * `FromImageName` means that the hash is generated from the internal resource name that the game uses for the image, which may not exist for all images or even be unique. However, it is generally fairly reliable. If an image has no resource name, it will not be dumped.
  * `FromImageData` means that the hash is generated from the data stored in the image, which is guaranteed to exist for all images. However, generating the hash comes at a performance cost, that will also be incurred by the end-users.
- * `FromImageNameAndScene` means that it should use the name and scene to generate a hash. The name is still required for this to work. When using this option, there is a chance the same texture could be dumped with different hashes, which is undesirable, but it could be required for some games, if the name itself is not unique and the `FromImageData` option causes performance issues.
+ * `FromImageNameAndScene` means that it should use the name and scene to generate a hash. The name is still required for this to work. When using this option, there is a chance the same texture could be dumped with different hashes, which is undesirable, but it could be required for some games, if the name itself is not unique and the `FromImageData` option causes performance issues. If this is used, it is recommended to enable `EnableTextureScanOnSceneLoad` as well.
 
 There's an important catch you need to be aware when dealing with these options and that is if ANY of these options exists: `EnableTextureDumping=True`, `EnableTextureToggling=True`, `TextureHashGenerationStrategy=FromImageData`, then the game will need to read the raw data from all images it finds in game in order to replace the image and this is an expensive operation.
 
@@ -393,6 +426,8 @@ Here's how it works, and what is required:
     3. IMGUI: public static event Func<object, string, string> OnUnableToTranslateIMGUI
  * Also, the events can be either instance based or static.
 
+Be aware that if you do this, that the hooking functionality of the Auto Translator itself will be disabled. So you are entirely responsible for implementing the required hooks for the overriden text framework.
+
 ### Implementing a component that the Auto Translator should not interfere with
 As a mod author, you might not want the Auto Translator to interfere with your mods UI. If this is the case there's two ways to tell Auto Translator not to perform any translation:
  * If your UI is based on GameObjects, you can simply name your GameObjects containing the text element (for example Text class) to something that contains the string "XUAIGNORE". The Auto Translator will check for this and ignore components that contains the string.
@@ -431,3 +466,196 @@ public class MyPlugin : XPluginBase
 ```
 
 This approach requires version 2.15.0 or later!
+
+## Implementing a Translator
+Since version 3.0.0, you can now also implement your own translators.
+
+In order to do so, all you have to do is implement the following interface, build the assembly and place the generated DLL in the `Translators` folder.
+
+```C#
+/// <summary>
+/// The interface that must be implemented by a translator.
+/// </summary>
+public interface ITranslateEndpoint
+{
+   /// <summary>
+   /// Gets the id of the ITranslateEndpoint that is used as a configuration parameter.
+   /// </summary>
+   string Id { get; }
+
+   /// <summary>
+   /// Gets a friendly name that can be displayed to the user representing the plugin.
+   /// </summary>
+   string FriendlyName { get; }
+
+   /// <summary>
+   /// Gets the maximum concurrency for the endpoint. This specifies how many times "Translate"
+   /// can be called before it returns.
+   /// </summary>
+   int MaxConcurrency { get; }
+
+   /// <summary>
+   /// Gets the maximum number of translations that can be served per translation request.
+   /// </summary>
+   int MaxTranslationsPerRequest { get; }
+
+   /// <summary>
+   /// Called during initialization. Use this to initialize plugin or throw exception if impossible.
+   /// </summary>
+   void Initialize( IInitializationContext context );
+
+   /// <summary>
+   /// Attempt to translated the provided untranslated text. Will be used in a "coroutine",
+   /// so it can be implemented in an asynchronous fashion.
+   /// </summary>
+   IEnumerator Translate( ITranslationContext context );
+}
+```
+
+Often an implementation of this interface will access an external web service. If this is the case, you do not need to implement the entire interface yourself. Instead you can rely on a base class in the `XUnity.AutoTranslator.Plugin.Core` assembly. But more on this later.
+
+### Important Notes on Implementing a Translator based on an Online Service
+Whenever you implement a translator based on an online service, it is important to not use it in an abusive way. For example by:
+ * Establishing a large number of connections to it
+ * Performing web scraping instead of using an available API
+ * Making concurrent requests towards it
+ * *This is especially important if the service is not authenticated*
+
+With that in mind, consider the following:
+ * The `WWW` class in Unity establishes a new TCP connection on each request you make, making it extremely poor at this kind of job. Especially if SSL (https) is involved because it has to do the entire handshake procedure each time. Yuck.
+ * The `UnityWebRequest` class in Unity does not exist in most games, because the authors use an old engine, so it is not a good choice either.
+ * The `WebClient` class from .NET is capable of using persistent connections (it does so by default), but has its own problems with SSL. The version of Mono used in most Unity games rejects all certificates by default making all HTTPS connections fail. This, however, can be remedied during the initialization phase of the translator (see examples below). Another shortcoming of this API is the fact that the runtime will never release the TCP connections it has used until the process ends. The API also integrates terribly with Unity because callbacks return on a background thread.
+ * The `WebRequest` class from .NET is essentially the same as WebClient.
+ * The `HttpClient` class from .NET is also unlikely to exist in most Unity games.
+
+None of these are therefore an ideal solution.
+
+To remedy this, the plugin implements a class `XUnityWebClient`, which is based on Mono's version of WebClient. However, it adds the following features:
+ * Enables integration with Unity by returning result classes that can be 'yielded'.
+ * Properly closes connections that has not been used for 50 seconds.
+
+I recommend using this class, or in case that cannot be used, falling back to the .NET 'WebClient'.
+
+### How-To
+Follow these steps:
+ 1. Download XUnity.AutoTranslator-Developer-{VERSION}.zip from [releases](../../releases)
+ 2. Start a new project (.NET 3.5) in Visual Studio 2017 or later. I recommend using the same name for your assembly/project as the "Id" you are going to use in your interface implementation. This makes it easier for users to know how to configure your translator
+    * I recommend using the "Class Library (.NET Standard)" and simply editing the generated .csproj file to use 'net35' instead of 'netstandard2.0'. This generates much cleaner .csproj files.
+ 3. Add a reference to the XUnity.AutoTranslator.Plugin.Core.dll that you downloaded in step 1
+ 4. You do not need to directly reference the UnityEngine.dll assembly. This is good, because you do not need to worry about which version of Unity is used.
+    * If you do need a reference to this assembly (because you need functionality from it) consider using an old version of it (if `UnityEngine.CoreModule.dll` exists in the Managed folder, it is not an old version!)
+ 5. Create a new class that either:
+    * Implements the `ITranslateEndpoint` interface
+    * Inherits from the `HttpEndpoint` class
+    * Inherits from the `WwwEndpoint` class
+    * Inherits from the `ExtProtocolEndpoint` class
+
+Here's an example that simply reverses the text and also reads some configuration from the configuration file the plugin uses:
+
+```C#
+public class ReverseTranslatorEndpoint : ITranslateEndpoint
+{
+   private bool _myConfig;
+
+   public string Id => "ReverseTranslator";
+
+   public string FriendlyName => "Reverser";
+
+   public int MaxConcurrency => 50;
+
+   public int MaxTranslationsPerRequest => 1;
+
+   public void Initialize( IInitializationContext context )
+   {
+      _myConfig = context.GetOrCreateSetting( "Reverser", "MyConfig", true );
+   }
+
+   public IEnumerator Translate( ITranslationContext context )
+   {
+      var reversedText = new string( context.UntranslatedText.Reverse().ToArray() );
+      context.Complete( reversedText );
+
+      return null;
+   }
+}
+```
+
+Arguably, this is not a particularly interesting example, but it illustrates the basic principles of what must be done in order to implement a Translator.
+
+Let's take a look at a more advanced example that accesses the web:
+
+```C#
+internal class YandexTranslateEndpoint : HttpEndpoint
+{
+   private static readonly HashSet<string> SupportedLanguages = new HashSet<string> { "az", "sq", "am", "en", "ar", "hy", "af", "eu", "ba", "be", "bn", "my", "bg", "bs", "cy", "hu", "vi", "ht", "gl", "nl", "mrj", "el", "ka", "gu", "da", "he", "yi", "id", "ga", "it", "is", "es", "kk", "kn", "ca", "ky", "zh", "ko", "xh", "km", "lo", "la", "lv", "lt", "lb", "mg", "ms", "ml", "mt", "mk", "mi", "mr", "mhr", "mn", "de", "ne", "no", "pa", "pap", "fa", "pl", "pt", "ro", "ru", "ceb", "sr", "si", "sk", "sl", "sw", "su", "tg", "th", "tl", "ta", "tt", "te", "tr", "udm", "uz", "uk", "ur", "fi", "fr", "hi", "hr", "cs", "sv", "gd", "et", "eo", "jv", "ja" };
+   private static readonly string HttpsServicePointTemplateUrl = "https://translate.yandex.net/api/v1.5/tr.json/translate?key={3}&text={2}&lang={0}-{1}&format=plain";
+
+   private string _key;
+
+   public override string Id => "YandexTranslate";
+
+   public override string FriendlyName => "Yandex Translate";
+
+   public override void Initialize( IInitializationContext context )
+   {
+      _key = context.GetOrCreateSetting( "Yandex", "YandexAPIKey", "" );
+      context.DisableCertificateChecksFor( "translate.yandex.net" );
+
+      // if the plugin cannot be enabled, simply throw so the user cannot select the plugin
+      if( string.IsNullOrEmpty( _key ) ) throw new Exception( "The YandexTranslate endpoint requires an API key which has not been provided." );
+      if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+      if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
+   }
+
+   public override void OnCreateRequest( IHttpRequestCreationContext context )
+   {
+      var request = new XUnityWebRequest(
+         string.Format(
+            HttpsServicePointTemplateUrl,
+            context.SourceLanguage,
+            context.DestinationLanguage,
+            WwwHelper.EscapeUrl( context.UntranslatedText ),
+            _key ) );
+         
+      request.Headers[ HttpRequestHeader.Accept ] = "*/*";
+      request.Headers[ HttpRequestHeader.AcceptCharset ] = "UTF-8";
+
+      context.Complete( request );
+   }
+
+   public override void OnExtractTranslation( IHttpTranslationExtractionContext context )
+   {
+      var data = context.Response.Data;
+      var obj = JSON.Parse( data );
+
+      var code = obj.AsObject[ "code" ].ToString();
+      if( code != "200" ) context.Fail( "Received bad response code: " + code );
+
+      var token = obj.AsObject[ "text" ].ToString();
+      var translation = JsonHelper.Unescape( token.Substring( 2, token.Length - 4 ) );
+
+      if( string.IsNullOrEmpty( translation ) ) context.Fail( "Received no translation." );
+
+      context.Complete( translation );
+   }
+}
+```
+
+This plugin extends from `HttpEndpoint`. Let's look at the three methods it overrides:
+ * `Initialize` is used to read the API key the user has configured. In addition it calls `context.DisableCertificateChecksFor( "translate.yandex.net" )` in order to disable the certificate check for this specific hostname. If this is neglected, SSL will fail in most versions of Unity. Finally, it throws an exception if the plugin cannot be used with the specified configuration.
+ * `OnCreateRequest` is used to construct the `XUnityWebRequest` object that will be sent to the external endpoint. The call to `context.Complete( request )` specifies the request to use.
+ * `OnExtractTranslation` is used to extract the text from the response returned from the web server.
+
+As you can see, the `XUnityWebClient` class is not even used. We simply specify a request object that the `HttpEndpoint` will use internally to perform the request.
+
+After implementing the class, simply build the project and place the generated DLL file in the "Translators" directory of the plugin folder. That's it.
+
+For more examples of implementations, you can simply take a look at this projects source code.
+
+**NOTE**: If you implement a class based on the `HttpEndpoint` and you get an error where the web request is never completed, then it is likely due to the web server requiring Tls1.2. Unity-mono has issues with this spec and it will cause the request to lock up forever. The only solutions to this for now are:
+ * Disable SSL, if you can. There are many situations where it is simply not possible to do this because the web server will simply redirect back to the HTTPS endoint.
+ * Use the `WwwEndpoint` instead. I highly advice against this though, unless it is an authenticated endpoint.
+
+Another way to implement a translator is to implement the `ExtProtocolEndpoint` class. This can be used to delegate the actual translation logic to an external process. Currently there is no documentation on this, but you can take a look at the LEC implementation, which uses it.
+
+If instead, you use the interface directly, it is also possible to extend from MonoBehaviour to get access to all the normal lifecycle callbacks of Unity components.

+ 117 - 3
XUnity.AutoTranslator.sln

@@ -8,23 +8,66 @@ EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.Core", "src\XUnity.AutoTranslator.Plugin.Core\XUnity.AutoTranslator.Plugin.Core.csproj", "{718A3B1D-A5E5-4223-AD53-45C60C874150}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.BepIn", "src\XUnity.AutoTranslator.Plugin.BepIn\XUnity.AutoTranslator.Plugin.BepIn.csproj", "{5442ED94-2800-47A4-BBAC-C00FBA676D02}"
+	ProjectSection(ProjectDependencies) = postProject
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4} = {848D741E-A4F2-4680-AFD2-74C8723CE3C4}
+		{1C574D77-FCC8-4249-8890-0B00EF092705} = {1C574D77-FCC8-4249-8890-0B00EF092705}
+	EndProjectSection
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.IPA", "src\XUnity.AutoTranslator.Plugin.IPA\XUnity.AutoTranslator.Plugin.IPA.csproj", "{C749698C-9E49-4CC3-8B45-62AE3AD0C938}"
+	ProjectSection(ProjectDependencies) = postProject
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4} = {848D741E-A4F2-4680-AFD2-74C8723CE3C4}
+		{1C574D77-FCC8-4249-8890-0B00EF092705} = {1C574D77-FCC8-4249-8890-0B00EF092705}
+	EndProjectSection
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Patcher", "src\XUnity.AutoTranslator.Patcher\XUnity.AutoTranslator.Patcher.csproj", "{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Setup", "src\XUnity.AutoTranslator.Setup\XUnity.AutoTranslator.Setup.csproj", "{86BF1F46-44C1-4301-8314-6EC32F74575F}"
 	ProjectSection(ProjectDependencies) = postProject
-		{718A3B1D-A5E5-4223-AD53-45C60C874150} = {718A3B1D-A5E5-4223-AD53-45C60C874150}
-		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD} = {0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4} = {848D741E-A4F2-4680-AFD2-74C8723CE3C4}
+		{1C574D77-FCC8-4249-8890-0B00EF092705} = {1C574D77-FCC8-4249-8890-0B00EF092705}
+	EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.UnityInjector", "src\XUnity.AutoTranslator.Plugin.UnityInjector\XUnity.AutoTranslator.Plugin.UnityInjector.csproj", "{12F1D16B-B8E1-4A9D-B65A-044650F15440}"
+	ProjectSection(ProjectDependencies) = postProject
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4} = {848D741E-A4F2-4680-AFD2-74C8723CE3C4}
+		{1C574D77-FCC8-4249-8890-0B00EF092705} = {1C574D77-FCC8-4249-8890-0B00EF092705}
 	EndProjectSection
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnity.AutoTranslator.Plugin.UnityInjector", "src\XUnity.AutoTranslator.Plugin.UnityInjector\XUnity.AutoTranslator.Plugin.UnityInjector.csproj", "{12F1D16B-B8E1-4A9D-B65A-044650F15440}"
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{ACADAE2C-1642-428A-84D4-CC53E24F1348}"
 	ProjectSection(SolutionItems) = preProject
 		.editorconfig = .editorconfig
 	EndProjectSection
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Translators", "Translators", "{7A01BA34-3B96-4910-AC70-462BA59417CB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReverseTranslator", "src\Translators\ReverseTranslator\ReverseTranslator.csproj", "{7493BA4A-C688-4103-B161-7E578FBB6C0E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GoogleTranslate", "src\Translators\GoogleTranslate\GoogleTranslate.csproj", "{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BingTranslate", "src\Translators\BingTranslate\BingTranslate.csproj", "{95C83913-8D7F-4FA1-877E-288251A2A461}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BaiduTranslate", "src\Translators\BaiduTranslate\BaiduTranslate.csproj", "{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BingTranslateLegitimate", "src\Translators\BingTranslateLegitimate\BingTranslateLegitimate.csproj", "{F49BA6D1-FAFB-414C-97B1-28B77FC21286}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomTranslate", "src\Translators\CustomTranslate\CustomTranslate.csproj", "{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YandexTranslate", "src\Translators\YandexTranslate\YandexTranslate.csproj", "{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WatsonTranslate", "src\Translators\WatsonTranslate\WatsonTranslate.csproj", "{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LecPowerTranslator15", "src\Translators\LecPowerTranslator15\LecPowerTranslator15.csproj", "{AE28F88E-E877-456B-98AB-BD03A59A3E44}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lec.ExtProtocol", "src\Translators\Lec.ExtProtocol\Lec.ExtProtocol.csproj", "{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GoogleTranslateLegitimate", "src\Translators\GoogleTranslateLegitimate\GoogleTranslateLegitimate.csproj", "{41B612A5-2974-4988-A82E-84EEF2212A61}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.ExtProtocol", "src\XUnity.AutoTranslator.Plugin.ExtProtocol\XUnity.AutoTranslator.Plugin.ExtProtocol.csproj", "{65500234-7AD4-48B8-B51B-945ADBFF1133}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnity.AutoTranslator.Setup.Build", "src\XUnity.AutoTranslator.Setup.Build\XUnity.AutoTranslator.Setup.Build.csproj", "{848D741E-A4F2-4680-AFD2-74C8723CE3C4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnity.AutoTranslator.Setup.Build-x86", "src\XUnity.AutoTranslator.Setup.Build-x86\XUnity.AutoTranslator.Setup.Build-x86.csproj", "{1C574D77-FCC8-4249-8890-0B00EF092705}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -55,6 +98,62 @@ Global
 		{12F1D16B-B8E1-4A9D-B65A-044650F15440}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{12F1D16B-B8E1-4A9D-B65A-044650F15440}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{12F1D16B-B8E1-4A9D-B65A-044650F15440}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{95C83913-8D7F-4FA1-877E-288251A2A461}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{95C83913-8D7F-4FA1-877E-288251A2A461}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{95C83913-8D7F-4FA1-877E-288251A2A461}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{95C83913-8D7F-4FA1-877E-288251A2A461}.Release|Any CPU.Build.0 = Release|Any CPU
+		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Release|Any CPU.Build.0 = Release|Any CPU
+		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Release|Any CPU.Build.0 = Release|Any CPU
+		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Release|Any CPU.Build.0 = Release|Any CPU
+		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Release|Any CPU.Build.0 = Release|Any CPU
+		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Debug|Any CPU.ActiveCfg = Debug|x86
+		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Debug|Any CPU.Build.0 = Debug|x86
+		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Release|Any CPU.ActiveCfg = Release|x86
+		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Release|Any CPU.Build.0 = Release|x86
+		{41B612A5-2974-4988-A82E-84EEF2212A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{41B612A5-2974-4988-A82E-84EEF2212A61}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{41B612A5-2974-4988-A82E-84EEF2212A61}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{41B612A5-2974-4988-A82E-84EEF2212A61}.Release|Any CPU.Build.0 = Release|Any CPU
+		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Release|Any CPU.Build.0 = Release|Any CPU
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{1C574D77-FCC8-4249-8890-0B00EF092705}.Debug|Any CPU.ActiveCfg = Debug|x86
+		{1C574D77-FCC8-4249-8890-0B00EF092705}.Debug|Any CPU.Build.0 = Debug|x86
+		{1C574D77-FCC8-4249-8890-0B00EF092705}.Release|Any CPU.ActiveCfg = Release|x86
+		{1C574D77-FCC8-4249-8890-0B00EF092705}.Release|Any CPU.Build.0 = Release|x86
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -66,6 +165,21 @@ Global
 		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
 		{86BF1F46-44C1-4301-8314-6EC32F74575F} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
 		{12F1D16B-B8E1-4A9D-B65A-044650F15440} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{7A01BA34-3B96-4910-AC70-462BA59417CB} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{7493BA4A-C688-4103-B161-7E578FBB6C0E} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
+		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
+		{95C83913-8D7F-4FA1-877E-288251A2A461} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
+		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
+		{F49BA6D1-FAFB-414C-97B1-28B77FC21286} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
+		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
+		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
+		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
+		{AE28F88E-E877-456B-98AB-BD03A59A3E44} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
+		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
+		{41B612A5-2974-4988-A82E-84EEF2212A61} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
+		{65500234-7AD4-48B8-B51B-945ADBFF1133} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{1C574D77-FCC8-4249-8890-0B00EF092705} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {EE803FED-4447-4D19-B3D6-88C56E8DFCCA}

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

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

+ 100 - 0
src/Translators/BaiduTranslate/BaiduTranslateEndpoint.cs

@@ -0,0 +1,100 @@
+using System;
+using System.Net;
+using System.Security.Cryptography;
+using System.Text;
+using SimpleJSON;
+using XUnity.AutoTranslator.Plugin.Core;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints.Http;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace BaiduTranslate
+{
+   internal class BaiduTranslateEndpoint : HttpEndpoint
+   {
+      private static readonly string HttpServicePointTemplateUrl = "http://api.fanyi.baidu.com/api/trans/vip/translate?q={0}&from={1}&to={2}&appid={3}&salt={4}&sign={5}";
+      private static readonly MD5 HashMD5 = MD5.Create();
+
+      private string _appId;
+      private string _appSecret;
+
+      public override string Id => "BaiduTranslate";
+
+      public override string FriendlyName => "Baidu Translator";
+
+      public override void Initialize( IInitializationContext context )
+      {
+         _appId = context.GetOrCreateSetting( "Baidu", "BaiduAppId", "" );
+         _appSecret = context.GetOrCreateSetting( "Baidu", "BaiduAppSecret", "" );
+         if( string.IsNullOrEmpty( _appId ) ) throw new ArgumentException( "The BaiduTranslate endpoint requires an App Id which has not been provided." );
+         if( string.IsNullOrEmpty( _appSecret ) ) throw new ArgumentException( "The BaiduTranslate endpoint requires an App Secret which has not been provided." );
+
+         context.DisableCertificateChecksFor( "api.fanyi.baidu.com" );
+
+         // frankly, I have no idea what languages this does, or does not support...
+      }
+
+      public override void OnCreateRequest( IHttpRequestCreationContext context )
+      {
+         string salt = DateTime.UtcNow.Millisecond.ToString();
+         var md5 = CreateMD5( _appId + context.UntranslatedText + salt + _appSecret );
+
+         var request = new XUnityWebRequest(
+            string.Format(
+               HttpServicePointTemplateUrl,
+               WwwHelper.EscapeUrl( context.UntranslatedText ),
+               context.SourceLanguage,
+               context.DestinationLanguage,
+               _appId,
+               salt,
+               md5 ) );
+
+         request.Headers[ HttpRequestHeader.UserAgent ] = string.IsNullOrEmpty( AutoTranslatorSettings.UserAgent ) ? UserAgents.Chrome_Win10_Latest : AutoTranslatorSettings.UserAgent;
+         request.Headers[ HttpRequestHeader.AcceptCharset ] = "UTF-8";
+
+         context.Complete( request );
+      }
+
+      public override void OnExtractTranslation( IHttpTranslationExtractionContext context )
+      {
+         var data = context.Response.Data;
+         if( data.StartsWith( "{\"error" ) )
+         {
+            return;
+         }
+         
+         var obj = JSON.Parse( data );
+         var lineBuilder = new StringBuilder( data.Length );
+
+         foreach( JSONNode entry in obj.AsObject[ "trans_result" ].AsArray )
+         {
+            var token = entry.AsObject[ "dst" ].ToString();
+            token = JsonHelper.Unescape( token.Substring( 1, token.Length - 2 ) );
+
+            if( !lineBuilder.EndsWithWhitespaceOrNewline() ) lineBuilder.Append( "\n" );
+
+            lineBuilder.Append( token );
+         }
+
+         var translated = lineBuilder.ToString();
+
+         context.Complete( translated );
+      }
+
+      private static string CreateMD5( string input )
+      {
+         byte[] inputBytes = Encoding.UTF8.GetBytes( input );
+         byte[] hashBytes = HashMD5.ComputeHash( inputBytes );
+
+         StringBuilder sb = new StringBuilder();
+         for( int i = 0 ; i < hashBytes.Length ; i++ )
+         {
+            sb.Append( hashBytes[ i ].ToString( "X2" ) );
+         }
+         return sb.ToString().ToLower();
+      }
+   }
+}

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

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

+ 255 - 0
src/Translators/BingTranslate/BingTranslateEndpoint.cs

@@ -0,0 +1,255 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Text;
+using SimpleJSON;
+using XUnity.AutoTranslator.Plugin.Core;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints.Http;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace BingTranslate
+{
+   internal class BingTranslateEndpoint : HttpEndpoint
+   {
+      private static readonly HashSet<string> SupportedLanguages = new HashSet<string>
+      {
+         "af","ar","bn","bs","bg","yue","ca","zh-Hans","zh-Hant","hr","cs","da","nl","en","et","fj","fil","fi","fr","de","el","ht","he","hi","mww","hu","is","id","it","ja","sw","tlh","tlh-Qaak","ko","lv","lt","mg","ms","mt","nb","fa","pl","pt","otq","ro","ru","sm","sr-Cyrl","sr-Latn","sk","sl","es","sv","ty","ta","te","th","to","tr","uk","ur","vi","cy","yua"
+      };
+
+      private static readonly string HttpsServicePointTemplateUrl = "https://www.bing.com/ttranslate?&category=&IG={0}&IID={1}.{2}";
+      private static readonly string HttpsServicePointTemplateUrlWithoutIG = "https://www.bing.com/ttranslate?&category=";
+      private static readonly string HttpsTranslateUserSite = "https://www.bing.com/translator";
+      private static readonly string RequestTemplate = "&text={0}&from={1}&to={2}";
+      private static readonly Random RandomNumbers = new Random();
+
+      private static readonly string[] Accepts = new string[] { "*/*" };
+      private static readonly string[] AcceptLanguages = new string[] { null, "en-US,en;q=0.9", "en-US", "en" };
+      private static readonly string[] Referers = new string[] { "https://bing.com/translator" };
+      private static readonly string[] Origins = new string[] { "https://www.bing.com" };
+      private static readonly string[] AcceptCharsets = new string[] { null, Encoding.UTF8.WebName };
+      private static readonly string[] ContentTypes = new string[] { "application/x-www-form-urlencoded" };
+
+      private static readonly string Accept = Accepts[ RandomNumbers.Next( Accepts.Length ) ];
+      private static readonly string AcceptLanguage = AcceptLanguages[ RandomNumbers.Next( AcceptLanguages.Length ) ];
+      private static readonly string Referer = Referers[ RandomNumbers.Next( Referers.Length ) ];
+      private static readonly string Origin = Origins[ RandomNumbers.Next( Origins.Length ) ];
+      private static readonly string AcceptCharset = AcceptCharsets[ RandomNumbers.Next( AcceptCharsets.Length ) ];
+      private static readonly string ContentType = ContentTypes[ RandomNumbers.Next( ContentTypes.Length ) ];
+
+      private CookieContainer _cookieContainer;
+      private bool _hasSetup = false;
+      private string _ig;
+      private string _iid;
+      private int _translationCount = 0;
+      private int _resetAfter = RandomNumbers.Next( 75, 125 );
+
+      public BingTranslateEndpoint()
+      {
+         _cookieContainer = new CookieContainer();
+      }
+
+      public override string Id => "BingTranslate";
+
+      public override string FriendlyName => "Bing Translator";
+
+      public override void Initialize( IInitializationContext context )
+      {
+         // Configure service points / service point manager
+         context.DisableCertificateChecksFor( "www.bing.com" );
+
+         if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
+      }
+
+      public override IEnumerator OnBeforeTranslate( IHttpTranslationContext context )
+      {
+         if( !_hasSetup || _translationCount % _resetAfter == 0 )
+         {
+            _resetAfter = RandomNumbers.Next( 75, 125 );
+            _hasSetup = true;
+
+            // Setup TKK and cookies
+            var enumerator = SetupIGAndIID();
+            while( enumerator.MoveNext() ) yield return enumerator.Current;
+         }
+      }
+
+      public override void OnCreateRequest( IHttpRequestCreationContext context )
+      {
+         _translationCount++;
+
+         string address = null;
+         if( _ig == null || _iid == null )
+         {
+            address = HttpsServicePointTemplateUrlWithoutIG;
+         }
+         else
+         {
+            address = string.Format( HttpsServicePointTemplateUrl, _ig, _iid, _translationCount );
+         }
+
+         var data = string.Format(
+            RequestTemplate,
+            Uri.EscapeDataString( context.UntranslatedText ),
+            context.SourceLanguage,
+            context.DestinationLanguage );
+
+         var request = new XUnityWebRequest( "POST", address, data );
+
+         request.Cookies = _cookieContainer;
+         AddHeaders( request, true );
+
+         context.Complete( request );
+      }
+
+      public override void OnInspectResponse( IHttpResponseInspectionContext context )
+      {
+         InspectResponse( context.Response );
+      }
+
+      public override void OnExtractTranslation( IHttpTranslationExtractionContext context )
+      {
+         var obj = JSON.Parse( context.Response.Data );
+
+         var code = obj[ "statusCode" ].AsInt;
+         if( code != 200 ) context.Fail( "Bad response code received from service: " + code );
+
+         var token = obj[ "translationResponse" ].ToString();
+         var translatedText = JsonHelper.Unescape( token.Substring( 1, token.Length - 2 ) );
+
+         context.Complete( translatedText );
+      }
+
+      private XUnityWebRequest CreateWebSiteRequest()
+      {
+         var request = new XUnityWebRequest( HttpsTranslateUserSite );
+
+         request.Cookies = _cookieContainer;
+         AddHeaders( request, false );
+
+         return request;
+      }
+
+      private void AddHeaders( XUnityWebRequest request, bool isTranslationRequest )
+      {
+         request.Headers[ HttpRequestHeader.UserAgent ] = string.IsNullOrEmpty( AutoTranslatorSettings.UserAgent ) ? UserAgents.Chrome_Win10_Latest : AutoTranslatorSettings.UserAgent;
+
+         if( AcceptLanguage != null )
+         {
+            request.Headers[ HttpRequestHeader.AcceptLanguage ] = AcceptLanguage;
+         }
+         if( Accept != null )
+         {
+            request.Headers[ HttpRequestHeader.Accept ] = Accept;
+         }
+         if( Referer != null && isTranslationRequest )
+         {
+            request.Headers[ HttpRequestHeader.Referer ] = Referer;
+         }
+         if( Origin != null && isTranslationRequest )
+         {
+            request.Headers[ "Origin" ] = Origin;
+         }
+         if( AcceptCharset != null )
+         {
+            request.Headers[ HttpRequestHeader.AcceptCharset ] = AcceptCharset;
+         }
+         if( ContentType != null && isTranslationRequest )
+         {
+            request.Headers[ HttpRequestHeader.ContentType ] = ContentType;
+         }
+      }
+
+      private void InspectResponse( XUnityWebResponse response )
+      {
+         CookieCollection cookies = response.NewCookies;
+
+         // FIXME: Is this needed? Should already be added
+         _cookieContainer.Add( cookies );
+      }
+
+      public IEnumerator SetupIGAndIID()
+      {
+         XUnityWebResponse response = null;
+
+         _cookieContainer = new CookieContainer();
+         _translationCount = 0;
+
+         try
+         {
+            var client = new XUnityWebClient();
+            var request = CreateWebSiteRequest();
+            response = client.Send( request );
+         }
+         catch( Exception e )
+         {
+            XuaLogger.Current.Warn( e, "An error occurred while setting up BingTranslate IG. Proceeding without..." );
+            yield break;
+         }
+
+         var iterator = response.GetSupportedEnumerator();
+         while( iterator.MoveNext() ) yield return iterator.Current;
+
+         InspectResponse( response );
+
+         // failure
+         if( response.Error != null )
+         {
+            XuaLogger.Current.Warn( response.Error, "An error occurred while setting up BingTranslate IG. Proceeding without..." );
+            yield break;
+         }
+
+         // failure
+         if( response.Data == null )
+         {
+            XuaLogger.Current.Warn( null, "An error occurred while setting up BingTranslate IG. Proceeding without..." );
+            yield break;
+         }
+
+         try
+         {
+            var html = response.Data;
+
+            _ig = Lookup( "ig\":\"", html );
+            _iid = Lookup( ".init(\"/feedback/submission?\",\"", html );
+
+            if( _ig == null || _iid == null )
+            {
+               XuaLogger.Current.Warn( "An error occurred while setting up BingTranslate IG/IID. Proceeding without..." );
+            }
+         }
+         catch( Exception e )
+         {
+            XuaLogger.Current.Warn( e, "An error occurred while setting up BingTranslate IG. Proceeding without..." );
+         }
+      }
+
+      private string Lookup( string lookFor, string html )
+      {
+         var index = html.IndexOf( lookFor );
+         if( index > -1 ) // simple string approach
+         {
+            var startIndex = index + lookFor.Length;
+            var endIndex = html.IndexOf( "\"", startIndex );
+
+            if( startIndex > -1 && endIndex > -1 )
+            {
+               var result = html.Substring( startIndex, endIndex - startIndex );
+
+               return result;
+            }
+         }
+         return null;
+      }
+   }
+}

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

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

+ 110 - 0
src/Translators/BingTranslateLegitimate/BingTranslateLegitimateEndpoint.cs

@@ -0,0 +1,110 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Reflection;
+using System.Text;
+using SimpleJSON;
+using XUnity.AutoTranslator.Plugin.Core;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints.Http;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace BingTranslateLegitimate
+{
+   internal class BingTranslateLegitimateEndpoint : HttpEndpoint
+   {
+      private static readonly HashSet<string> SupportedLanguages = new HashSet<string>
+      {
+         "af","ar","bn","bs","bg","yue","ca","zh-Hans","zh-Hant","hr","cs","da","nl","en","et","fj","fil","fi","fr","de","el","ht","he","hi","mww","hu","is","id","it","ja","sw","tlh","tlh-Qaak","ko","lv","lt","mg","ms","mt","nb","fa","pl","pt","otq","ro","ru","sm","sr-Cyrl","sr-Latn","sk","sl","es","sv","ty","ta","te","th","to","tr","uk","ur","vi","cy","yua"
+      };
+
+      private static readonly string HttpsServicePointTemplateUrl = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from={0}&to={1}";
+      private static readonly Random RandomNumbers = new Random();
+
+      private static readonly string[] Accepts = new string[] { "application/json" };
+      private static readonly string[] ContentTypes = new string[] { "application/json" };
+
+      private static readonly string Accept = Accepts[ RandomNumbers.Next( Accepts.Length ) ];
+      private static readonly string ContentType = ContentTypes[ RandomNumbers.Next( ContentTypes.Length ) ];
+
+      private string _key;
+
+      public override string Id => "BingTranslateLegitimate";
+
+      public override string FriendlyName => "Bing Translator (Authenticated)";
+
+      public override int MaxTranslationsPerRequest => 10;
+
+      public override void Initialize( IInitializationContext context )
+      {
+         _key = context.GetOrCreateSetting( "BingLegitimate", "OcpApimSubscriptionKey", "" );
+         if( string.IsNullOrEmpty( _key ) ) throw new Exception( "The BingTranslateLegitimate endpoint requires an API key which has not been provided." );
+
+         // Configure service points / service point manager
+         context.DisableCertificateChecksFor( "api.cognitive.microsofttranslator.com" );
+
+         if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
+      }
+
+      public override void OnCreateRequest( IHttpRequestCreationContext context )
+      {
+         StringBuilder data = new StringBuilder();
+         data.Append( "[" );
+         for( int i = 0 ; i < context.UntranslatedTexts.Length ; i++ )
+         {
+            var untranslatedText = JsonHelper.Escape( context.UntranslatedTexts[ i ] );
+            data.Append( "{\"Text\":\"" );
+            data.Append( untranslatedText );
+            data.Append( "\"}" );
+
+            if( context.UntranslatedTexts.Length - 1 != i )
+            {
+               data.Append( "," );
+            }
+         }
+         data.Append( "]" );
+
+
+         var request = new XUnityWebRequest(
+            "POST",
+            string.Format( HttpsServicePointTemplateUrl, context.SourceLanguage, context.DestinationLanguage ),
+            data.ToString() );
+
+         if( Accept != null )
+         {
+            request.Headers[ HttpRequestHeader.Accept ] = Accept;
+         }
+         if( ContentType != null )
+         {
+            request.Headers[ HttpRequestHeader.ContentType ] = ContentType;
+         }
+         request.Headers[ "Ocp-Apim-Subscription-Key" ] = _key;
+
+         context.Complete( request );
+      }
+
+      public override void OnExtractTranslation( IHttpTranslationExtractionContext context )
+      {
+         var arr = JSON.Parse( context.Response.Data ).AsArray;
+
+         var translations = new List<string>();
+         for( int i = 0 ; i < arr.Count ; i++ )
+         {
+            var token = arr[ i ].AsObject[ "translations" ]?.AsArray[ 0 ]?.AsObject[ "text" ]?.ToString();
+            var translation = JsonHelper.Unescape( token.Substring( 1, token.Length - 2 ) );
+
+            translations.Add( translation );
+         }
+
+         context.Complete( translations.ToArray() );
+      }
+   }
+}

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

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

+ 57 - 0
src/Translators/CustomTranslate/CustomTranslateEndpoint.cs

@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints.Http;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace CustomTranslate
+{
+   internal class CustomTranslateEndpoint : HttpEndpoint
+   {
+      private static readonly string ServicePointTemplateUrl = "{0}?from={1}&to={2}&text={3}";
+      private string _endpoint;
+      private string _friendlyName;
+
+      public CustomTranslateEndpoint()
+      {
+         _friendlyName = "Custom";
+      }
+
+      public override string Id => "CustomTranslate";
+
+      public override string FriendlyName => _friendlyName;
+
+      public override void Initialize( IInitializationContext context )
+      {
+         _endpoint = context.GetOrCreateSetting( "Custom", "Url", "" );
+         if( string.IsNullOrEmpty( _endpoint ) ) throw new ArgumentException( "The custom endpoint requires a url which has not been provided." );
+
+         var uri = new Uri( _endpoint );
+         context.DisableCertificateChecksFor( uri.Host );
+
+         _friendlyName += " (" + uri.Host + ")";
+      }
+
+      public override void OnCreateRequest( IHttpRequestCreationContext context )
+      {
+         var request = new XUnityWebRequest(
+            string.Format(
+               ServicePointTemplateUrl,
+               _endpoint,
+               context.SourceLanguage,
+               context.DestinationLanguage,
+               Uri.EscapeDataString( context.UntranslatedText ) ) );
+
+         context.Complete( request );
+      }
+
+      public override void OnExtractTranslation( IHttpTranslationExtractionContext context )
+      {
+         context.Complete( context.Response.Data );
+      }
+   }
+}

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

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

+ 369 - 0
src/Translators/GoogleTranslate/GoogleTranslateEndpoint.cs

@@ -0,0 +1,369 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Reflection;
+using System.Text;
+using SimpleJSON;
+using XUnity.AutoTranslator.Plugin.Core;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints.Http;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace GoogleTranslate
+{
+   internal class GoogleTranslateEndpoint : HttpEndpoint
+   {
+      private static readonly HashSet<string> SupportedLanguages = new HashSet<string>
+      {
+         "romaji","af","sq","am","ar","hy","az","eu","be","bn","bs","bg","ca","ceb","zh-CN","zh-TW","co","hr","cs","da","nl","en","eo","et","fi","fr","fy","gl","ka","de","el","gu","ht","ha","haw","he","hi","hmn","hu","is","ig","id","ga","it","ja","jw","kn","kk","km","ko","ku","ky","lo","la","lv","lt","lb","mk","mg","ms","ml","mt","mi","mr","mn","my","ne","no","ny","ps","fa","pl","pt","pa","ro","ru","sm","gd","sr","st","sn","sd","si","sk","sl","so","es","su","sw","sv","tl","tg","ta","te","th","tr","uk","ur","uz","vi","cy","xh","yi","yo","zu"
+      };
+
+      private static readonly string HttpsServicePointTranslateTemplateUrl = "https://translate.googleapis.com/translate_a/single?client=webapp&sl={0}&tl={1}&dt=t&tk={2}&q={3}";
+      private static readonly string HttpsServicePointRomanizeTemplateUrl = "https://translate.googleapis.com/translate_a/single?client=webapp&sl={0}&tl=en&dt=rm&tk={1}&q={2}";
+      private static readonly string HttpsTranslateUserSite = "https://translate.google.com";
+      private static readonly Random RandomNumbers = new Random();
+
+      private static readonly string[] Accepts = new string[] { null, "*/*", "application/json" };
+      private static readonly string[] AcceptLanguages = new string[] { null, "en-US,en;q=0.9", "en-US", "en" };
+      private static readonly string[] Referers = new string[] { null, "https://translate.google.com/" };
+      private static readonly string[] AcceptCharsets = new string[] { null, Encoding.UTF8.WebName };
+
+      private static readonly string Accept = Accepts[ RandomNumbers.Next( Accepts.Length ) ];
+      private static readonly string AcceptLanguage = AcceptLanguages[ RandomNumbers.Next( AcceptLanguages.Length ) ];
+      private static readonly string Referer = Referers[ RandomNumbers.Next( Referers.Length ) ];
+      private static readonly string AcceptCharset = AcceptCharsets[ RandomNumbers.Next( AcceptCharsets.Length ) ];
+
+      private CookieContainer _cookieContainer;
+      private bool _hasSetup = false;
+      private long m = 427761;
+      private long s = 1179739010;
+      private int _translationsPerRequest = 10;
+
+      public GoogleTranslateEndpoint()
+      {
+         _cookieContainer = new CookieContainer();
+      }
+
+      public override string Id => KnownTranslateEndpointNames.GoogleTranslate;
+
+      public override string FriendlyName => "Google! Translate";
+
+      public override int MaxTranslationsPerRequest => _translationsPerRequest;
+
+      public override void Initialize( IInitializationContext context )
+      {
+         context.DisableCertificateChecksFor( "translate.google.com", "translate.googleapis.com" );
+
+         if( context.DestinationLanguage == "romaji" )
+         {
+            _translationsPerRequest = 1;
+         }
+
+         if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
+      }
+
+      public override IEnumerator OnBeforeTranslate( IHttpTranslationContext context )
+      {
+         if( !_hasSetup || AutoTranslatorState.TranslationCount % 100 == 0 )
+         {
+            _hasSetup = true;
+
+            // Setup TKK and cookies
+            var enumerator = SetupTKK();
+            while( enumerator.MoveNext() )
+            {
+               yield return enumerator.Current;
+            }
+         }
+      }
+
+      public override void OnCreateRequest( IHttpRequestCreationContext context )
+      {
+         var allUntranslatedText = string.Join( "\n", context.UntranslatedTexts );
+
+         XUnityWebRequest request;
+         if( context.DestinationLanguage == "romaji" )
+         {
+            request = new XUnityWebRequest(
+               string.Format(
+                  HttpsServicePointRomanizeTemplateUrl,
+                  context.SourceLanguage,
+                  Tk( allUntranslatedText ),
+                  Uri.EscapeDataString( allUntranslatedText ) ) );
+         }
+         else
+         {
+            request = new XUnityWebRequest(
+               string.Format(
+                  HttpsServicePointTranslateTemplateUrl,
+                  context.SourceLanguage,
+                  context.DestinationLanguage,
+                  Tk( allUntranslatedText ),
+                  Uri.EscapeDataString( allUntranslatedText ) ) );
+         }
+
+         request.Cookies = _cookieContainer;
+         AddHeaders( request, true );
+
+         context.Complete( request );
+      }
+
+      public override void OnInspectResponse( IHttpResponseInspectionContext context )
+      {
+         InspectResponse( context.Response );
+      }
+
+      public override void OnExtractTranslation( IHttpTranslationExtractionContext context )
+      {
+         var isRomaji = context.DestinationLanguage == "romaji";
+         var dataIndex = isRomaji ? 3 : 0;
+
+         var data = context.Response.Data;
+         var arr = JSON.Parse( data );
+         var lineBuilder = new StringBuilder( data.Length );
+
+         arr = arr.AsArray[ 0 ];
+         if( arr.IsNull && isRomaji )
+         {
+            context.Complete( context.UntranslatedText );
+            return;
+         }
+
+         foreach( JSONNode entry in arr.AsArray )
+         {
+            var token = entry.AsArray[ dataIndex ].ToString();
+            token = JsonHelper.Unescape( token.Substring( 1, token.Length - 2 ) );
+
+            if( !lineBuilder.EndsWithWhitespaceOrNewline() ) lineBuilder.Append( '\n' );
+
+            lineBuilder.Append( token );
+         }
+
+         var allTranslation = lineBuilder.ToString();
+         if( context.UntranslatedTexts.Length == 1 )
+         {
+            context.Complete( allTranslation );
+         }
+         else
+         {
+            var translatedLines = allTranslation.Split( '\n' );
+            var translatedTexts = new List<string>();
+
+            int current = 0;
+            foreach( var untranslatedText in context.UntranslatedTexts )
+            {
+               var untranslatedLines = untranslatedText.Split( '\n' );
+               var untranslatedLinesCount = untranslatedLines.Length;
+               var translatedText = string.Empty;
+
+               for( int i = 0 ; i < untranslatedLinesCount ; i++ )
+               {
+                  if( current >= translatedLines.Length ) context.Fail( "Batch operation received incorrect number of translations." );
+
+                  var translatedLine = translatedLines[ current++ ];
+                  translatedText += translatedLine;
+
+                  if( i != untranslatedLinesCount - 1 ) translatedText += '\n';
+               }
+
+               translatedTexts.Add( translatedText );
+            }
+
+            if( current != translatedLines.Length ) context.Fail( "Batch operation received incorrect number of translations." );
+
+            context.Complete( translatedTexts.ToArray() );
+         }
+      }
+
+      private XUnityWebRequest CreateWebSiteRequest()
+      {
+         var request = new XUnityWebRequest( HttpsTranslateUserSite );
+
+         request.Cookies = _cookieContainer;
+         AddHeaders( request, false );
+
+         return request;
+      }
+
+      private void AddHeaders( XUnityWebRequest request, bool isTranslationRequest )
+      {
+         request.Headers[ HttpRequestHeader.UserAgent ] = string.IsNullOrEmpty( AutoTranslatorSettings.UserAgent ) ? UserAgents.Chrome_Win10_Latest : AutoTranslatorSettings.UserAgent;
+         if( AcceptLanguage != null )
+         {
+            request.Headers[ HttpRequestHeader.AcceptLanguage ] = AcceptLanguage;
+         }
+         if( Accept != null )
+         {
+            request.Headers[ HttpRequestHeader.Accept ] = Accept;
+         }
+         if( Referer != null && isTranslationRequest )
+         {
+            request.Headers[ HttpRequestHeader.Referer ] = Referer;
+         }
+         if( AcceptCharset != null )
+         {
+            request.Headers[ HttpRequestHeader.AcceptCharset ] = AcceptCharset;
+         }
+      }
+
+      private void InspectResponse( XUnityWebResponse response )
+      {
+         CookieCollection cookies = response.NewCookies;
+         foreach( Cookie cookie in cookies )
+         {
+            // redirect cookie to correct domain
+            cookie.Domain = ".googleapis.com";
+         }
+
+         // FIXME: Is this needed? Should already be added
+         _cookieContainer.Add( cookies );
+      }
+
+      public IEnumerator SetupTKK()
+      {
+         XUnityWebResponse response = null;
+
+         _cookieContainer = new CookieContainer();
+
+         try
+         {
+            var client = new XUnityWebClient();
+            var request = CreateWebSiteRequest();
+            response = client.Send( request );
+         }
+         catch( Exception e )
+         {
+            XuaLogger.Current.Warn( e, "An error occurred while setting up GoogleTranslate TKK. Using fallback TKK values instead." );
+            yield break;
+         }
+
+         // wait for response completion
+         var iterator = response.GetSupportedEnumerator();
+         while( iterator.MoveNext() ) yield return iterator.Current;
+
+         InspectResponse( response );
+
+         // failure
+         if( response.Error != null )
+         {
+            XuaLogger.Current.Warn( response.Error, "An error occurred while setting up GoogleTranslate TKK. Using fallback TKK values instead." );
+            yield break;
+         }
+
+         // failure
+         if( response.Data == null )
+         {
+            XuaLogger.Current.Warn( null, "An error occurred while setting up GoogleTranslate TKK. Using fallback TKK values instead." );
+            yield break;
+         }
+
+         try
+         {
+            var html = response.Data;
+
+            bool found = false;
+            string[] lookups = new[] { "tkk:'", "TKK='" };
+            foreach( var lookup in lookups )
+            {
+               var index = html.IndexOf( lookup );
+               if( index > -1 ) // simple string approach
+               {
+                  var startIndex = index + lookup.Length;
+                  var endIndex = html.IndexOf( "'", startIndex );
+                  var result = html.Substring( startIndex, endIndex - startIndex );
+
+                  var parts = result.Split( '.' );
+                  if( parts.Length == 2 )
+                  {
+                     m = long.Parse( parts[ 0 ] );
+                     s = long.Parse( parts[ 1 ] );
+                     found = true;
+                     break;
+                  }
+               }
+            }
+
+            if( !found )
+            {
+               XuaLogger.Current.Warn( "An error occurred while setting up GoogleTranslate TKK. Could not locate TKK value. Using fallback TKK values instead." );
+            }
+         }
+         catch( Exception e )
+         {
+            XuaLogger.Current.Warn( e, "An error occurred while setting up GoogleTranslate TKK. Using fallback TKK values instead." );
+         }
+      }
+
+      // TKK Approach stolen from Translation Aggregator r190, all credits to Sinflower
+
+      private long Vi( long r, string o )
+      {
+         for( var t = 0 ; t < o.Length ; t += 3 )
+         {
+            long a = o[ t + 2 ];
+            a = a >= 'a' ? a - 87 : a - '0';
+            a = '+' == o[ t + 1 ] ? r >> (int)a : r << (int)a;
+            r = '+' == o[ t ] ? r + a & 4294967295 : r ^ a;
+         }
+
+         return r;
+      }
+
+      private string Tk( string r )
+      {
+         List<long> S = new List<long>();
+
+         for( var v = 0 ; v < r.Length ; v++ )
+         {
+            long A = r[ v ];
+            if( 128 > A )
+               S.Add( A );
+            else
+            {
+               if( 2048 > A )
+                  S.Add( A >> 6 | 192 );
+               else if( 55296 == ( 64512 & A ) && v + 1 < r.Length && 56320 == ( 64512 & r[ v + 1 ] ) )
+               {
+                  A = 65536 + ( ( 1023 & A ) << 10 ) + ( 1023 & r[ ++v ] );
+                  S.Add( A >> 18 | 240 );
+                  S.Add( A >> 12 & 63 | 128 );
+               }
+               else
+               {
+                  S.Add( A >> 12 | 224 );
+                  S.Add( A >> 6 & 63 | 128 );
+               }
+
+               S.Add( 63 & A | 128 );
+            }
+         }
+
+         const string F = "+-a^+6";
+         const string D = "+-3^+b+-f";
+         long p = m;
+
+         for( var b = 0 ; b < S.Count ; b++ )
+         {
+            p += S[ b ];
+            p = Vi( p, F );
+         }
+
+         p = Vi( p, D );
+         p ^= s;
+         if( 0 > p )
+            p = ( 2147483647 & p ) + 2147483648;
+
+         p %= (long)1e6;
+
+         return p.ToString( CultureInfo.InvariantCulture ) + "." + ( p ^ m ).ToString( CultureInfo.InvariantCulture );
+      }
+   }
+}

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

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

+ 81 - 0
src/Translators/GoogleTranslateLegitimate/GoogleTranslateLegitimateEndpoint.cs

@@ -0,0 +1,81 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Reflection;
+using System.Text;
+using SimpleJSON;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints.Http;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace GoogleTranslateLegitimate
+{
+   internal class GoogleTranslateLegitimateEndpoint : HttpEndpoint
+   {
+      private static readonly HashSet<string> SupportedLanguages = new HashSet<string> { "af","sq","am","ar","hy","az","eu","be","bn","bs","bg","ca","ceb","zh-CN","zh-TW","co","hr","cs","da","nl","en","eo","et","fi","fr","fy","gl","ka","de","el","gu","ht","ha","haw","he","hi","hmn","hu","is","ig","id","ga","it","ja","jw","kn","kk","km","ko","ku","ky","lo","la","lv","lt","lb","mk","mg","ms","ml","mt","mi","mr","mn","my","ne","no","ny","ps","fa","pl","pt","pa","ro","ru","sm","gd","sr","st","sn","sd","si","sk","sl","so","es","su","sw","sv","tl","tg","ta","te","th","tr","uk","ur","uz","vi","cy","xh","yi","yo","zu" };
+      private static readonly string HttpsServiceUrl = "https://translation.googleapis.com/language/translate/v2";
+
+      private string _key;
+
+      public override string Id => "GoogleTranslateLegitimate";
+
+      public override string FriendlyName => "Google! Translate (Authenticated)";
+
+      public override int MaxTranslationsPerRequest => 10;
+
+      public override void Initialize( IInitializationContext context )
+      {
+         _key = context.GetOrCreateSetting( "GoogleLegitimate", "GoogleAPIKey", "" );
+         if( string.IsNullOrEmpty( _key ) ) throw new Exception( "The GoogleTranslateLegitimate endpoint requires an API key which has not been provided." );
+
+         // Configure service points / service point manager
+         context.DisableCertificateChecksFor( "translation.googleapis.com" );
+
+         if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
+      }
+
+      public override void OnCreateRequest( IHttpRequestCreationContext context )
+      {
+         var urlBuilder = new StringBuilder( HttpsServiceUrl );
+         urlBuilder.Append( "?key=" ).Append( Uri.EscapeDataString( _key ) );
+         urlBuilder.Append( "&source=" ).Append( context.SourceLanguage );
+         urlBuilder.Append( "&target=" ).Append( context.DestinationLanguage );
+         for( int i = 0 ; i < context.UntranslatedTexts.Length ; i++ )
+         {
+            var untranslatedText = context.UntranslatedTexts[ i ];
+            urlBuilder.Append( "&q=" ).Append( Uri.EscapeDataString( untranslatedText ) );
+         }
+
+         var request = new XUnityWebRequest(
+            "POST",
+            urlBuilder.ToString() );
+
+         context.Complete( request );
+      }
+
+      public override void OnExtractTranslation( IHttpTranslationExtractionContext context )
+      {
+         var data = context.Response.Data;
+         var arr = JSON.Parse( data ).AsObject[ "data" ].AsObject[ "translations" ].AsArray;
+
+         var translatedTexts = new List<string>();
+         for( int i = 0 ; i < arr.Count ; i++ )
+         {
+            var obj = arr[ i ];
+            var token = obj.AsObject[ "translatedText" ].ToString();
+            var translatedText = JsonHelper.Unescape( token.Substring( 1, token.Length - 2 ) );
+            translatedTexts.Add( translatedText );
+         }
+
+         context.Complete( translatedTexts.ToArray() );
+      }
+   }
+}

+ 17 - 0
src/Translators/Lec.ExtProtocol/Kernel32.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Lec.ExtProtocol
+{
+   internal static class Kernel32
+   {
+      [DllImport( "kernel32.dll" )]
+      public static extern IntPtr LoadLibrary( string dllToLoad );
+
+      [DllImport( "kernel32.dll" )]
+      public static extern IntPtr GetProcAddress( IntPtr hModule, string procedureName );
+
+      [DllImport( "kernel32.dll" )]
+      public static extern bool FreeLibrary( IntPtr hModule );
+   }
+}

+ 26 - 0
src/Translators/Lec.ExtProtocol/Lec.ExtProtocol.csproj

@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net35</TargetFramework>
+    <Platforms>AnyCPU;x86</Platforms>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
+    <AllowUnsafeBlocks>false</AllowUnsafeBlocks>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
+    <Optimize>false</Optimize>
+    <AllowUnsafeBlocks>false</AllowUnsafeBlocks>
+  </PropertyGroup>
+
+   <Target Name="PostBuild" AfterTargets="PostBuildEvent">
+      <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\Translators\&quot;&#xD;&#xA;)" />
+   </Target>
+
+   <ItemGroup>
+     <ProjectReference Include="..\..\XUnity.AutoTranslator.Plugin.ExtProtocol\XUnity.AutoTranslator.Plugin.ExtProtocol.csproj" />
+   </ItemGroup>
+
+</Project>

+ 159 - 0
src/Translators/Lec.ExtProtocol/LecTranslationLibrary.cs

@@ -0,0 +1,159 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Lec.ExtProtocol
+{
+   class LecTranslationLibrary : UnmanagedTranslationLibrary
+   {
+      public const int JapaneseCodepage = 932;
+
+      [UnmanagedFunctionPointer( CallingConvention.Cdecl )]
+      private delegate int eg_init( string path );
+      [UnmanagedFunctionPointer( CallingConvention.Cdecl )]
+      private delegate int eg_init2( string path, int i );
+      [UnmanagedFunctionPointer( CallingConvention.Cdecl )]
+      private delegate int eg_end();
+      [UnmanagedFunctionPointer( CallingConvention.Cdecl )]
+      private delegate int eg_translate_multi( int i, IntPtr in_str, int out_size, IntPtr out_str );
+
+      private eg_end _end;
+      private eg_translate_multi _translate;
+      private eg_init _init;
+      private eg_init2 _init2;
+
+      public LecTranslationLibrary( string libraryPath )
+         : base( libraryPath )
+      {
+
+      }
+
+      private string PreprocessString( string str )
+      {
+         var builder = new StringBuilder( str.Length );
+
+         foreach( var c in str )
+         {
+            char o;
+
+            switch( c )
+            {
+               case '『':
+               case '「':
+               case '「':
+                  o = '['; break;
+               case '』':
+               case '」':
+               case '」':
+                  o = ']'; break;
+               case '≪':
+               case '(':
+                  o = '('; break;
+               case '≫':
+               case ')':
+                  o = ')'; break;
+               case '…':
+                  o = ' '; break;
+               case ':':
+                  o = '¦'; break;
+               case '・':
+                  o = '.'; break;
+               default:
+                  o = c;
+                  break;
+            }
+            builder.Append( c );
+         }
+
+         return builder.ToString();
+      }
+
+      public override string Translate( string toTranslate )
+      {
+         toTranslate = PreprocessString( toTranslate );
+         var size = toTranslate.Length * 3;
+         int translatedSize;
+         IntPtr ptr = IntPtr.Zero;
+
+         // we can't know the output size, so just try until we have a big enough buffer
+         try
+         {
+            do
+            {
+               size = size * 2;
+               // give up when we reach 10 MB string
+               if( size > 10 * 1024 * 1024 )
+               {
+                  return null;
+               }
+
+               if( ptr != IntPtr.Zero )
+               {
+                  Marshal.FreeHGlobal( ptr );
+               }
+               ptr = Marshal.AllocHGlobal( size );
+
+               var str = ConvertStringToNative( toTranslate, JapaneseCodepage );
+               translatedSize = _translate( 0, str, size, ptr );
+
+               Marshal.FreeHGlobal( str );
+
+            } while( translatedSize > size );
+
+            var result = ConvertNativeToString( ptr, JapaneseCodepage );
+
+            return result;
+         }
+         finally
+         {
+            if( ptr != IntPtr.Zero )
+            {
+               Marshal.FreeHGlobal( ptr );
+            }
+         }
+      }
+
+      protected override bool OnInitialize( string libraryPath )
+      {
+         try
+         {
+            _end = Loader.LoadFunction<eg_end>( "eg_end" );
+            _translate = Loader.LoadFunction<eg_translate_multi>( "eg_translate_multi" );
+         }
+         catch( Exception )
+         {
+            return false;
+         }
+
+         try
+         {
+            _init2 = Loader.LoadFunction<eg_init2>( "eg_init2" );
+         }
+         catch
+         {
+            try
+            {
+               _init = Loader.LoadFunction<eg_init>( "eg_init" );
+            }
+            catch
+            {
+               return false;
+            }
+         }
+
+         var directory = Path.GetDirectoryName( libraryPath ) + '\\';
+
+         var succeded = _init2?.Invoke( directory, 0 );
+         var initialized = succeded ?? _init( directory );
+
+         return initialized == 0;
+      }
+
+      protected override void Dispose( bool disposing )
+      {
+         if( disposing ) _end?.Invoke();
+         base.Dispose( disposing );
+      }
+   }
+}

+ 72 - 0
src/Translators/Lec.ExtProtocol/Program.cs

@@ -0,0 +1,72 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Threading;
+using XUnity.AutoTranslator.Plugin.ExtProtocol;
+
+namespace Lec.ExtProtocol
+{
+   class Program
+   {
+      static void Main( string[] args )
+      {
+         // Implementation of this is based off of texel-sensei's LEC implementation
+
+         try
+         {
+            if( args.Length == 0 )
+            {
+               Console.WriteLine( "This program is automatically run during a game session if LEC is selected, and will be automatically stopped afterwards. This program cannot be run independently." );
+               Console.WriteLine( "Press any key to exit." );
+               Console.ReadKey();
+               return;
+            }
+
+            var powerTranslatorPathPayload = args[ 0 ];
+            var powerTranslatorPath = Encoding.UTF8.GetString( Convert.FromBase64String( powerTranslatorPathPayload ) );
+            var dllPath = Path.Combine( powerTranslatorPath, @"Nova\JaEn\EngineDll_je.dll" );
+
+            using( var translator = new LecTranslationLibrary( dllPath ) )
+            {
+               using( var stdout = Console.OpenStandardOutput() )
+               using( var writer = new StreamWriter( stdout ) )
+               using( var stdin = Console.OpenStandardInput() )
+               using( var reader = new StreamReader( stdin ) )
+               {
+                  writer.AutoFlush = true;
+
+                  while( true )
+                  {
+                     var receivedPayload = reader.ReadLine();
+                     if( string.IsNullOrEmpty( receivedPayload ) ) return;
+
+                     var message = ExtProtocolConvert.Decode( receivedPayload ) as TranslationRequest;
+                     if( message == null ) return;
+
+                     var translatedTexts = new string[ message.UntranslatedTexts.Length ];
+                     for( int i = 0 ; i < message.UntranslatedTexts.Length ; i++ )
+                     {
+                        var untranslatedText = message.UntranslatedTexts[ i ];
+                        var translatedText = translator.Translate( untranslatedText );
+                        translatedTexts[ i ] = translatedText;
+                     }
+
+                     var response = new TranslationResponse
+                     {
+                        Id = message.Id,
+                        TranslatedTexts = translatedTexts
+                     };
+
+                     var translatedPayload = ExtProtocolConvert.Encode( response );
+                     writer.WriteLine( translatedPayload );
+                  }
+               }
+            }
+         }
+         catch( Exception )
+         {
+            // "Graceful shutdown"
+         }
+      }
+   }
+}

+ 7 - 0
src/Translators/Lec.ExtProtocol/Properties/launchSettings.json

@@ -0,0 +1,7 @@
+{
+  "profiles": {
+    "XUnity.AutoTranslator.Plugin.Lec": {
+      "commandName": "Project"
+    }
+  }
+}

+ 53 - 0
src/Translators/Lec.ExtProtocol/UnmanagedLibraryLoader.cs

@@ -0,0 +1,53 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Lec.ExtProtocol
+{
+   public sealed class UnmanagedLibraryLoader : IDisposable
+   {
+      private IntPtr _libraryPointer;
+      private bool _disposed = false;
+
+      public bool LoadLibrary( string path )
+      {
+         _libraryPointer = Kernel32.LoadLibrary( path );
+
+         return _libraryPointer != IntPtr.Zero;
+      }
+
+      public TDelegate LoadFunction<TDelegate>( string name )
+      {
+         if( _libraryPointer == IntPtr.Zero ) throw new InvalidOperationException( $"Could not load the function {name} because no library has been loaded!" );
+
+         var addr = Kernel32.GetProcAddress( _libraryPointer, name );
+         if( addr == IntPtr.Zero ) throw new InvalidOperationException( $"Could not find the function pointer {name}!" );
+
+         return (TDelegate)(object)Marshal.GetDelegateForFunctionPointer( addr, typeof( TDelegate ) );
+      }
+
+      #region IDisposable Support
+
+      void Dispose( bool disposing )
+      {
+         if( !_disposed )
+         {
+            Kernel32.FreeLibrary( _libraryPointer );
+
+            _disposed = true;
+         }
+      }
+
+      ~UnmanagedLibraryLoader()
+      {
+         Dispose( false );
+      }
+      
+      public void Dispose()
+      {
+         Dispose( true );
+         GC.SuppressFinalize( this );
+      }
+
+      #endregion
+   }
+}

+ 64 - 0
src/Translators/Lec.ExtProtocol/UnmanagedTranslationLibrary.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Lec.ExtProtocol
+{
+   internal abstract class UnmanagedTranslationLibrary : IDisposable
+   {
+      protected UnmanagedLibraryLoader Loader = new UnmanagedLibraryLoader();
+
+      public UnmanagedTranslationLibrary( string libraryPath )
+      {
+         var ok = Initialize( libraryPath );
+
+         if( !ok ) throw new Exception( "Could not load library." );
+      }
+
+      public bool Initialize( string libraryPath )
+      {
+         if( !File.Exists( libraryPath ) ) return false;
+
+         return Loader.LoadLibrary( libraryPath ) && OnInitialize( libraryPath );
+      }
+
+      protected abstract bool OnInitialize( string libraryPath );
+
+      public abstract string Translate( string untranslatedText );
+
+      public static IntPtr ConvertStringToNative( string managedString, int codepage )
+      {
+         var encoding = Encoding.GetEncoding( codepage );
+         var buffer = encoding.GetBytes( managedString );
+         IntPtr nativeUtf8 = Marshal.AllocHGlobal( buffer.Length + 1 );
+         Marshal.Copy( buffer, 0, nativeUtf8, buffer.Length );
+         Marshal.WriteByte( nativeUtf8, buffer.Length, 0 );
+         return nativeUtf8;
+      }
+
+      public static string ConvertNativeToString( IntPtr nativeUtf8, int codepage )
+      {
+         int len = 0;
+         while( Marshal.ReadByte( nativeUtf8, len ) != 0 ) ++len;
+         byte[] buffer = new byte[ len ];
+         Marshal.Copy( nativeUtf8, buffer, 0, buffer.Length );
+         return Encoding.GetEncoding( codepage ).GetString( buffer );
+      }
+
+      protected virtual void Dispose( bool disposing )
+      {
+         if( disposing )
+         {
+            Loader.Dispose();
+         }
+      }
+
+      public void Dispose()
+      {
+         Dispose( true );
+      }
+   }
+}

+ 21 - 0
src/Translators/LecPowerTranslator15/LecPowerTranslator15.csproj

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

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

@@ -0,0 +1,49 @@
+using System;
+using System.IO;
+using System.Text;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints.ExtProtocol;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace LecPowerTranslator15
+{
+   internal class LecPowerTranslator15Endpoint : ExtProtocolEndpoint
+   {
+      public override string Id => "LecPowerTranslator15";
+
+      public override string FriendlyName => "LEC Power Translator 15";
+
+      public override int MaxConcurrency => 1;
+
+      public override int MaxTranslationsPerRequest => 50;
+
+      public override void Initialize( IInitializationContext context )
+      {
+         var pathToLec = context.GetOrCreateSetting( "LecPowerTranslator15", "InstallationPath", "" );
+         if( string.IsNullOrEmpty( pathToLec ) ) throw new Exception( "The LecPowerTranslator15 requires the path to the installation folder." );
+
+         var exePath = Path.Combine( context.PluginDirectory, @"Translators\Lec.ExtProtocol.exe" );
+         var fileExists = File.Exists( exePath );
+         if( !fileExists )
+         {
+            throw new Exception( $"Could not find any executable at '{exePath}'" );
+         }
+
+         Arguments = Convert.ToBase64String( Encoding.UTF8.GetBytes( pathToLec ) );
+
+         if( fileExists )
+         {
+            ExecutablePath = exePath;
+         }
+         else
+         {
+            throw new Exception( "Unexpected error occurred." );
+         }
+
+         if( context.SourceLanguage != "ja" ) throw new Exception( "Current implementation only supports japanese-to-english." );
+         if( context.DestinationLanguage != "en" ) throw new Exception( "Current implementation only supports japanese-to-english." );
+      }
+   }
+}

+ 11 - 0
src/Translators/ReverseTranslator/ReverseTranslator.csproj

@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net35</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\XUnity.AutoTranslator.Plugin.Core\XUnity.AutoTranslator.Plugin.Core.csproj" />
+  </ItemGroup>
+
+</Project>

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

@@ -0,0 +1,35 @@
+using System;
+using System.Collections;
+using System.Linq;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace ReverseTranslator
+{
+   public class ReverseTranslatorEndpoint : ITranslateEndpoint
+   {
+      private bool _myConfig;
+
+      public string Id => "ReverseTranslator";
+
+      public string FriendlyName => "Reverser";
+
+      public int MaxConcurrency => 50;
+
+      public int MaxTranslationsPerRequest => 1;
+
+      public void Initialize( IInitializationContext context )
+      {
+         _myConfig = context.GetOrCreateSetting( "Reverser", "MyConfig", true );
+      }
+
+      public IEnumerator Translate( ITranslationContext context )
+      {
+         var reversedText = new string( context.UntranslatedText.Reverse().ToArray() );
+         context.Complete( reversedText );
+
+         return null;
+      }
+   }
+}

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

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

+ 92 - 0
src/Translators/WatsonTranslate/WatsonTranslateEndpoint.cs

@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using SimpleJSON;
+using XUnity.AutoTranslator.Plugin.Core;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints.Www;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace WatsonTranslate
+{
+   internal class WatsonTranslateEndpoint : WwwEndpoint
+   {
+      private static readonly HashSet<string> SupportedLanguagePairs = new HashSet<string> { "ar-en", "ca-es", "zh-en", "zh-TW-en", "cs-en", "da-en", "nl-en", "en-ar", "en-cs", "en-da", "en-de", "en-es", "en-fi", "en-fr", "en-hi", "en-it", "en-ja", "en-ko", "en-nb", "en-nl", "en-pl", "en-pt", "en-ru", "en-sv", "en-tr", "en-zh", "en-zh-TW", "fi-en", "fr-de", "fr-en", "fr-es", "de-en", "de-fr", "de-it", "hi-en", "hu-en", "it-de", "it-en", "ja-en", "ko-en", "nb-en", "pl-en", "pt-en", "ru-en", "es-ca", "es-en", "es-fr", "sv-en", "tr-en" };
+
+      private string _fullUrl;
+      private string _url;
+      private string _key;
+
+      public override string Id => "WatsonTranslate";
+
+      public override string FriendlyName => "Watson Language Translator";
+
+      public override int MaxTranslationsPerRequest => 10;
+
+      public override void Initialize( IInitializationContext context )
+      {
+         _url = context.GetOrCreateSetting( "Watson", "Url", "" );
+         _key = context.GetOrCreateSetting( "Watson", "Key", "" );
+         if( string.IsNullOrEmpty( _url ) ) throw new Exception( "The WatsonTranslate endpoint requires a url which has not been provided." );
+         if( string.IsNullOrEmpty( _key ) ) throw new Exception( "The WatsonTranslate endpoint requires a key which has not been provided." );
+
+         _fullUrl = _url.TrimEnd( '/' ) + "/v3/translate?version=2018-05-01";
+
+         var model = context.SourceLanguage + "-" + context.DestinationLanguage;
+         if( !SupportedLanguagePairs.Contains( model ) ) throw new Exception( $"The language model '{model}' is not supported." );
+      }
+
+      public override void OnCreateRequest( IWwwRequestCreationContext context )
+      {
+         StringBuilder data = new StringBuilder();
+         data.Append( "{\"text\":[" );
+         for( int i = 0 ; i < context.UntranslatedTexts.Length ; i++ )
+         {
+            var untranslatedText = JsonHelper.Escape( context.UntranslatedTexts[ i ] );
+            data.Append( "\"" ).Append( untranslatedText ).Append( "\"" );
+
+            if( context.UntranslatedTexts.Length - 1 != i )
+            {
+               data.Append( "," );
+            }
+         }
+         data.Append( "],\"model_id\":\"" )
+            .Append( context.SourceLanguage )
+            .Append( "-" )
+            .Append( context.DestinationLanguage )
+            .Append( "\"}" );
+
+         var request = new WwwRequestInfo(
+            _fullUrl,
+            data.ToString() );
+         
+         request.Headers[ "Accept" ] = "application/json";
+         request.Headers[ "Content-Type" ] = "application/json";
+         request.Headers[ "Authorization" ] = "Basic " + Convert.ToBase64String( Encoding.ASCII.GetBytes( "apikey:" + _key ) );
+
+         context.Complete( request );
+      }
+
+      public override void OnExtractTranslation( IWwwTranslationExtractionContext context )
+      {
+         var data = context.ResponseData;
+         var arr = JSON.Parse( data ).AsObject[ "translations" ].AsArray;
+
+         var translatedTexts = new List<string>();
+         for( int i = 0 ; i < arr.Count ; i++ )
+         {
+            var token = arr[ i ].AsObject[ "translation" ].ToString();
+            var translatedText = JsonHelper.Unescape( token.Substring( 1, token.Length - 2 ) );
+            translatedTexts.Add( translatedText );
+         }
+
+         context.Complete( translatedTexts.ToArray() );
+      }
+   }
+}

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

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

+ 72 - 0
src/Translators/YandexTranslate/YandexTranslateEndpoint.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using SimpleJSON;
+using XUnity.AutoTranslator.Plugin.Core;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints.Http;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace YandexTranslate
+{
+   internal class YandexTranslateEndpoint : HttpEndpoint
+   {
+      private static readonly HashSet<string> SupportedLanguages = new HashSet<string> { "az", "sq", "am", "en", "ar", "hy", "af", "eu", "ba", "be", "bn", "my", "bg", "bs", "cy", "hu", "vi", "ht", "gl", "nl", "mrj", "el", "ka", "gu", "da", "he", "yi", "id", "ga", "it", "is", "es", "kk", "kn", "ca", "ky", "zh", "ko", "xh", "km", "lo", "la", "lv", "lt", "lb", "mg", "ms", "ml", "mt", "mk", "mi", "mr", "mhr", "mn", "de", "ne", "no", "pa", "pap", "fa", "pl", "pt", "ro", "ru", "ceb", "sr", "si", "sk", "sl", "sw", "su", "tg", "th", "tl", "ta", "tt", "te", "tr", "udm", "uz", "uk", "ur", "fi", "fr", "hi", "hr", "cs", "sv", "gd", "et", "eo", "jv", "ja" };
+      private static readonly string HttpsServicePointTemplateUrl = "https://translate.yandex.net/api/v1.5/tr.json/translate?key={3}&text={2}&lang={0}-{1}&format=plain";
+
+      private string _key;
+
+      public override string Id => "YandexTranslate";
+
+      public override string FriendlyName => "Yandex Translate";
+
+      public override void Initialize( IInitializationContext context )
+      {
+         _key = context.GetOrCreateSetting( "Yandex", "YandexAPIKey", "" );
+         context.DisableCertificateChecksFor( "translate.yandex.net" );
+
+         // if the plugin cannot be enabled, simply throw so the user cannot select the plugin
+         if( string.IsNullOrEmpty( _key ) ) throw new Exception( "The YandexTranslate endpoint requires an API key which has not been provided." );
+         if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
+      }
+
+      public override void OnCreateRequest( IHttpRequestCreationContext context )
+      {
+         var request = new XUnityWebRequest(
+            string.Format(
+               HttpsServicePointTemplateUrl,
+               context.SourceLanguage,
+               context.DestinationLanguage,
+               WwwHelper.EscapeUrl( context.UntranslatedText ),
+               _key ) );
+         
+         request.Headers[ HttpRequestHeader.Accept ] = "*/*";
+         request.Headers[ HttpRequestHeader.AcceptCharset ] = "UTF-8";
+
+         context.Complete( request );
+      }
+
+      public override void OnExtractTranslation( IHttpTranslationExtractionContext context )
+      {
+         var data = context.Response.Data;
+         var obj = JSON.Parse( data );
+
+         var code = obj.AsObject[ "code" ].ToString();
+         if( code != "200" ) context.Fail( "Received bad response code: " + code );
+
+         var token = obj.AsObject[ "text" ].ToString();
+         var translation = JsonHelper.Unescape( token.Substring( 2, token.Length - 4 ) );
+
+         if( string.IsNullOrEmpty( translation ) ) context.Fail( "Received no translation." );
+
+         context.Complete( translation );
+      }
+   }
+}

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

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

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

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>net35</TargetFramework>

+ 7 - 14
src/XUnity.AutoTranslator.Plugin.BepIn/AutoTranslatorPlugin.cs

@@ -11,35 +11,28 @@ using XUnity.AutoTranslator.Plugin.Core.Constants;
 
 namespace XUnity.AutoTranslator.Plugin.BepIn
 {
-   [BepInEx.BepInPlugin( GUID: PluginData.Identifier, Name: PluginData.Name, Version: PluginData.Version )]
-   public class AutoTranslatorPlugin : BepInEx.BaseUnityPlugin, IConfiguration
+   [BepInPlugin( GUID: PluginData.Identifier, Name: PluginData.Name, Version: PluginData.Version )]
+   public class AutoTranslatorPlugin : BaseUnityPlugin, IPluginEnvironment
    {
       private IniFile _file;
       private string _configPath;
-      private string _dataFolder;
 
       public AutoTranslatorPlugin()
       {
-         _dataFolder = "BepInEx";
-         _configPath = Path.Combine( _dataFolder, "AutoTranslatorConfig.ini" );
-         Core.Logger.Current = new BepInLogger();
+         DataPath = "BepInEx";
+         _configPath = Path.Combine( DataPath, "AutoTranslatorConfig.ini" );
+         XuaLogger.Current = new BepInLogger();
       }
 
       public IniFile Preferences
       {
          get
          {
-            return ( _file ?? ( _file = ReloadConfig() ) ); ;
+            return ( _file ?? ( _file = ReloadConfig() ) );
          }
       }
 
-      public string DataPath
-      {
-         get
-         {
-            return _dataFolder;
-         }
-      }
+      public string DataPath { get; }
 
       public IniFile ReloadConfig()
       {

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.BepIn/BepInLogger.cs

@@ -7,7 +7,7 @@ using XUnity.AutoTranslator.Plugin.Core.Constants;
 
 namespace XUnity.AutoTranslator.Plugin.BepIn
 {
-   public class BepInLogger : Logger
+   public class BepInLogger : XuaLogger
    {
       public BepInLogger()
       {

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

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>2.18.0</Version>
+      <Version>3.0.0</Version>
    </PropertyGroup>
 
    <ItemGroup>
@@ -31,7 +31,7 @@
       <ItemGroup>
          <VersionNumber Include="$([System.Text.RegularExpressions.Regex]::Replace(&quot;%(Targets.Version)&quot;, &quot;^(.+?)(\.0+)$&quot;, &quot;$1&quot;))" />
       </ItemGroup>
-      <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)ExIni.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.Core.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   COPY /Y &quot;$(SolutionDir)README.md&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\README (AutoTranslator).md&quot;&#xD;&#xA;   powershell Compress-Archive -Path '$(SolutionDir)dist\BepIn\BepInEx' -DestinationPath '$(SolutionDir)dist\BepIn\XUnity.AutoTranslator-BepIn-@(VersionNumber).zip' -Force)&#xD;&#xA;)" />
+      <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   for %%f in (&quot;$(SolutionDir)dist\Translators\*&quot;) do XCOPY /Y /I &quot;%%f&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\Translators\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)ExIni.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.Core.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.ExtProtocol.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   COPY /Y &quot;$(SolutionDir)README.md&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\README (AutoTranslator).md&quot;&#xD;&#xA;   powershell Compress-Archive -Path '$(SolutionDir)dist\BepIn\BepInEx' -DestinationPath '$(SolutionDir)dist\BepIn\XUnity.AutoTranslator-BepIn-@(VersionNumber).zip' -Force)&#xD;&#xA;)" />
    </Target>
 
 </Project>

+ 26 - 0
src/XUnity.AutoTranslator.Plugin.Core/ApplicationInformation.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core
+{
+   internal static class ApplicationInformation
+   {
+      [DllImport( "kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = false )]
+      private static extern int GetModuleFileName( HandleRef hModule, StringBuilder buffer, int length );
+
+      private static HandleRef Null = new HandleRef( null, IntPtr.Zero );
+
+      public static string StartupPath
+      {
+         get
+         {
+            StringBuilder stringBuilder = new StringBuilder( 260 );
+            GetModuleFileName( Null, stringBuilder, stringBuilder.Capacity );
+            return stringBuilder.ToString();
+         }
+      }
+   }
+}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 380 - 131
src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs


+ 25 - 0
src/XUnity.AutoTranslator.Plugin.Core/AutoTranslatorSettings.cs

@@ -0,0 +1,25 @@
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+
+namespace XUnity.AutoTranslator.Plugin.Core
+{
+   /// <summary>
+   /// Class representing publicly available settings of the plugin.
+   /// </summary>
+   public static class AutoTranslatorSettings
+   {
+      /// <summary>
+      /// Gets the user-supplied user agent.
+      /// </summary>
+      public static string UserAgent => Settings.UserAgent;
+
+      /// <summary>
+      /// Gets the configured source language.
+      /// </summary>
+      public static string SourceLanguage => Settings.FromLanguage;
+
+      /// <summary>
+      /// Gets the configured destination language.
+      /// </summary>
+      public static string DestinationLanguage => Settings.Language;
+   }
+}

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

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+
+namespace XUnity.AutoTranslator.Plugin.Core
+{
+   /// <summary>
+   /// Class representing publicly available state of the plugin.
+   /// </summary>
+   public static class AutoTranslatorState
+   {
+      /// <summary>
+      /// Gets the number of translation requests that has been completed.
+      /// </summary>
+      public static int TranslationCount => Settings.TranslationCount;
+   }
+}

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

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

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

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

+ 0 - 13
src/XUnity.AutoTranslator.Plugin.Core/Configuration/Config.cs

@@ -1,13 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-
-namespace XUnity.AutoTranslator.Plugin.Core.Configuration
-{
-   public static class Config
-   {
-      public static IConfiguration Current;
-   }
-}

+ 0 - 19
src/XUnity.AutoTranslator.Plugin.Core/Configuration/IConfiguration.cs

@@ -1,19 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using ExIni;
-
-namespace XUnity.AutoTranslator.Plugin.Core.Configuration
-{
-   public interface IConfiguration
-   {
-      string DataPath { get; }
-
-      IniFile Preferences { get; }
-
-      void SaveConfig();
-
-      IniFile ReloadConfig();
-   }
-}

+ 0 - 92
src/XUnity.AutoTranslator.Plugin.Core/Configuration/IniKeyExtensions.cs

@@ -1,92 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using ExIni;
-using XUnity.AutoTranslator.Plugin.Core.Extensions;
-
-namespace XUnity.AutoTranslator.Plugin.Core.Configuration
-{
-   public static class IniKeyExtensions
-   {
-      public static T GetOrDefault<T>( this IniKey that, T defaultValue, bool allowEmpty = false )
-      {
-         var typeOfT = typeof( T ).UnwrapNullable();
-         if( allowEmpty )
-         {
-            var value = that.Value;
-            if( value == null ) // we want to use the default value, because it is null, the config has just been created
-            {
-               if( defaultValue != null )
-               {
-                  if( typeOfT.IsEnum )
-                  {
-                     that.Value = Enum.GetName( typeOfT, defaultValue );
-                  }
-                  else
-                  {
-                     that.Value = Convert.ToString( defaultValue, CultureInfo.InvariantCulture );
-                  }
-               }
-               else
-               {
-                  that.Value = string.Empty;
-               }
-               return defaultValue;
-            }
-            else
-            {
-               // there exists a value in the config, so we do not want to set anything
-               // we just want to return what we can find, default not included
-               if( !string.IsNullOrEmpty( value ) )
-               {
-                  if( typeOfT.IsEnum )
-                  {
-                     return (T)Enum.Parse( typeOfT, that.Value, true );
-                  }
-                  else
-                  {
-                     return (T)Convert.ChangeType( that.Value, typeOfT, CultureInfo.InvariantCulture );
-                  }
-               }
-               return default( T );
-            }
-         }
-         else
-         {
-            var value = that.Value;
-            if( string.IsNullOrEmpty( value ) )
-            {
-               if( defaultValue != null )
-               {
-                  if( typeOfT.IsEnum )
-                  {
-                     that.Value = Enum.GetName( typeOfT, defaultValue );
-                  }
-                  else
-                  {
-                     that.Value = Convert.ToString( defaultValue, CultureInfo.InvariantCulture );
-                  }
-               }
-               else
-               {
-                  that.Value = string.Empty;
-               }
-               return defaultValue;
-            }
-            else
-            {
-               if( typeOfT.IsEnum )
-               {
-                  return (T)Enum.Parse( typeOfT, that.Value, true );
-               }
-               else
-               {
-                  return (T)Convert.ChangeType( that.Value, typeOfT, CultureInfo.InvariantCulture );
-               }
-            }
-         }
-      }
-   }
-}

+ 81 - 126
src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs

@@ -11,37 +11,38 @@ using XUnity.AutoTranslator.Plugin.Core.Utilities;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Configuration
 {
-   public static class Settings
+   internal static class Settings
    {
       // cannot be changed
+      public static readonly int MaxFailuresForSameTextPerEndpoint = 3;
+      public static readonly string PluginFolder = "Translators";
       public static readonly int MaxMaxCharactersPerTranslation = 500;
       public static readonly string DefaultLanguage = "en";
       public static readonly string DefaultFromLanguage = "ja";
       public static readonly string EnglishLanguage = "en";
+      public static readonly string Romaji = "romaji";
       public static readonly int MaxErrors = 5;
       public static readonly float ClipboardDebounceTime = 1f;
       public static readonly int MaxTranslationsBeforeShutdown = 8000;
       public static readonly int MaxUnstartedJobs = 3500;
       public static readonly float IncreaseBatchOperationsEvery = 30;
-      public static readonly bool EnableObjectTracking = true;
       public static readonly int MaximumStaggers = 6;
       public static readonly int PreviousTextStaggerCount = 3;
       public static readonly int MaximumConsecutiveFramesTranslated = 90;
       public static readonly int MaximumConsecutiveSecondsTranslated = 60;
       public static bool UsesWhitespaceBetweenWords = false;
+      public static string ApplicationName;
 
 
       public static bool IsShutdown = false;
       public static bool IsShutdownFatal = false;
       public static int TranslationCount = 0;
-      public static int MaxAvailableBatchOperations = 40;
+      public static int MaxAvailableBatchOperations = 50;
 
       public static readonly float MaxTranslationsQueuedPerSecond = 5;
       public static readonly int MaxSecondsAboveTranslationThreshold = 30;
       public static readonly int TranslationQueueWatchWindow = 6;
 
-      public static readonly int BatchSize = 10;
-
       // can be changed
       public static string ServiceEndpoint;
       public static string Language;
@@ -63,20 +64,12 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static bool IgnoreWhitespaceInDialogue;
       public static bool IgnoreWhitespaceInNGUI;
       public static int MinDialogueChars;
-      public static string BaiduAppId;
-      public static string BaiduAppSecret;
-      public static string YandexAPIKey;
-      public static string WatsonAPIUrl;
-      public static string WatsonAPIUsername;
-      public static string WatsonAPIPassword;
-      public static string BingOcpApimSubscriptionKey;
       public static int ForceSplitTextAfterCharacters;
       public static bool EnableMigrations;
       public static string MigrationsTag;
       public static bool EnableBatching;
       public static bool TrimAllText;
       public static bool EnableUIResizing;
-      public static string GoogleAPIKey;
       public static bool UseStaticTranslations;
       public static string OverrideFont;
       public static string UserAgent;
@@ -84,7 +77,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static float? ResizeUILineSpacingScale;
       public static bool ForceUIResizing;
       public static string[] IgnoreTextStartingWith;
+      public static HashSet<string> GameLogTextPaths;
       public static bool TextGetterCompatibilityMode;
+      public static TextPostProcessing RomajiPostProcessing;
 
       public static string TextureDirectory;
       public static bool EnableTextureTranslation;
@@ -93,116 +88,96 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static bool EnableTextureScanOnSceneLoad;
       public static bool EnableSpriteRendererHooking;
       public static bool LoadUnmodifiedTextures;
-      //public static bool DeleteUnmodifiedTextures;
       public static TextureHashGenerationStrategy TextureHashGenerationStrategy;
 
       public static bool CopyToClipboard;
       public static int MaxClipboardCopyCharacters;
 
-      public static void Configure()
+      public static void SetEndpoint( string id )
       {
-         try
-         {
-            // clear configuration from old versions...
-            var section = Config.Current.Preferences[ "AutoTranslator" ];
-            foreach( var key in section.Keys.ToList() )
-            {
-               section.DeleteKey( key.Key );
-            }
-
-            Config.Current.Preferences.DeleteSection( "AutoTranslator" );
-            Config.Current.Preferences[ "Service" ].DeleteKey( "EnableSSL" );
-         }
-         catch { }
-
-
-
-         ServiceEndpoint = Config.Current.Preferences[ "Service" ][ "Endpoint" ].GetOrDefault( KnownEndpointNames.GoogleTranslate, true );
-
-         Language = Config.Current.Preferences[ "General" ][ "Language" ].GetOrDefault( DefaultLanguage );
-         FromLanguage = Config.Current.Preferences[ "General" ][ "FromLanguage" ].GetOrDefault( DefaultFromLanguage, true );
-
-         TranslationDirectory = Config.Current.Preferences[ "Files" ][ "Directory" ].GetOrDefault( @"Translation" );
-         OutputFile = Config.Current.Preferences[ "Files" ][ "OutputFile" ].GetOrDefault( @"Translation\_AutoGeneratedTranslations.{lang}.txt" );
-
-         EnableIMGUI = Config.Current.Preferences[ "TextFrameworks" ][ "EnableIMGUI" ].GetOrDefault( false );
-         EnableUGUI = Config.Current.Preferences[ "TextFrameworks" ][ "EnableUGUI" ].GetOrDefault( true );
-         EnableNGUI = Config.Current.Preferences[ "TextFrameworks" ][ "EnableNGUI" ].GetOrDefault( true );
-         EnableTextMeshPro = Config.Current.Preferences[ "TextFrameworks" ][ "EnableTextMeshPro" ].GetOrDefault( true );
-         EnableUtage = Config.Current.Preferences[ "TextFrameworks" ][ "EnableUtage" ].GetOrDefault( true );
-         AllowPluginHookOverride = Config.Current.Preferences[ "TextFrameworks" ][ "AllowPluginHookOverride" ].GetOrDefault( true );
-
-         Delay = Config.Current.Preferences[ "Behaviour" ][ "Delay" ].GetOrDefault( 0f );
-         MaxCharactersPerTranslation = Config.Current.Preferences[ "Behaviour" ][ "MaxCharactersPerTranslation" ].GetOrDefault( 200 );
-         IgnoreWhitespaceInDialogue = Config.Current.Preferences[ "Behaviour" ][ "IgnoreWhitespaceInDialogue" ].GetOrDefault( true );
-         IgnoreWhitespaceInNGUI = Config.Current.Preferences[ "Behaviour" ][ "IgnoreWhitespaceInNGUI" ].GetOrDefault( true );
-         MinDialogueChars = Config.Current.Preferences[ "Behaviour" ][ "MinDialogueChars" ].GetOrDefault( 20 );
-         ForceSplitTextAfterCharacters = Config.Current.Preferences[ "Behaviour" ][ "ForceSplitTextAfterCharacters" ].GetOrDefault( 0 );
-         CopyToClipboard = Config.Current.Preferences[ "Behaviour" ][ "CopyToClipboard" ].GetOrDefault( false );
-         MaxClipboardCopyCharacters = Config.Current.Preferences[ "Behaviour" ][ "MaxClipboardCopyCharacters" ].GetOrDefault( 450 );
-         EnableUIResizing = Config.Current.Preferences[ "Behaviour" ][ "EnableUIResizing" ].GetOrDefault( true );
-         EnableBatching = Config.Current.Preferences[ "Behaviour" ][ "EnableBatching" ].GetOrDefault( true );
-         TrimAllText = Config.Current.Preferences[ "Behaviour" ][ "TrimAllText" ].GetOrDefault( ClrTypes.AdvEngine == null );
-         UseStaticTranslations = Config.Current.Preferences[ "Behaviour" ][ "UseStaticTranslations" ].GetOrDefault( true );
-         OverrideFont = Config.Current.Preferences[ "Behaviour" ][ "OverrideFont" ].GetOrDefault( string.Empty );
-         ResizeUILineSpacingScale = Config.Current.Preferences[ "Behaviour" ][ "ResizeUILineSpacingScale" ].GetOrDefault<float?>( null, true );
-         ForceUIResizing = Config.Current.Preferences[ "Behaviour" ][ "ForceUIResizing" ].GetOrDefault( false );
-         IgnoreTextStartingWith = Config.Current.Preferences[ "Behaviour" ][ "IgnoreTextStartingWith" ].GetOrDefault( "\\u180e;", true )
-            ?.Split( new[] { ';' }, StringSplitOptions.RemoveEmptyEntries ).Select( x => x.UnescapeJson() ).ToArray() ?? new string[ 0 ];
-         TextGetterCompatibilityMode = Config.Current.Preferences[ "Behaviour" ][ "TextGetterCompatibilityMode" ].GetOrDefault( false );
-
-         TextureDirectory = Config.Current.Preferences[ "Texture" ][ "TextureDirectory" ].GetOrDefault( @"Translation\Texture" );
-         EnableTextureTranslation = Config.Current.Preferences[ "Texture" ][ "EnableTextureTranslation" ].GetOrDefault( false );
-         EnableTextureDumping = Config.Current.Preferences[ "Texture" ][ "EnableTextureDumping" ].GetOrDefault( false );
-         EnableTextureToggling = Config.Current.Preferences[ "Texture" ][ "EnableTextureToggling" ].GetOrDefault( false );
-         EnableTextureScanOnSceneLoad = Config.Current.Preferences[ "Texture" ][ "EnableTextureScanOnSceneLoad" ].GetOrDefault( false );
-         EnableSpriteRendererHooking = Config.Current.Preferences[ "Texture" ][ "EnableSpriteRendererHooking" ].GetOrDefault( false );
-         LoadUnmodifiedTextures = Config.Current.Preferences[ "Texture" ][ "LoadUnmodifiedTextures" ].GetOrDefault( false );
-         //DeleteUnmodifiedTextures = Config.Current.Preferences[ "Texture" ][ "DeleteUnmodifiedTextures" ].GetOrDefault( false );
-         TextureHashGenerationStrategy = Config.Current.Preferences[ "Texture" ][ "TextureHashGenerationStrategy" ].GetOrDefault( TextureHashGenerationStrategy.FromImageName );
-
-         if( MaxCharactersPerTranslation > MaxMaxCharactersPerTranslation )
-         {
-            Config.Current.Preferences[ "Behaviour" ][ "MaxCharactersPerTranslation" ].Value = MaxMaxCharactersPerTranslation.ToString( CultureInfo.InvariantCulture );
-            MaxCharactersPerTranslation = MaxMaxCharactersPerTranslation;
-         }
+         ServiceEndpoint = id;
+         PluginEnvironment.Current.Preferences[ "Service" ][ "Endpoint" ].Value = id;
+         PluginEnvironment.Current.SaveConfig();
+      }
 
-         // special handling because of enum parsing
+      public static void Configure()
+      {
          try
          {
-            WhitespaceRemovalStrategy = Config.Current.Preferences[ "Behaviour" ][ "WhitespaceRemovalStrategy" ].GetOrDefault( WhitespaceHandlingStrategy.TrimPerNewline );
+            ApplicationName = Path.GetFileNameWithoutExtension( ApplicationInformation.StartupPath );
          }
          catch( Exception e )
          {
-            WhitespaceRemovalStrategy = WhitespaceHandlingStrategy.TrimPerNewline;
-
-            Logger.Current.Warn( e, "An error occurred while configuring 'WhitespaceRemovalStrategy'. Using default." );
+            ApplicationName = "Unknown";
+            XuaLogger.Current.Error( e, "An error occurred while getting application name." );
          }
 
-         UserAgent = Config.Current.Preferences[ "Http" ][ "UserAgent" ].GetOrDefault( string.Empty );
-
-         GoogleAPIKey = Config.Current.Preferences[ "GoogleLegitimate" ][ "GoogleAPIKey" ].GetOrDefault( "" );
 
-         BingOcpApimSubscriptionKey = Config.Current.Preferences[ "BingLegitimate" ][ "OcpApimSubscriptionKey" ].GetOrDefault( "" );
+         ServiceEndpoint = PluginEnvironment.Current.Preferences.GetOrDefault( "Service", "Endpoint", KnownTranslateEndpointNames.GoogleTranslate );
+
+         Language = string.Intern( PluginEnvironment.Current.Preferences.GetOrDefault( "General", "Language", DefaultLanguage ) );
+         FromLanguage = string.Intern( PluginEnvironment.Current.Preferences.GetOrDefault( "General", "FromLanguage", DefaultFromLanguage ) );
+
+         TranslationDirectory = PluginEnvironment.Current.Preferences.GetOrDefault( "Files", "Directory", "Translation" );
+         OutputFile = PluginEnvironment.Current.Preferences.GetOrDefault( "Files", "OutputFile", @"Translation\_AutoGeneratedTranslations.{lang}.txt" );
+
+         EnableIMGUI = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableIMGUI", false );
+         EnableUGUI = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableUGUI", true );
+         EnableNGUI = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableNGUI", true );
+         EnableTextMeshPro = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableTextMeshPro", true );
+         EnableUtage = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableUtage", true );
+         AllowPluginHookOverride = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "AllowPluginHookOverride", true );
+
+         Delay = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "Delay", 0f );
+         MaxCharactersPerTranslation = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "MaxCharactersPerTranslation", 200 );
+         IgnoreWhitespaceInDialogue = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "IgnoreWhitespaceInDialogue", true );
+         IgnoreWhitespaceInNGUI = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "IgnoreWhitespaceInNGUI", true );
+         MinDialogueChars = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "MinDialogueChars", 20 );
+         ForceSplitTextAfterCharacters = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "ForceSplitTextAfterCharacters", 0 );
+         CopyToClipboard = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "CopyToClipboard", false );
+         MaxClipboardCopyCharacters = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "MaxClipboardCopyCharacters", 450 );
+         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 );
+         ForceUIResizing = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "ForceUIResizing", false );
+         IgnoreTextStartingWith = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "IgnoreTextStartingWith", "\\u180e;" )
+            ?.Split( new[] { ';' }, StringSplitOptions.RemoveEmptyEntries ).Select( x => JsonHelper.Unescape( x ) ).ToArray() ?? new string[ 0 ];
+         TextGetterCompatibilityMode = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "TextGetterCompatibilityMode", false );
+         GameLogTextPaths = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "GameLogTextPaths", string.Empty )
+            ?.Split( new[] { ';' }, StringSplitOptions.RemoveEmptyEntries ).ToHashSet() ?? new HashSet<string>();
+         GameLogTextPaths.RemoveWhere( x => !x.StartsWith( "/" ) ); // clean up to ensure no 'empty' entries
+         WhitespaceRemovalStrategy = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "WhitespaceRemovalStrategy", WhitespaceHandlingStrategy.TrimPerNewline );
+         RomajiPostProcessing = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "RomajiPostProcessing", TextPostProcessing.ReplaceMacronWithCircumflex | TextPostProcessing.RemoveApostrophes );
+
+         TextureDirectory = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "TextureDirectory", @"Translation\Texture" );
+         EnableTextureTranslation = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "EnableTextureTranslation", false );
+         EnableTextureDumping = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "EnableTextureDumping", false );
+         EnableTextureToggling = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "EnableTextureToggling", false );
+         EnableTextureScanOnSceneLoad = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "EnableTextureScanOnSceneLoad", false );
+         EnableSpriteRendererHooking = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "EnableSpriteRendererHooking", false );
+         LoadUnmodifiedTextures = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "LoadUnmodifiedTextures", false );
+         TextureHashGenerationStrategy = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "TextureHashGenerationStrategy", TextureHashGenerationStrategy.FromImageName );
 
-         BaiduAppId = Config.Current.Preferences[ "Baidu" ][ "BaiduAppId" ].GetOrDefault( "" );
-         BaiduAppSecret = Config.Current.Preferences[ "Baidu" ][ "BaiduAppSecret" ].GetOrDefault( "" );
-
-         YandexAPIKey = Config.Current.Preferences[ "Yandex" ][ "YandexAPIKey" ].GetOrDefault( "" );
+         if( MaxCharactersPerTranslation > MaxMaxCharactersPerTranslation )
+         {
+            PluginEnvironment.Current.Preferences[ "Behaviour" ][ "MaxCharactersPerTranslation" ].Value = MaxMaxCharactersPerTranslation.ToString( CultureInfo.InvariantCulture );
+            MaxCharactersPerTranslation = MaxMaxCharactersPerTranslation;
+         }
 
-         WatsonAPIUrl = Config.Current.Preferences[ "Watson" ][ "WatsonAPIUrl" ].GetOrDefault( "" );
-         WatsonAPIUsername = Config.Current.Preferences[ "Watson" ][ "WatsonAPIUsername" ].GetOrDefault( "" );
-         WatsonAPIPassword = Config.Current.Preferences[ "Watson" ][ "WatsonAPIPassword" ].GetOrDefault( "" );
+         UserAgent = PluginEnvironment.Current.Preferences.GetOrDefault( "Http", "UserAgent", string.Empty );
 
-         EnablePrintHierarchy = Config.Current.Preferences[ "Debug" ][ "EnablePrintHierarchy" ].GetOrDefault( false );
-         EnableConsole = Config.Current.Preferences[ "Debug" ][ "EnableConsole" ].GetOrDefault( false );
-         EnableDebugLogs = Config.Current.Preferences[ "Debug" ][ "EnableLog" ].GetOrDefault( false );
+         EnablePrintHierarchy = PluginEnvironment.Current.Preferences.GetOrDefault( "Debug", "EnablePrintHierarchy", false );
+         EnableConsole = PluginEnvironment.Current.Preferences.GetOrDefault( "Debug", "EnableConsole", false );
+         EnableDebugLogs = PluginEnvironment.Current.Preferences.GetOrDefault( "Debug", "EnableLog", false );
 
-         EnableMigrations = Config.Current.Preferences[ "Migrations" ][ "Enable" ].GetOrDefault( true );
-         MigrationsTag = Config.Current.Preferences[ "Migrations" ][ "Tag" ].GetOrDefault( string.Empty );
+         EnableMigrations = PluginEnvironment.Current.Preferences.GetOrDefault( "Migrations", "Enable", true );
+         MigrationsTag = PluginEnvironment.Current.Preferences.GetOrDefault( "Migrations", "Tag", string.Empty );
 
-         AutoTranslationsFilePath = Path.Combine( Config.Current.DataPath, OutputFile.Replace( "{lang}", Language ) );
-         UsesWhitespaceBetweenWords = TextHelper.RequiresWhitespaceUponLineMerging( FromLanguage );
+         AutoTranslationsFilePath = Path.Combine( PluginEnvironment.Current.DataPath, OutputFile.Replace( "{lang}", Language ) ).Replace( "/", "\\" ).Parameterize();
+         UsesWhitespaceBetweenWords = LanguageHelper.RequiresWhitespaceUponLineMerging( FromLanguage );
 
          if( EnableMigrations )
          {
@@ -210,33 +185,13 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
          }
 
          // update tag
-         MigrationsTag = Config.Current.Preferences[ "Migrations" ][ "Tag" ].Value = PluginData.Version;
+         MigrationsTag = PluginEnvironment.Current.Preferences[ "Migrations" ][ "Tag" ].Value = PluginData.Version;
 
-         Config.Current.SaveConfig();
+         PluginEnvironment.Current.SaveConfig();
       }
-
+      
       private static void Migrate()
       {
-         var currentTag = MigrationsTag;
-         var newTag = PluginData.Version;
-
-         // migrate from unknown version to known version. Reset to google translate
-         if( string.IsNullOrEmpty( currentTag ) )
-         {
-            if( ServiceEndpoint == KnownEndpointNames.GoogleTranslateHack )
-            {
-               ServiceEndpoint = Config.Current.Preferences[ "Service" ][ "Endpoint" ].Value = KnownEndpointNames.GoogleTranslate;
-            }
-         }
-      }
-
-      public static string GetUserAgent( string defaultUserAgent )
-      {
-         if( !string.IsNullOrEmpty( UserAgent ) )
-         {
-            return UserAgent;
-         }
-         return defaultUserAgent;
       }
    }
 }

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

@@ -2,11 +2,11 @@
 
 namespace XUnity.AutoTranslator.Plugin.Core
 {
-   public class ConsoleLogger : Logger
+   internal class ConsoleLogger : XuaLogger
    {
       protected override void Log( LogLevel level, string message )
       {
-         Console.WriteLine( $"{GetPrefix( level )} {message}" );
+         Console.WriteLine( $"{GetDefaultPrefix( level )} {message}" );
       }
    }
 }

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

@@ -4,7 +4,7 @@ using System.Reflection;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Constants
 {
-   public static class ClrTypes
+   internal static class ClrTypes
    {
       // TextMeshPro
       public static readonly Type TMP_InputField = FindType( "TMPro.TMP_InputField" );

+ 0 - 28
src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownEndpointNames.cs

@@ -1,28 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace XUnity.AutoTranslator.Plugin.Core.Constants
-{
-   public static class KnownEndpointNames
-   {
-      public const string GoogleTranslate = "GoogleTranslate";
-
-      public const string GoogleTranslateHack = "GoogleTranslateHack";
-
-      public const string GoogleTranslateLegitimate = "GoogleTranslateLegitimate";
-
-      public const string BaiduTranslate = "BaiduTranslate";
-
-      public const string YandexTranslate = "YandexTranslate";
-
-      public const string WatsonTranslate = "WatsonTranslate";
-
-      public const string ExciteTranslate = "ExciteTranslate";
-
-      public const string BingTranslate = "BingTranslate";
-
-      public const string BingTranslateLegitimate = "BingTranslateLegitimate";
-   }
-}

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

@@ -5,7 +5,7 @@ using System.Text;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Constants
 {
-   public static class KnownEvents
+   internal static class KnownEvents
    {
       public static string OnUnableToTranslateUGUI = "OnUnableToTranslateUGUI";
       public static string OnUnableToTranslateTextMeshPro = "OnUnableToTranslateTextMeshPro";

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

@@ -5,7 +5,7 @@ using System.Text;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Constants
 {
-   public static class KnownPlugins
+   internal static class KnownPlugins
    {
       public const string DynamicTranslationLoader = "com.bepis.bepinex.dynamictranslationloader";
    }

+ 23 - 0
src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownTranslateEndpointNames.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Constants
+{
+   /// <summary>
+   /// Class representing known endpoint names.
+   /// </summary>
+   public static class KnownTranslateEndpointNames
+   {
+      /// <summary>
+      /// Gets the id used by GoogleTranslate.
+      /// </summary>
+      public const string GoogleTranslate = "GoogleTranslate";
+
+      /// <summary>
+      /// Gets the id used by GoogleTranslate legitimate.
+      /// </summary>
+      public const string GoogleTranslateLegitimate = "GoogleTranslateLegitimate";
+   }
+}

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

@@ -5,12 +5,24 @@ using System.Text;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Constants
 {
+   /// <summary>
+   /// Class providing information about the plugin.
+   /// </summary>
    public static class PluginData
    {
+      /// <summary>
+      /// Gets a globally unique identifier.
+      /// </summary>
       public const string Identifier = "gravydevsupreme.xunity.autotranslator";
 
+      /// <summary>
+      /// Gets a friendly name for the plugin.
+      /// </summary>
       public const string Name = "XUnity Auto Translator";
 
-      public const string Version = "2.18.0";
+      /// <summary>
+      /// Gets the version of the plugin.
+      /// </summary>
+      public const string Version = "3.0.0";
    }
 }

+ 35 - 0
src/XUnity.AutoTranslator.Plugin.Core/Constants/UserAgents.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Constants
+{
+   /// <summary>
+   /// Class defining user agents for various browsers.
+   ///
+   /// Updated 2019-02-09.
+   /// </summary>
+   public static class UserAgents
+   {
+      /// <summary>
+      /// Latest Chrome Win10 user-agent as of 2019-02-09.
+      /// </summary>
+      public static readonly string Chrome_Win10_Latest = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36";
+
+      /// <summary>
+      /// Latest Chrome Win7 user-agent as of 2019-02-09.
+      /// </summary>
+      public static readonly string Chrome_Win7_Latest = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36";
+
+      /// <summary>
+      /// Latest Firefox Win10 user-agent as of 2019-02-09.
+      /// </summary>
+      public static readonly string Firefox_Win10_Latest = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0";
+
+      /// <summary>
+      /// Latest Edge Win10 user-agent as of 2019-02-09.
+      /// </summary>
+      public static readonly string Edge_Win10_Latest = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134";
+   }
+}

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/Debugging/DebugConsole.cs

@@ -6,7 +6,7 @@ using System.Text;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Debugging
 {
-   public static class DebugConsole
+   internal static class DebugConsole
    {
       private static IntPtr _consoleOut;
 

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/Debugging/Kernel32.cs

@@ -6,7 +6,7 @@ using System.Text;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Debugging
 {
-   public static class Kernel32
+   internal static class Kernel32
    {
       [DllImport( "kernel32.dll", SetLastError = true )]
       public static extern bool AllocConsole();

+ 3 - 3
src/XUnity.AutoTranslator.Plugin.Core/Configuration/DefaultConfiguration.cs → src/XUnity.AutoTranslator.Plugin.Core/DefaultPluginEnvironment.cs

@@ -5,15 +5,15 @@ using System.Linq;
 using System.Text;
 using ExIni;
 
-namespace XUnity.AutoTranslator.Plugin.Core.Configuration
+namespace XUnity.AutoTranslator.Plugin.Core
 {
-   public class DefaultConfiguration : IConfiguration
+   internal class DefaultPluginEnvironment : IPluginEnvironment
    {
       private IniFile _file;
       private string _configPath;
       private string _dataFolder;
 
-      public DefaultConfiguration()
+      public DefaultPluginEnvironment()
       {
          _dataFolder = "AutoTranslator";
          _configPath = Path.Combine( _dataFolder, "Config.ini" );

+ 49 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/AssemblyLoader.cs

@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
+{
+   internal static class AssemblyLoader
+   {
+      internal static List<Type> GetAllTypesOf<TService>( string directory )
+      {
+         if( !Directory.Exists( directory ) ) return new List<Type>();
+
+         var files = Directory.GetFiles( directory, "*.dll" );
+         var allTypes = new HashSet<Type>();
+
+         foreach( var file in files )
+         {
+            try
+            {
+               var assemblyName = AssemblyName.GetAssemblyName( file );
+               var assembly = Assembly.Load( assemblyName );
+               var types = GetAllTypesOf<TService>( assembly );
+
+               foreach( var type in types )
+               {
+                  allTypes.Add( type );
+               }
+            }
+            catch( Exception e )
+            {
+               XuaLogger.Current.Error( e, "An error occurred while loading types in assembly: " + file );
+            }
+         }
+
+         return allTypes.ToList();
+      }
+
+      internal static List<Type> GetAllTypesOf<TService>( Assembly assembly )
+      {
+         return assembly
+            .GetTypes()
+            .Where( x => typeof( TService ).IsAssignableFrom( x ) && !x.IsAbstract && !x.IsInterface )
+            .ToList();
+      }
+   }
+}

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

@@ -0,0 +1,92 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
+{
+   internal class ConfiguredEndpoint
+   {
+      private int _ongoingTranslations;
+      private Dictionary<string, byte> _failedTranslations;
+
+      public ConfiguredEndpoint( ITranslateEndpoint endpoint, Exception error )
+      {
+         Endpoint = endpoint;
+         Error = error;
+         _ongoingTranslations = 0;
+         _failedTranslations = new Dictionary<string, byte>();
+      }
+
+      public ITranslateEndpoint Endpoint { get; }
+
+      public Exception Error { get; }
+
+      public bool IsBusy => _ongoingTranslations >= Endpoint.MaxConcurrency;
+
+      public bool CanTranslate( string untranslatedText )
+      {
+         if( _failedTranslations.TryGetValue( untranslatedText, out var count ) )
+         {
+            return count < Settings.MaxFailuresForSameTextPerEndpoint;
+         }
+         return true;
+      }
+
+      public void RegisterTranslationFailureFor( string untranslatedText )
+      {
+         byte count;
+         if( !_failedTranslations.TryGetValue( untranslatedText, out count ) )
+         {
+            count = 1;
+         }
+         else
+         {
+            count++;
+         }
+
+         _failedTranslations[ untranslatedText ] = count;
+      }
+
+      public IEnumerator Translate( string[] untranslatedTexts, string from, string to, Action<string[]> success, Action<string, Exception> failure )
+      {
+         var context = new TranslationContext( untranslatedTexts, from, to, success, failure );
+         _ongoingTranslations++;
+
+         bool ok = false;
+         IEnumerator iterator = null;
+         try
+         {
+            iterator = Endpoint.Translate( context );
+            if( iterator != null )
+            {
+               TryMe: try
+               {
+                  ok = iterator.MoveNext();
+               }
+               catch( TranslationContextException )
+               {
+                  ok = false;
+               }
+               catch( Exception e )
+               {
+                  ok = false;
+                  context.FailWithoutThrowing( "Error occurred during translation.", e );
+               }
+
+               if( ok )
+               {
+                  yield return iterator.Current;
+                  goto TryMe;
+               }
+            }
+         }
+         finally
+         {
+            _ongoingTranslations--;
+
+            context.FailIfNotCompleted();
+         }
+      }
+   }
+}

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

@@ -0,0 +1,280 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using UnityEngine;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+using XUnity.AutoTranslator.Plugin.ExtProtocol;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.ExtProtocol
+{
+   /// <summary>
+   /// An implementation of ITranslateEndpoint that simplifies implementing
+   /// the interface based on an external program.
+   /// </summary>
+   public abstract class ExtProtocolEndpoint : MonoBehaviour, ITranslateEndpoint, IDisposable
+   {
+      private readonly Dictionary<Guid, ProtocolTransactionHandle> _transactionHandles = new Dictionary<Guid, ProtocolTransactionHandle>();
+      private readonly object _sync = new object();
+
+      private bool _disposed = false;
+      private Process _process;
+      private Thread _thread;
+      private bool _startedThread;
+      private bool _initializing;
+      private bool _failed;
+
+      /// <summary>
+      /// Gets the id of the ITranslateEndpoint that is used as a configuration parameter.
+      /// </summary>
+      public abstract string Id { get; }
+
+      /// <summary>
+      /// Gets a friendly name that can be displayed to the user representing the plugin.
+      /// </summary>
+      public abstract string FriendlyName { get; }
+
+      /// <summary>
+      /// Gets the maximum concurrency for the endpoint. This specifies how many times "Translate"
+      /// can be called before it returns.
+      /// </summary>
+      public virtual int MaxConcurrency => 1;
+
+      /// <summary>
+      /// Gets the maximum number of translations that can be served per translation request.
+      /// </summary>
+      public virtual int MaxTranslationsPerRequest => 1;
+
+      /// <summary>
+      /// Gets the path to the executable that should be communicated with.
+      /// </summary>
+      protected string ExecutablePath { get; set; }
+
+      /// <summary>
+      /// Gets the arguments that should be supplied to the executable.
+      /// </summary>
+      protected string Arguments { get; set; }
+
+      /// <summary>
+      /// Called during initialization. Use this to initialize plugin or throw exception if impossible.
+      ///
+      /// Must set the ExecutablePath and optionally the Arguments property.
+      /// </summary>
+      public abstract void Initialize( IInitializationContext context );
+
+      private void EnsureInitialized()
+      {
+         if( !_startedThread )
+         {
+            _startedThread = true;
+            _initializing = true;
+
+            _thread = new Thread( ReaderLoop );
+            _thread.IsBackground = true;
+            _thread.Start();
+         }
+      }
+
+      private void ReaderLoop( object state )
+      {
+         try
+         {
+            if( _process == null )
+            {
+               _process = new Process();
+               _process.StartInfo.FileName = ExecutablePath;
+               _process.StartInfo.Arguments = Arguments;
+               _process.EnableRaisingEvents = false;
+               _process.StartInfo.UseShellExecute = false;
+               _process.StartInfo.CreateNoWindow = true;
+               _process.StartInfo.RedirectStandardInput = true;
+               _process.StartInfo.RedirectStandardOutput = true;
+               _process.StartInfo.RedirectStandardError = true;
+               _process.Start();
+
+               // wait a second...
+               _process.WaitForExit( 2500 );
+            }
+
+            if( _process.HasExited )
+            {
+               return;
+            }
+            _initializing = false;
+
+            while( !_disposed )
+            {
+               var returnedPayload = _process.StandardOutput.ReadLine();
+               var response = ExtProtocolConvert.Decode( returnedPayload );
+               HandleProtocolMessageResponse( response );
+            }
+         }
+         catch( Exception e )
+         {
+            _failed = true;
+            _initializing = false;
+
+            XuaLogger.Current.Error( e, "Error occurred while reading standard output from external process." );
+         }
+      }
+
+      void Update()
+      {
+         if( Time.frameCount % 30 == 0 )
+         {
+            lock( _sync )
+            {
+               var time = Time.realtimeSinceStartup;
+
+               List<Guid> idsToRemove = null;
+               foreach( var kvp in _transactionHandles )
+               {
+                  var elapsed = time - kvp.Value.StartTime;
+                  if( elapsed > 60 )
+                  {
+                     if( idsToRemove == null )
+                     {
+                        idsToRemove = new List<Guid>();
+                     }
+
+                     idsToRemove.Add( kvp.Key );
+                     kvp.Value.SetCompleted( null, "Request timed out." );
+                  }
+               }
+
+               if( idsToRemove != null )
+               {
+                  foreach( var id in idsToRemove )
+                  {
+                     _transactionHandles.Remove( id );
+                  }
+               }
+            }
+         }
+      }
+
+      /// <summary>
+      /// Attempt to translated the provided untranslated text. Will be used in a "coroutine",
+      /// so it can be implemented in an asynchronous fashion.
+      /// </summary>
+      public IEnumerator Translate( ITranslationContext context )
+      {
+         EnsureInitialized();
+
+         while( _initializing && !_failed ) yield return new WaitForSeconds( 0.2f ); 
+
+         if( _failed ) context.Fail( "External process failed." );
+
+         var result = new ProtocolTransactionHandle();
+         var id = Guid.NewGuid();
+
+         lock( _sync )
+         {
+            _transactionHandles[ id ] = result;
+         }
+
+         try
+         {
+            var request = new TranslationRequest
+            {
+               Id = id,
+               SourceLanguage = context.SourceLanguage,
+               DestinationLanguage = context.DestinationLanguage,
+               UntranslatedTexts = context.UntranslatedTexts
+            };
+            var payload = ExtProtocolConvert.Encode( request );
+
+            _process.StandardInput.WriteLine( payload );
+         }
+         catch( Exception e )
+         {
+            result.SetCompleted( null, e.Message );
+         }
+
+         // yield-wait for completion
+         var iterator = result.GetSupportedEnumerator();
+         while( iterator.MoveNext() ) yield return iterator.Current;
+
+         if( !result.Succeeded ) context.Fail( "Error occurred while retrieving translation. " + result.Error );
+
+         context.Complete( result.Results );
+      }
+
+      private void HandleProtocolMessageResponse( ProtocolMessage message )
+      {
+         switch( message )
+         {
+            case TranslationResponse translationResponse:
+               HandleTranslationResponse( translationResponse );
+               break;
+            case TranslationError translationResponse:
+               HandleTranslationError( translationResponse );
+               break;
+            default:
+               break;
+         }
+      }
+
+      private void HandleTranslationResponse( TranslationResponse message )
+      {
+         lock( _sync )
+         {
+            if( _transactionHandles.TryGetValue( message.Id, out var result ) )
+            {
+               result.SetCompleted( message.TranslatedTexts, null );
+               _transactionHandles.Remove( message.Id );
+            }
+         }
+      }
+
+      private void HandleTranslationError( TranslationError message )
+      {
+         lock( _sync )
+         {
+            if( _transactionHandles.TryGetValue( message.Id, out var result ) )
+            {
+               result.SetCompleted( null, message.Reason );
+               _transactionHandles.Remove( message.Id );
+            }
+         }
+      }
+
+      #region IDisposable Support
+
+      /// <summary>
+      /// Disposes the endpoint and kills the external process.
+      /// </summary>
+      /// <param name="disposing"></param>
+      protected virtual void Dispose( bool disposing )
+      {
+         if( !_disposed )
+         {
+            if( disposing )
+            {
+               if( _process != null )
+               {
+                  _process.Kill();
+                  _process.Dispose();
+                  _thread.Abort();
+               }
+            }
+
+            _disposed = true;
+         }
+      }
+
+      /// <summary>
+      /// Disposes the endpoint and kills the external process.
+      /// </summary>
+      public void Dispose()
+      {
+         Dispose( true );
+      }
+
+      #endregion
+   }
+}

+ 32 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ExtProtocol/ProtocolTransactionHandle.cs

@@ -0,0 +1,32 @@
+using UnityEngine;
+using XUnity.AutoTranslator.Plugin.Core.Shim;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.ExtProtocol
+{
+   internal class ProtocolTransactionHandle : CustomYieldInstructionShim
+   {
+      public ProtocolTransactionHandle()
+      {
+         StartTime = Time.realtimeSinceStartup;
+      }
+
+      public void SetCompleted( string[] translatedTexts, string error )
+      {
+         IsCompleted = true;
+         Results = translatedTexts;
+         Error = error;
+      }
+
+      public override bool keepWaiting => !IsCompleted;
+
+      public float StartTime { get; set; }
+
+      public string[] Results { get; set; }
+
+      public string Error { get; set; }
+
+      public bool IsCompleted { get; private set; } = false;
+
+      public bool Succeeded => Error == null;
+   }
+}

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

@@ -0,0 +1,104 @@
+using System;
+using System.Collections;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
+{
+   /// <summary>
+   /// An implementation of ITranslateEndpoint that simplifies implementing
+   /// the interface based on a web service.
+   /// </summary>
+   public abstract class HttpEndpoint : ITranslateEndpoint
+   {
+      /// <summary>
+      /// Gets the id of the ITranslateEndpoint that is used as a configuration parameter.
+      /// </summary>
+      public abstract string Id { get; }
+
+      /// <summary>
+      /// Gets a friendly name that can be displayed to the user representing the plugin.
+      /// </summary>
+      public abstract string FriendlyName { get; }
+
+      /// <summary>
+      /// Gets the maximum concurrency for the endpoint. This specifies how many times "Translate"
+      /// can be called before it returns.
+      /// </summary>
+      public int MaxConcurrency => 1;
+
+      /// <summary>
+      /// Gets the maximum number of translations that can be served per translation request.
+      /// </summary>
+      public virtual int MaxTranslationsPerRequest => 1;
+
+      /// <summary>
+      /// Called during initialization. Use this to initialize plugin or throw exception if impossible.
+      /// </summary>
+      public abstract void Initialize( IInitializationContext context );
+
+      /// <summary>
+      /// Callback that can be overwritten that is called before any requests are sent out.
+      /// </summary>
+      /// <param name="context"></param>
+      /// <returns></returns>
+      public virtual IEnumerator OnBeforeTranslate( IHttpTranslationContext context ) => null;
+
+      /// <summary>
+      /// Callback that must be overwritten to create the request object.
+      /// </summary>
+      /// <param name="context"></param>
+      public abstract void OnCreateRequest( IHttpRequestCreationContext context );
+
+      /// <summary>
+      /// Callback that can be overwritten to inspect the response before extracting the text.
+      /// </summary>
+      /// <param name="context"></param>
+      public virtual void OnInspectResponse( IHttpResponseInspectionContext context ) { }
+
+      /// <summary>
+      /// Callback that must overwritten to extract the text from the web response.
+      /// </summary>
+      /// <param name="context"></param>
+      public abstract void OnExtractTranslation( IHttpTranslationExtractionContext context );
+
+      /// <summary>
+      /// Attempt to translated the provided untranslated text. Will be used in a "coroutine",
+      /// so it can be implemented in an asynchronous fashion.
+      /// </summary>
+      public IEnumerator Translate( ITranslationContext context )
+      {
+         var httpContext = new HttpTranslationContext( context );
+
+         // allow implementer of HttpEndpoint to do anything before starting translation
+         var setup = OnBeforeTranslate( httpContext );
+         if( setup != null )
+         {
+            while( setup.MoveNext() ) yield return setup.Current;
+         }
+
+         // prepare request
+         OnCreateRequest( httpContext );
+         if( httpContext.Request == null ) httpContext.Fail( "No request object was provided by the translator." );
+         
+         // execute request
+         var client = new XUnityWebClient();
+         var response = client.Send( httpContext.Request );
+         
+         // wait for completion
+         var iterator = response.GetSupportedEnumerator();
+         while( iterator.MoveNext() ) yield return iterator.Current;
+
+         httpContext.Response = response;
+         OnInspectResponse( httpContext );
+
+         // failure
+         if( response.Error != null ) httpContext.Fail( "Error occurred while retrieving translation.", response.Error ); 
+
+         // failure
+         if( response.Data == null ) httpContext.Fail( "Error occurred while retrieving translation. Nothing was returned." );
+
+         // extract text
+         OnExtractTranslation( httpContext );
+      }
+   }
+}

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

@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
+{
+   internal class HttpTranslationContext : IHttpTranslationContext, IHttpRequestCreationContext, IHttpResponseInspectionContext, IHttpTranslationExtractionContext
+   {
+      private readonly ITranslationContext _context;
+
+      internal HttpTranslationContext( ITranslationContext context )
+      {
+         _context = context;
+      }
+
+      public string UntranslatedText => _context.UntranslatedText;
+      public string[] UntranslatedTexts => _context.UntranslatedTexts;
+      public string SourceLanguage => _context.SourceLanguage;
+      public string DestinationLanguage => _context.DestinationLanguage;
+      public XUnityWebResponse Response { get; internal set; }
+      public XUnityWebRequest Request { get; internal set; }
+
+      public void Fail( string reason, Exception exception )
+      {
+         _context.Fail( reason, exception );
+      }
+
+      public void Fail( string reason )
+      {
+         _context.Fail( reason );
+      }
+
+
+      void IHttpRequestCreationContext.Complete( XUnityWebRequest request )
+      {
+         Request = request;
+      }
+
+      void IHttpTranslationExtractionContext.Complete( string translatedText )
+      {
+         _context.Complete( translatedText );
+      }
+
+      void IHttpTranslationExtractionContext.Complete( string[] translatedTexts )
+      {
+         _context.Complete( translatedTexts );
+      }
+   }
+}

+ 16 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/IHttpRequestCreationContext.cs

@@ -0,0 +1,16 @@
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
+{
+   /// <summary>
+   /// Interface used in the context of creating a new web request object.
+   /// </summary>
+   public interface IHttpRequestCreationContext : IHttpTranslationContext
+   {
+      /// <summary>
+      /// Completes the callback by specifying the created request to use.
+      /// </summary>
+      /// <param name="request"></param>
+      void Complete( XUnityWebRequest request );
+   }
+}

+ 20 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/IHttpResponseInspectionContext.cs

@@ -0,0 +1,20 @@
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
+{
+   /// <summary>
+   /// Interface used in the context of inspecting a web response object.
+   /// </summary>
+   public interface IHttpResponseInspectionContext : IHttpTranslationContext
+   {
+      /// <summary>
+      /// Gets the web request.
+      /// </summary>
+      XUnityWebRequest Request { get; }
+
+      /// <summary>
+      /// Gets the web response.
+      /// </summary>
+      XUnityWebResponse Response { get; }
+   }
+}

+ 11 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/IHttpTranslationContext.cs

@@ -0,0 +1,11 @@
+using System;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
+{
+   /// <summary>
+   /// Interface used in the context of the HttpEndpoint.
+   /// </summary>
+   public interface IHttpTranslationContext : ITranslationContextBase
+   {
+   }
+}

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

@@ -0,0 +1,25 @@
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
+{
+   /// <summary>
+   /// Interface used in the context of extracting a translated text from a web response.
+   /// </summary>
+   public interface IHttpTranslationExtractionContext : IHttpResponseInspectionContext
+   {
+      /// <summary>
+      /// Completes the translation by providing the translated text.
+      /// </summary>
+      /// <param name="translatedText"></param>
+      void Complete( string translatedText );
+
+      /// <summary>
+      /// Completes the translation by providing the translated texts.
+      ///
+      /// The indices of the translations must match the indices of the
+      /// untranslated texts.
+      /// </summary>
+      /// <param name="translatedTexts"></param>
+      void Complete( string[] translatedTexts );
+   }
+}

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

@@ -0,0 +1,48 @@
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
+{
+   /// <summary>
+   /// Interface used in context of initializing a translator plugin.
+   /// </summary>
+   public interface IInitializationContext
+   {
+      /// <summary>
+      /// Gets the directory where the configuration file and translations are stored.
+      /// </summary>
+      string PluginDirectory { get; }
+
+      /// <summary>
+      /// Gets or creates the specified setting.
+      /// </summary>
+      /// <typeparam name="T"></typeparam>
+      /// <param name="section"></param>
+      /// <param name="key"></param>
+      /// <param name="defaultValue"></param>
+      /// <returns></returns>
+      T GetOrCreateSetting<T>( string section, string key, T defaultValue );
+
+      /// <summary>
+      /// Gets or creates the specified setting.
+      /// </summary>
+      /// <typeparam name="T"></typeparam>
+      /// <param name="section"></param>
+      /// <param name="key"></param>
+      /// <returns></returns>
+      T GetOrCreateSetting<T>( string section, string key );
+
+      /// <summary>
+      /// Disables the certificate check for the specified hostnames.
+      /// </summary>
+      /// <param name="hosts"></param>
+      void DisableCertificateChecksFor( params string[] hosts );
+
+      /// <summary>
+      /// Gets the source language that the plugin is configured with.
+      /// </summary>
+      string SourceLanguage { get; }
+
+      /// <summary>
+      /// Gets the destination language that the plugin is configured with.
+      /// </summary>
+      string DestinationLanguage { get; }
+   }
+}

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

@@ -0,0 +1,47 @@
+using System;
+using System.Collections;
+using System.IO;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
+{
+   /// <summary>
+   /// The interface that must be implemented by a translator.
+   /// </summary>
+   public interface ITranslateEndpoint
+   {
+      /// <summary>
+      /// Gets the id of the ITranslateEndpoint that is used as a configuration parameter.
+      /// </summary>
+      string Id { get; }
+
+      /// <summary>
+      /// Gets a friendly name that can be displayed to the user representing the plugin.
+      /// </summary>
+      string FriendlyName { get; }
+
+      /// <summary>
+      /// Gets the maximum concurrency for the endpoint. This specifies how many times "Translate"
+      /// can be called before it returns.
+      /// </summary>
+      int MaxConcurrency { get; }
+
+      /// <summary>
+      /// Gets the maximum number of translations that can be served per translation request.
+      /// </summary>
+      int MaxTranslationsPerRequest { get; }
+
+      /// <summary>
+      /// Called during initialization. Use this to initialize plugin or throw exception if impossible.
+      /// </summary>
+      void Initialize( IInitializationContext context );
+
+      /// <summary>
+      /// Attempt to translated the provided untranslated text. Will be used in a "coroutine",
+      /// so it can be implemented in an asynchronous fashion.
+      /// </summary>
+      IEnumerator Translate( ITranslationContext context );
+   }
+}

+ 23 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ITranslationContext.cs

@@ -0,0 +1,23 @@
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
+{
+   /// <summary>
+   /// Interface used in the context of translating a text.
+   /// </summary>
+   public interface ITranslationContext : ITranslationContextBase
+   {
+      /// <summary>
+      /// Completes the translation by providing the translated text.
+      /// </summary>
+      /// <param name="translatedText"></param>
+      void Complete( string translatedText );
+
+      /// <summary>
+      /// Completes the translation by providing the translated texts.
+      ///
+      /// The indices of the translations must match the indices of the
+      /// untranslated texts.
+      /// </summary>
+      /// <param name="translatedTexts"></param>
+      void Complete( string[] translatedTexts );
+   }
+}

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

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

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

@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
+{
+   internal class InitializationContext : IInitializationContext
+   {
+      private HttpSecurity _security;
+
+      internal InitializationContext(
+         HttpSecurity httpSecurity,
+         string sourceLanguage,
+         string destinationLanguage )
+      {
+         _security = httpSecurity;
+
+         SourceLanguage = sourceLanguage;
+         DestinationLanguage = destinationLanguage;
+      }
+
+      /// <summary>
+      /// Gets the source language.
+      /// </summary>
+      public string SourceLanguage { get; }
+
+      /// <summary>
+      /// Gets the destination language.
+      /// </summary>
+      public string DestinationLanguage { get; }
+
+      public string PluginDirectory => PluginEnvironment.Current.DataPath;
+
+      public void DisableCertificateChecksFor( params string[] hosts )
+      {
+         _security.EnableSslFor( hosts );
+      }
+
+      public T GetOrCreateSetting<T>( string section, string key, T defaultValue )
+      {
+         return PluginEnvironment.Current.Preferences.GetOrDefault( section, key, defaultValue );
+      }
+
+      public T GetOrCreateSetting<T>( string section, string key )
+      {
+         return PluginEnvironment.Current.Preferences.GetOrDefault( section, key, default( T ) );
+      }
+   }
+}

+ 66 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/KnownTranslateEndpoints.cs

@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
+{
+   internal static class KnownTranslateEndpoints
+   {
+      public static List<ConfiguredEndpoint> CreateEndpoints( GameObject go, InitializationContext context )
+      {
+         var endpoints = new List<ConfiguredEndpoint>();
+
+         // could dynamically load types from other assemblies...
+         //var integratedTypes = AssemblyLoader.GetAllTypesOf<ITranslateEndpoint>( typeof( KnownEndpoints ).Assembly );
+         var pluginFolder = Path.Combine( PluginEnvironment.Current.DataPath, Settings.PluginFolder );
+         var dynamicTypes = AssemblyLoader.GetAllTypesOf<ITranslateEndpoint>( pluginFolder );
+
+         foreach( var type in dynamicTypes )
+         {
+            AddEndpoint( go, context, endpoints, type );
+         }
+
+         return endpoints;
+      }
+
+      private static void AddEndpoint( GameObject go, InitializationContext context, List<ConfiguredEndpoint> endpoints, Type type )
+      {
+         ITranslateEndpoint endpoint;
+         try
+         {
+            if( typeof( MonoBehaviour ).IsAssignableFrom( type ) )
+            {
+               // allow implementing plugins to hook into Unity lifecycle
+               endpoint = (ITranslateEndpoint)go.AddComponent( type );
+               UnityEngine.Object.DontDestroyOnLoad( (UnityEngine.Object)endpoint );
+            }
+            else
+            {
+               // or... just use any old object
+               endpoint = (ITranslateEndpoint)Activator.CreateInstance( type );
+            }
+         }
+         catch( Exception e )
+         {
+            XuaLogger.Current.Error( e, "Could not instantiate class: " + type.Name );
+            return;
+         }
+
+         try
+         {
+            endpoint.Initialize( context );
+            endpoints.Add( new ConfiguredEndpoint( endpoint, null ) );
+         }
+         catch( Exception e )
+         {
+            endpoints.Add( new ConfiguredEndpoint( endpoint, e ) );
+         }
+      }
+   }
+}

+ 134 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationContext.cs

@@ -0,0 +1,134 @@
+using System;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
+{
+   internal class TranslationContext : ITranslationContext
+   {
+      private Action<string[]> _complete;
+      private Action<string, Exception> _fail;
+
+      public TranslationContext(
+         string[] untranslatedTexts,
+         string sourceLanguage,
+         string destinationLanguage,
+         Action<string[]> complete,
+         Action<string, Exception> fail )
+      {
+         UntranslatedTexts = untranslatedTexts;
+         SourceLanguage = sourceLanguage;
+         DestinationLanguage = destinationLanguage;
+
+         _complete = complete;
+         _fail = fail;
+      }
+
+      public string UntranslatedText => UntranslatedTexts[ 0 ];
+      public string[] UntranslatedTexts { get; }
+      public string SourceLanguage { get; }
+      public string DestinationLanguage { get; }
+
+      internal bool IsDone { get; private set; }
+
+
+      public void Complete( string translatedText )
+      {
+         Complete( new[] { translatedText } );
+      }
+
+      public void Complete( string[] translatedTexts )
+      {
+         if( IsDone ) return;
+
+         try
+         {
+            if( translatedTexts.Length == 0 )
+            {
+               _fail( "Received empty translation from translator.", null );
+               return;
+            }
+
+            for( int i = 0 ; i < translatedTexts.Length ; i++ )
+            {
+               var translatedText = translatedTexts[ 0 ];
+               if( string.IsNullOrEmpty( translatedText ) )
+               {
+                  _fail( "Received empty translation from translator.", null );
+                  return;
+               }
+            }
+
+            _complete( translatedTexts );
+         }
+         finally
+         {
+            IsDone = true;
+         }
+      }
+
+      public void Fail( string reason, Exception exception )
+      {
+         if( IsDone ) return;
+
+         try
+         {
+            _fail( reason, exception );
+
+            throw new TranslationContextException();
+         }
+         finally
+         {
+            IsDone = true;
+         }
+      }
+
+      public void Fail( string reason )
+      {
+         if( IsDone ) return;
+
+         try
+         {
+            _fail( reason, null );
+
+            throw new TranslationContextException();
+         }
+         finally
+         {
+            IsDone = true;
+         }
+      }
+
+      internal void FailWithoutThrowing( string reason, Exception exception )
+      {
+         if( IsDone ) return;
+
+         try
+         {
+            _fail( reason, exception );
+         }
+         finally
+         {
+            IsDone = true;
+         }
+      }
+
+      internal void FailIfNotCompleted()
+      {
+         if( !IsDone )
+         {
+            FailWithoutThrowing( "The translation request was not completed before returning from translator.", null );
+         }
+      }
+   }
+
+
+   [Serializable]
+   internal class TranslationContextException : Exception
+   {
+      public TranslationContextException() { }
+      public TranslationContextException( string message ) : base( message ) { }
+      public TranslationContextException( string message, Exception inner ) : base( message, inner ) { }
+      protected TranslationContextException(
+       System.Runtime.Serialization.SerializationInfo info,
+       System.Runtime.Serialization.StreamingContext context ) : base( info, context ) { }
+   }
+}

+ 14 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/IWwwRequestCreationContext.cs

@@ -0,0 +1,14 @@
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
+{
+   /// <summary>
+   /// Interface used in the context of creating a new web request object.
+   /// </summary>
+   public interface IWwwRequestCreationContext : IWwwTranslationContext
+   {
+      /// <summary>
+      /// Completes the callback by specifying the created request to use.
+      /// </summary>
+      /// <param name="requestInfo"></param>
+      void Complete( WwwRequestInfo requestInfo );
+   }
+}

+ 11 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/IWwwTranslationContext.cs

@@ -0,0 +1,11 @@
+using System;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
+{
+   /// <summary>
+   /// Interface used in the context of the WwwEndpoint.
+   /// </summary>
+   public interface IWwwTranslationContext : ITranslationContextBase
+   {
+   }
+}

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

@@ -0,0 +1,28 @@
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
+{
+   /// <summary>
+   /// Interface used in the context of extracting a translated text from a web response.
+   /// </summary>
+   public interface IWwwTranslationExtractionContext : IWwwTranslationContext
+   {
+      /// <summary>
+      /// Gets the response data provided by the web response.
+      /// </summary>
+      string ResponseData { get; }
+
+      /// <summary>
+      /// Completes the translation by providing the translated text.
+      /// </summary>
+      /// <param name="translatedText"></param>
+      void Complete( string translatedText );
+
+      /// <summary>
+      /// Completes the translation by providing the translated texts.
+      ///
+      /// The indices of the translations must match the indices of the
+      /// untranslated texts.
+      /// </summary>
+      /// <param name="translatedTexts"></param>
+      void Complete( string[] translatedTexts );
+   }
+}

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

@@ -0,0 +1,129 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Net;
+using System.Reflection;
+using System.Text;
+using Harmony;
+using UnityEngine;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
+{
+   /// <summary>
+   /// An implementation of ITranslateEndpoint that simplifies implementing
+   /// the interface based on a web service.
+   ///
+   /// Consider using HttpEndpoint instead!
+   /// </summary>
+   public abstract class WwwEndpoint : ITranslateEndpoint
+   {
+      private static readonly ConstructorInfo WwwConstructor = ClrTypes.WWW.GetConstructor( new[] { typeof( string ), typeof( byte[] ), typeof( Dictionary<string, string> ) } );
+
+      /// <summary>
+      /// Gets the id of the ITranslateEndpoint that is used as a configuration parameter.
+      /// </summary>
+      public abstract string Id { get; }
+
+      /// <summary>
+      /// Gets a friendly name that can be displayed to the user representing the plugin.
+      /// </summary>
+      public abstract string FriendlyName { get; }
+
+      /// <summary>
+      /// Gets the maximum concurrency for the endpoint. This specifies how many times "Translate"
+      /// can be called before it returns.
+      /// </summary>
+      public int MaxConcurrency => 1;
+
+      /// <summary>
+      /// Gets the maximum number of translations that can be served per translation request.
+      /// </summary>
+      public virtual int MaxTranslationsPerRequest => 1;
+
+      /// <summary>
+      /// Callback that can be overwritten that is called before any requests are sent out.
+      /// </summary>
+      /// <param name="context"></param>
+      /// <returns></returns>
+      public virtual IEnumerator OnBeforeTranslate( IWwwTranslationContext context ) => null;
+
+      /// <summary>
+      /// Called during initialization. Use this to initialize plugin or throw exception if impossible.
+      /// </summary>
+      public abstract void Initialize( IInitializationContext context );
+
+      /// <summary>
+      /// Callback that must be overwritten to create the request object.
+      /// </summary>
+      /// <param name="context"></param>
+      public abstract void OnCreateRequest( IWwwRequestCreationContext context );
+
+      /// <summary>
+      /// Callback that must overwritten to extract the text from the web response.
+      /// </summary>
+      /// <param name="context"></param>
+      public abstract void OnExtractTranslation( IWwwTranslationExtractionContext context );
+
+      /// <summary>
+      /// Creates a WWW object used to send a web request.
+      ///
+      /// Consider using this instead of default WWW constructor, as that
+      /// may cause issues with different unity versions.
+      /// </summary>
+      /// <param name="address"></param>
+      /// <param name="data"></param>
+      /// <param name="headers"></param>
+      /// <returns></returns>
+      protected WWW CreateWww( string address, byte[] data, Dictionary<string, string> headers )
+      {
+         return (WWW)WwwConstructor.Invoke( new object[] { address, data, headers } );
+      }
+
+      /// <summary>
+      /// Attempt to translated the provided untranslated text. Will be used in a "coroutine",
+      /// so it can be implemented in an asynchronous fashion.
+      /// </summary>
+      public IEnumerator Translate( ITranslationContext context )
+      {
+         var wwwContext = new WwwTranslationContext( context );
+
+         // allow implementer of HttpEndpoint to do anything before starting translation
+         var setup = OnBeforeTranslate( wwwContext );
+         if( setup != null )
+         {
+            while( setup.MoveNext() ) yield return setup.Current; 
+         }
+
+         // prepare request
+         OnCreateRequest( wwwContext );
+         if( wwwContext.RequestInfo == null ) wwwContext.Fail( "No request object was provided by the translator." );
+
+         var request = wwwContext.RequestInfo;
+         var url = request.Address;
+         var data = request.Data;
+         var headers = request.Headers;
+
+         // execute request
+         var www = CreateWww( request.Address, data != null ? Encoding.UTF8.GetBytes( data ) : null, headers );
+
+         // wait for completion
+         yield return www;
+
+         // extract error
+         string error = (string)AccessTools.Property( ClrTypes.WWW, "error" ).GetValue( www, null );
+         if( error != null ) wwwContext.Fail( "Error occurred while retrieving translation. " + error );
+
+         // extract text
+         var text = (string)AccessTools.Property( ClrTypes.WWW, "text" ).GetValue( www, null );
+         if( text == null ) wwwContext.Fail( "Error occurred while extracting text from response." ); 
+
+         wwwContext.ResponseData = text;
+
+         // extract text
+         OnExtractTranslation( wwwContext );
+      }
+   }
+}

+ 59 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/WwwRequestInfo.cs

@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
+{
+   /// <summary>
+   /// Class representing the info required to setup a web request with WWW.
+   /// </summary>
+   public class WwwRequestInfo
+   {
+      private Dictionary<string, string> _headers;
+
+      /// <summary>
+      /// Creates a GET web request.
+      /// </summary>
+      /// <param name="address"></param>
+      public WwwRequestInfo( string address )
+      {
+         Address = address;
+      }
+
+      /// <summary>
+      /// Creates a POST web request.
+      /// </summary>
+      /// <param name="address"></param>
+      /// <param name="data"></param>
+      public WwwRequestInfo( string address, string data )
+      {
+         Address = address;
+         Data = data;
+      }
+
+      /// <summary>
+      /// Gets the the address.
+      /// </summary>
+      public string Address { get; private set; }
+
+      /// <summary>
+      /// Gets the data.
+      /// </summary>
+      public string Data { get; private set; }
+
+      /// <summary>
+      /// Gets or sets the headers of the request.
+      /// </summary>
+      public Dictionary<string, string> Headers
+      {
+         get
+         {
+            if( _headers == null ) _headers = new Dictionary<string, string>();
+
+            return _headers;
+         }
+         set
+         {
+            _headers = value;
+         }
+      }
+   }
+}

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

@@ -0,0 +1,49 @@
+using System;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
+{
+   internal class WwwTranslationContext : IWwwTranslationContext, IWwwRequestCreationContext, IWwwTranslationExtractionContext
+   {
+      private readonly ITranslationContext _context;
+
+      internal WwwTranslationContext( ITranslationContext context )
+      {
+         _context = context;
+      }
+
+      public string[] UntranslatedTexts => _context.UntranslatedTexts;
+      public string UntranslatedText => _context.UntranslatedText;
+      public string SourceLanguage => _context.SourceLanguage;
+      public string DestinationLanguage => _context.DestinationLanguage;
+
+      public string ResponseData { get; internal set; }
+      internal WwwRequestInfo RequestInfo { get; private set; }
+
+      void IWwwRequestCreationContext.Complete( WwwRequestInfo requestInfo )
+      {
+         RequestInfo = requestInfo;
+      }
+
+      void IWwwTranslationExtractionContext.Complete( string translatedText )
+      {
+         _context.Complete( translatedText );
+      }
+
+      void IWwwTranslationExtractionContext.Complete( string[] translatedTexts )
+      {
+         _context.Complete( translatedTexts );
+      }
+
+      public void Fail( string reason, Exception exception )
+      {
+         _context.Fail( reason, exception );
+      }
+
+      public void Fail( string reason )
+      {
+         _context.Fail( reason );
+      }
+   }
+}

+ 0 - 162
src/XUnity.AutoTranslator.Plugin.Core/Extensions/ComponentExtensions.cs

@@ -1,162 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Text;
-using Harmony;
-using UnityEngine;
-using UnityEngine.UI;
-
-namespace XUnity.AutoTranslator.Plugin.Core.Extensions
-{
-   public static class ComponentExtensions
-   {
-      private static readonly string TextPropertyName = "text";
-      private static readonly string TexturePropertyName = "texture";
-      private static readonly string MainTexturePropertyName = "mainTexture";
-      private static readonly string CapitalMainTexturePropertyName = "MainTexture";
-      private static readonly string MarkAsChangedMethodName = "MarkAsChanged";
-
-      public static string GetText( this object ui )
-      {
-         if( ui == null ) return null;
-
-         string text = null;
-         var type = ui.GetType();
-
-         if( ui is Text )
-         {
-            text = ( (Text)ui ).text;
-         }
-         else if( ui is GUIContent )
-         {
-            text = ( (GUIContent)ui ).text;
-         }
-         else
-         {
-            // fallback to reflective approach
-            text = (string)ui.GetType()?.GetProperty( TextPropertyName )?.GetValue( ui, null );
-         }
-
-         return text ?? string.Empty;
-      }
-
-      public static void SetText( this object ui, string text )
-      {
-         if( ui == null ) return;
-
-         var type = ui.GetType();
-
-         if( ui is Text )
-         {
-            ( (Text)ui ).text = text;
-         }
-         else if( ui is GUIContent )
-         {
-            ( (GUIContent)ui ).text = text;
-         }
-         else
-         {
-            // fallback to reflective approach
-            type.GetProperty( TextPropertyName )?.GetSetMethod()?.Invoke( ui, new[] { text } );
-         }
-      }
-
-      public static string GetTextureName( this Texture texture )
-      {
-         if( !string.IsNullOrEmpty( texture.name ) ) return texture.name;
-         return "Unnamed";
-      }
-
-      public static Texture2D GetTexture( this object ui )
-      {
-         if( ui == null ) return null;
-
-         if( ui is Image image )
-         {
-            return image.mainTexture as Texture2D;
-         }
-         else if( ui is RawImage rawImage )
-         {
-            return rawImage.mainTexture as Texture2D;
-         }
-         else if( ui is SpriteRenderer spriteRenderer )
-         {
-            return spriteRenderer.sprite?.texture;
-         }
-         else
-         {
-            // lets attempt some reflection for several known types
-            var type = ui.GetType();
-            var texture = type.GetProperty( MainTexturePropertyName )?.GetValue( ui, null )
-               ?? type.GetProperty( TexturePropertyName )?.GetValue( ui, null )
-               ?? type.GetProperty( CapitalMainTexturePropertyName )?.GetValue( ui, null );
-
-            return texture as Texture2D;
-         }
-      }
-
-      public static void SetAllDirtyEx( this object ui )
-      {
-         if( ui == null ) return;
-
-         if( ui is Graphic graphic )
-         {
-            graphic.SetAllDirty();
-         }
-         else
-         {
-            // lets attempt some reflection for several known types
-            var type = ui.GetType();
-
-            AccessTools.Method( type, MarkAsChangedMethodName )?.Invoke( ui, null );
-         }
-      }
-   }
-
-   public static class UILabelExtensions
-   {
-      private static readonly string SpacingYPropertyName = "spacingY";
-      private static readonly string FloatSpacingYPropertyName = "floatSpacingY";
-      private static readonly string UseFloatSpacingPropertyName = "useFloatSpacing";
-
-      public static object GetSpacingY( this object uiLabel )
-      {
-         var type = uiLabel.GetType();
-         var useFloatSpacing = (bool)type.GetProperty( UseFloatSpacingPropertyName )?.GetGetMethod()?.Invoke( uiLabel, null );
-         if( useFloatSpacing )
-         {
-            return type.GetProperty( FloatSpacingYPropertyName )?.GetGetMethod()?.Invoke( uiLabel, null );
-         }
-         else
-         {
-            return type.GetProperty( SpacingYPropertyName )?.GetGetMethod()?.Invoke( uiLabel, null );
-         }
-      }
-
-      public static void SetSpacingY( this object uiLabel, object spacing )
-      {
-         var type = uiLabel.GetType();
-         if( spacing is float )
-         {
-            type.GetProperty( FloatSpacingYPropertyName )?.GetSetMethod()?.Invoke( uiLabel, new[] { spacing } );
-         }
-         else
-         {
-            type.GetProperty( SpacingYPropertyName )?.GetSetMethod()?.Invoke( uiLabel, new[] { spacing } );
-         }
-      }
-
-      public static object Multiply( this object numeric, float scale )
-      {
-         if( numeric is float )
-         {
-            return (float)numeric * scale;
-         }
-         else
-         {
-            return (int)( (int)numeric * scale );
-         }
-      }
-   }
-}

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

@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Extensions
+{
+   internal static class EnumerableExtensions
+   {
+      public static HashSet<T> ToHashSet<T>( this IEnumerable<T> ts )
+      {
+         var hashSet = new HashSet<T>();
+         foreach( var t in ts )
+         {
+            hashSet.Add( t );
+         }
+         return hashSet;
+      }
+   }
+}

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

@@ -6,8 +6,9 @@ using UnityEngine;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {
-   public static class GameObjectExtensions
+   internal static class GameObjectExtensions
    {
+      private static GameObject[] _objects = new GameObject[ 128 ];
       private static readonly string DummyName = "Dummy";
       private static readonly string XuaIgnore = "XUAIGNORE";
 
@@ -31,6 +32,28 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          return null;
       }
 
+      public static string GetPath( this GameObject obj )
+      {
+         int i = 0;
+         _objects[ i++ ] = obj;
+         while( obj.transform.parent != null )
+         {
+            obj = obj.transform.parent.gameObject;
+            _objects[ i++ ] = obj;
+         }
+
+         StringBuilder path = new StringBuilder();
+         while( --i >= 0 )
+         {
+            path.Append( "/" ).Append( _objects[ i ].name );
+         }
+
+
+         var result = path.ToString();
+
+         return result;
+      }
+
       public static bool HasIgnoredName( this GameObject go )
       {
          return go.name.EndsWith( DummyName ) || go.name.Contains( XuaIgnore ) || go.transform?.parent?.name.EndsWith( DummyName ) == true;

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

@@ -6,7 +6,7 @@ using Harmony;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {
-   public static class HarmonyInstanceExtensions
+   internal static class HarmonyInstanceExtensions
    {
       public static void PatchAll( this HarmonyInstance instance, IEnumerable<Type> types )
       {
@@ -30,7 +30,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          }
          catch( Exception e )
          {
-            Logger.Current.Warn( e, "An error occurred while patching a property/method on a class." );
+            XuaLogger.Current.Warn( e, "An error occurred while patching a property/method on a class. Failing class: " + type.Name );
          }
       }
    }

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

@@ -0,0 +1,82 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using ExIni;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Extensions
+{
+   internal static class IniFileExtensions
+   {
+      public static T GetOrDefault<T>( this IniFile that, string section, string key, T defaultValue )
+      {
+         var typeOfT = typeof( T ).UnwrapNullable();
+         var iniSection = that[ section ];
+         var iniKey = iniSection[ key ];
+
+         try
+         {
+            var value = iniKey.Value;
+
+            if( value == null ) // we want to use the default value, because it is null, the config has just been created
+            {
+               if( defaultValue != null )
+               {
+                  if( typeOfT.IsEnum )
+                  {
+                     iniKey.Value = EnumHelper.GetNames( typeOfT, defaultValue );
+                  }
+                  else
+                  {
+                     iniKey.Value = Convert.ToString( defaultValue, CultureInfo.InvariantCulture );
+                  }
+               }
+               else
+               {
+                  iniKey.Value = string.Empty;
+               }
+               return defaultValue;
+            }
+            else
+            {
+               // there exists a value in the config, so we do not want to set anything
+               // we just want to return what we can find, default not included
+               if( !string.IsNullOrEmpty( value ) )
+               {
+                  if( typeOfT.IsEnum )
+                  {
+                     return (T)EnumHelper.GetValues( typeOfT, iniKey.Value );
+                  }
+                  else
+                  {
+                     return (T)Convert.ChangeType( iniKey.Value, typeOfT, CultureInfo.InvariantCulture );
+                  }
+               }
+               return default( T );
+            }
+         }
+         catch( Exception e )
+         {
+            XuaLogger.Current.Error( e, $"Error occurred while reading config '{key}' in section '{section}'. Updating the config to its default value '{defaultValue}'." );
+
+            if( defaultValue != null )
+            {
+               if( typeOfT.IsEnum )
+               {
+                  iniKey.Value = EnumHelper.GetNames( typeOfT, defaultValue );
+               }
+               else
+               {
+                  iniKey.Value = Convert.ToString( defaultValue, CultureInfo.InvariantCulture );
+               }
+            }
+            else
+            {
+               iniKey.Value = string.Empty;
+            }
+
+            return defaultValue;
+         }
+      }
+   }
+}

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

@@ -3,14 +3,22 @@ using System.Text;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {
+   /// <summary>
+   /// StringBuilder extensions.
+   /// </summary>
    public static class StringBuilderExtensions
    {
+      /// <summary>
+      /// Gets a bool indicating if the current line ends in whitespace or newline.
+      /// </summary>
+      /// <param name="builder"></param>
+      /// <returns></returns>
       public static bool EndsWithWhitespaceOrNewline( this StringBuilder builder )
       {
          if( builder.Length == 0 ) return true;
 
          var lastChar = builder[ builder.Length - 1 ];
-         return Char.IsWhiteSpace( lastChar ) || lastChar == '\n' || lastChar == '\r';
+         return char.IsWhiteSpace( lastChar ) || lastChar == '\n';
       }
    }
 }

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

@@ -8,7 +8,7 @@ using XUnity.AutoTranslator.Plugin.Core.Configuration;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {
-   public static class StringExtensions
+   internal static class StringExtensions
    {
       private static readonly HashSet<char> Numbers = new HashSet<char>
       {
@@ -63,8 +63,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
       private static readonly char[] NewlinesCharacters = new char[] { '\r', '\n' };
       private static readonly char[] WhitespacesAndNewlines = new char[] { '\r', '\n', ' ', ' ' };
 
-
-
       public static string SanitizeForFileSystem( this string path )
       {
          var builder = new StringBuilder( path.Length );
@@ -257,18 +255,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          return builder.ToString();
       }
 
-      public static bool ContainsNumbers( this string text )
-      {
-         foreach( var c in text )
-         {
-            if( Numbers.Contains( c ) )
-            {
-               return true;
-            }
-         }
-         return false;
-      }
-
       public static bool StartsWithStrict( this string str, string prefix )
       {
          var len = Math.Min( str.Length, prefix.Length );
@@ -282,131 +268,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          return true;
       }
 
-      public static string UnescapeJson( this string str )
-      {
-         if( str == null ) return null;
-
-         var builder = new StringBuilder( str );
-
-         bool escapeNext = false;
-         for( int i = 0 ; i < builder.Length ; i++ )
-         {
-            var c = builder[ i ];
-            if( escapeNext )
-            {
-               bool found = true;
-               char escapeWith = default( char );
-               switch( c )
-               {
-                  case 'b':
-                     escapeWith = '\b';
-                     break;
-                  case 'f':
-                     escapeWith = '\f';
-                     break;
-                  case 'n':
-                     escapeWith = '\n';
-                     break;
-                  case 'r':
-                     escapeWith = '\r';
-                     break;
-                  case 't':
-                     escapeWith = '\t';
-                     break;
-                  case '"':
-                     escapeWith = '\"';
-                     break;
-                  case '\\':
-                     escapeWith = '\\';
-                     break;
-                  case 'u':
-                     escapeWith = 'u';
-                     break;
-                  default:
-                     found = false;
-                     break;
-               }
-
-               // remove previous char and go one back
-               if( found )
-               {
-                  if( escapeWith == 'u' )
-                  {
-                     // unicode crap, lets handle the next 4 characters manually
-                     int code = int.Parse( new string( new char[] { builder[ i + 1 ], builder[ i + 2 ], builder[ i + 3 ], builder[ i + 4 ] } ), NumberStyles.HexNumber );
-                     var replacingChar = (char)code;
-                     builder.Remove( --i, 6 );
-                     builder.Insert( i, replacingChar );
-                  }
-                  else
-                  {
-                     // found proper escaping
-                     builder.Remove( --i, 2 );
-                     builder.Insert( i, escapeWith );
-                  }
-               }
-               else
-               {
-                  // dont do anything
-               }
-
-               escapeNext = false;
-            }
-            else if( c == '\\' )
-            {
-               escapeNext = true;
-            }
-         }
-
-         return builder.ToString();
-      }
-
-      public static string EscapeJson( this string str )
-      {
-         if( str == null || str.Length == 0 )
-         {
-            return "";
-         }
-
-         char c;
-         int len = str.Length;
-         StringBuilder sb = new StringBuilder( len + 4 );
-         for( int i = 0 ; i < len ; i += 1 )
-         {
-            c = str[ i ];
-            switch( c )
-            {
-               case '\\':
-               case '"':
-                  sb.Append( '\\' );
-                  sb.Append( c );
-                  break;
-               case '/':
-                  sb.Append( '\\' );
-                  sb.Append( c );
-                  break;
-               case '\b':
-                  sb.Append( "\\b" );
-                  break;
-               case '\t':
-                  sb.Append( "\\t" );
-                  break;
-               case '\n':
-                  sb.Append( "\\n" );
-                  break;
-               case '\f':
-                  sb.Append( "\\f" );
-                  break;
-               case '\r':
-                  sb.Append( "\\r" );
-                  break;
-               default:
-                  sb.Append( c );
-                  break;
-            }
-         }
-         return sb.ToString();
-      }
       public static string GetBetween( this string strSource, string strStart, string strEnd )
       {
          const int kNotFound = -1;
@@ -421,7 +282,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
                return strSource.Substring( startIdx, endIdx - startIdx );
             }
          }
-         return String.Empty;
+         return string.Empty;
       }
 
       public static bool RemindsOf( this string that, string other )

+ 132 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextComponentExtensions.cs

@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+using UnityEngine.UI;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Extensions
+{
+   internal static class TextComponentExtensions
+   {
+      private static readonly string RichTextPropertyName = "richText";
+      private static readonly string TextPropertyName = "text";
+
+      public static bool IsKnownTextType( this object ui )
+      {
+         if( ui == null ) return false;
+
+         var type = ui.GetType();
+
+         return ( Settings.EnableUGUI && ui is Text )
+            || ( Settings.EnableIMGUI && ui is GUIContent )
+            || ( Settings.EnableNGUI && ClrTypes.UILabel != null && ClrTypes.UILabel.IsAssignableFrom( type ) )
+            || ( Settings.EnableTextMeshPro && ClrTypes.TMP_Text != null && ClrTypes.TMP_Text.IsAssignableFrom( type ) )
+            || ( Settings.EnableUtage && ClrTypes.AdvCommand != null && ClrTypes.AdvCommand.IsAssignableFrom( type ) );
+      }
+
+      public static bool SupportsRichText( this object ui )
+      {
+         if( ui == null ) return false;
+
+         var type = ui.GetType();
+
+         return ( ui as Text )?.supportRichText == true
+            || ( ClrTypes.TMP_Text != null && ClrTypes.TMP_Text.IsAssignableFrom( type ) && Equals( type.GetProperty( RichTextPropertyName )?.GetValue( ui, null ), true ) )
+            || ( ClrTypes.AdvCommand != null && ClrTypes.AdvCommand.IsAssignableFrom( type ) )
+            || ( ClrTypes.UguiNovelText != null && ClrTypes.UguiNovelText.IsAssignableFrom( type ) );
+      }
+
+      public static bool SupportsStabilization( this object ui )
+      {
+         if( ui == null ) return false;
+
+         // shortcircuit for spammy component, to avoid reflective calls
+         if( ui is GUIContent ) return false;
+
+         var type = ui.GetType();
+
+         return ui is Text
+            || ( ClrTypes.UILabel != null && ClrTypes.UILabel.IsAssignableFrom( type ) )
+            || ( ClrTypes.TMP_Text != null && ClrTypes.TMP_Text.IsAssignableFrom( type ) );
+      }
+
+      public static bool SupportsLineParser( this object ui )
+      {
+         return Settings.GameLogTextPaths.Count > 0 && ui is Component comp && Settings.GameLogTextPaths.Contains( comp.gameObject.GetPath() );
+      }
+
+      public static bool IsSpammingComponent( this object ui )
+      {
+         if( ui == null ) return true;
+
+         return ui is GUIContent;
+      }
+
+      public static bool IsWhitelistedForImmediateRichTextTranslation( this object ui )
+      {
+         if( ui == null ) return false;
+
+         var type = ui.GetType();
+
+         return ClrTypes.AdvCommand != null && ClrTypes.AdvCommand.IsAssignableFrom( type );
+      }
+
+      public static bool IsNGUI( this object ui )
+      {
+         if( ui == null ) return false;
+
+         var type = ui.GetType();
+
+         return ClrTypes.UILabel != null && ClrTypes.UILabel.IsAssignableFrom( type );
+      }
+
+      public static string GetText( this object ui )
+      {
+         if( ui == null ) return null;
+
+         string text = null;
+         var type = ui.GetType();
+
+         if( ui is Text )
+         {
+            text = ( (Text)ui ).text;
+         }
+         else if( ui is GUIContent )
+         {
+            text = ( (GUIContent)ui ).text;
+         }
+         else
+         {
+            // fallback to reflective approach
+            text = (string)ui.GetType()?.GetProperty( TextPropertyName )?.GetValue( ui, null );
+         }
+
+         return text ?? string.Empty;
+      }
+
+      public static void SetText( this object ui, string text )
+      {
+         if( ui == null ) return;
+
+         var type = ui.GetType();
+
+         if( ui is Text )
+         {
+            ( (Text)ui ).text = text;
+         }
+         else if( ui is GUIContent )
+         {
+            ( (GUIContent)ui ).text = text;
+         }
+         else
+         {
+            // fallback to reflective approach
+            type.GetProperty( TextPropertyName )?.GetSetMethod()?.Invoke( ui, new[] { text } );
+         }
+      }
+   }
+}

+ 59 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextureComponentExtensions.cs

@@ -0,0 +1,59 @@
+using Harmony;
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Extensions
+{
+   internal static class TextureComponentExtensions
+   {
+      private static readonly string TexturePropertyName = "texture";
+      private static readonly string MainTexturePropertyName = "mainTexture";
+      private static readonly string CapitalMainTexturePropertyName = "MainTexture";
+      private static readonly string MarkAsChangedMethodName = "MarkAsChanged";
+
+      public static Texture2D GetTexture( this object ui )
+      {
+         if( ui == null ) return null;
+
+         if( ui is Image image )
+         {
+            return image.mainTexture as Texture2D;
+         }
+         else if( ui is RawImage rawImage )
+         {
+            return rawImage.mainTexture as Texture2D;
+         }
+         else if( ui is SpriteRenderer spriteRenderer )
+         {
+            return spriteRenderer.sprite?.texture;
+         }
+         else
+         {
+            // lets attempt some reflection for several known types
+            var type = ui.GetType();
+            var texture = type.GetProperty( MainTexturePropertyName )?.GetValue( ui, null )
+               ?? type.GetProperty( TexturePropertyName )?.GetValue( ui, null )
+               ?? type.GetProperty( CapitalMainTexturePropertyName )?.GetValue( ui, null );
+
+            return texture as Texture2D;
+         }
+      }
+
+      public static void SetAllDirtyEx( this object ui )
+      {
+         if( ui == null ) return;
+
+         if( ui is Graphic graphic )
+         {
+            graphic.SetAllDirty();
+         }
+         else
+         {
+            // lets attempt some reflection for several known types
+            var type = ui.GetType();
+
+            AccessTools.Method( type, MarkAsChangedMethodName )?.Invoke( ui, null );
+         }
+      }
+   }
+}

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

@@ -1,15 +1,17 @@
 using System;
 using System.Collections.Generic;
+using System.Drawing;
 using System.Linq;
 using System.Reflection;
 using System.Text;
 using Harmony;
 using UnityEngine;
+using UnityEngine.UI;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {
-   public static class TextureExtensions
+   internal static class TextureExtensions
    {
       private static readonly MethodInfo LoadImage = AccessTools.Method( ClrTypes.ImageConversion, "LoadImage", new[] { typeof( Texture2D ), typeof( byte[] ), typeof( bool ) } );
       private static readonly MethodInfo EncodeToPNG = AccessTools.Method( ClrTypes.ImageConversion, "EncodeToPNG", new[] { typeof( Texture2D ) } );
@@ -19,6 +21,24 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
       //   return texture.GetRawTextureData().Length == 0;
       //}
 
+      public static bool IsKnownImageType( this object ui )
+      {
+         var type = ui.GetType();
+
+         return ( ui is Material || ui is UnityEngine.UI.Image || ui is RawImage || ui is SpriteRenderer )
+            || ( ClrTypes.CubismRenderer != null && ClrTypes.CubismRenderer.IsAssignableFrom( type ) )
+            || ( ClrTypes.UIWidget != null && type != ClrTypes.UILabel && ClrTypes.UIWidget.IsAssignableFrom( type ) )
+            || ( ClrTypes.UIAtlas != null && ClrTypes.UIAtlas.IsAssignableFrom( type ) )
+            || ( ClrTypes.UITexture != null && ClrTypes.UITexture.IsAssignableFrom( type ) )
+            || ( ClrTypes.UIPanel != null && ClrTypes.UIPanel.IsAssignableFrom( type ) );
+      }
+
+      public static string GetTextureName( this Texture texture )
+      {
+         if( !string.IsNullOrEmpty( texture.name ) ) return texture.name;
+         return "Unnamed";
+      }
+
       public static void LoadImageEx( this Texture2D texture, byte[] data, bool markNonReadable )
       {
          if( LoadImage != null )

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

@@ -5,7 +5,7 @@ using System.Text;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {
-   public static class TypeExtensions
+   internal static class TypeExtensions
    {
       public static Type UnwrapNullable( this Type type )
       {

+ 48 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/UILabelExtensions.cs

@@ -0,0 +1,48 @@
+namespace XUnity.AutoTranslator.Plugin.Core.Extensions
+{
+   internal static class UILabelExtensions
+   {
+      private static readonly string SpacingYPropertyName = "spacingY";
+      private static readonly string FloatSpacingYPropertyName = "floatSpacingY";
+      private static readonly string UseFloatSpacingPropertyName = "useFloatSpacing";
+
+      public static object GetSpacingY( this object uiLabel )
+      {
+         var type = uiLabel.GetType();
+         var useFloatSpacing = (bool)type.GetProperty( UseFloatSpacingPropertyName )?.GetGetMethod()?.Invoke( uiLabel, null );
+         if( useFloatSpacing )
+         {
+            return type.GetProperty( FloatSpacingYPropertyName )?.GetGetMethod()?.Invoke( uiLabel, null );
+         }
+         else
+         {
+            return type.GetProperty( SpacingYPropertyName )?.GetGetMethod()?.Invoke( uiLabel, null );
+         }
+      }
+
+      public static void SetSpacingY( this object uiLabel, object spacing )
+      {
+         var type = uiLabel.GetType();
+         if( spacing is float )
+         {
+            type.GetProperty( FloatSpacingYPropertyName )?.GetSetMethod()?.Invoke( uiLabel, new[] { spacing } );
+         }
+         else
+         {
+            type.GetProperty( SpacingYPropertyName )?.GetSetMethod()?.Invoke( uiLabel, new[] { spacing } );
+         }
+      }
+
+      public static object Multiply( this object numeric, float scale )
+      {
+         if( numeric is float )
+         {
+            return (float)numeric * scale;
+         }
+         else
+         {
+            return (int)( (int)numeric * scale );
+         }
+      }
+   }
+}

+ 13 - 4
src/XUnity.AutoTranslator.Plugin.Core/Features.cs

@@ -5,13 +5,22 @@ using XUnity.AutoTranslator.Plugin.Core.Constants;
 
 namespace XUnity.AutoTranslator.Plugin.Core
 {
+   /// <summary>
+   /// Class that allows you to check which features are availble of the Unity version that is used.
+   /// </summary>
    public static class Features
    {
-      public static readonly bool SupportsClipboard = false;
+      internal static bool SupportsClipboard { get; } = false;
 
-      public static readonly bool SupportsCustomYieldInstruction = false;
+      /// <summary>
+      /// Gets a bool indicating if the class CustomYieldInstruction is available.
+      /// </summary>
+      public static bool SupportsCustomYieldInstruction { get; } = false;
 
-      public static readonly bool SupportsScenes = false;
+      /// <summary>
+      /// Gets a bool indicating if the SceneManager class is available.
+      /// </summary>
+      public static bool SupportsSceneManager { get; } = false;
 
       static Features()
       {
@@ -35,7 +44,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
          try
          {
-            SupportsScenes = ClrTypes.Scene != null && ClrTypes.SceneManager != null;
+            SupportsSceneManager = ClrTypes.Scene != null && ClrTypes.SceneManager != null;
          }
          catch( Exception )
          {

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/Fonts/FontCache.cs

@@ -7,7 +7,7 @@ using XUnity.AutoTranslator.Plugin.Core.Configuration;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Fonts
 {
-   public static class FontCache
+   internal static class FontCache
    {
       private static readonly Dictionary<int, Font> CachedFonts = new Dictionary<int, Font>();
 

+ 13 - 60
src/XUnity.AutoTranslator.Plugin.Core/Hooks/HooksSetup.cs

@@ -9,15 +9,16 @@ using UnityEngine;
 using XUnity.AutoTranslator.Plugin.Core.Configuration;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Hooks.IMGUI;
 using XUnity.AutoTranslator.Plugin.Core.Hooks.NGUI;
+using XUnity.AutoTranslator.Plugin.Core.Hooks.TextGetterCompat;
 using XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro;
 using XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI;
-using XUnity.AutoTranslator.Plugin.Core.IMGUI;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Hooks
 {
 
-   public static class HooksSetup
+   internal static class HooksSetup
    {
       private static HarmonyInstance _harmony;
 
@@ -57,7 +58,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          }
          catch( Exception e )
          {
-            Logger.Current.Error( e, "An error occurred while setting up text getter compat hooks." );
+            XuaLogger.Current.Error( e, "An error occurred while setting up text getter compat hooks." );
          }
       }
 
@@ -72,49 +73,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          }
          catch( Exception e )
          {
-            Logger.Current.Error( e, "An error occurred while setting up image hooks." );
-         }
-         
-         //var knownTypes = new HashSet<Type> { typeof( Texture ), typeof( Texture2D ), typeof( Sprite ), typeof( Material ) };
-         //foreach( var assembly in AppDomain.CurrentDomain.GetAssemblies() )
-         //{
-         //   try
-         //   {
-         //      var types = assembly.GetTypes();
-         //      foreach( var type in types )
-         //      {
-         //         try
-         //         {
-         //            var properties = type.GetProperties( BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public );
-         //            foreach( var property in properties )
-         //            {
-         //               if( property.CanWrite && knownTypes.Contains( property.PropertyType ) )
-         //               {
-         //                  try
-         //                  {
-         //                     var original = property.GetSetMethod();
-         //                     var prefix = typeof( GenericPrefix_Hook ).GetMethod( "Prefix" );
-         //                     _harmony.Patch( original, new HarmonyMethod( prefix ), null, null );
-         //                     Logger.Current.Warn( "Patched: " + type.Name + "." + property.Name );
-         //                  }
-         //                  catch( Exception e )
-         //                  {
-         //                     Logger.Current.Error( "Failed patching: " + type.Name + "." + property.Name );
-         //                  }
-         //               }
-         //            }
-         //         }
-         //         catch( Exception e )
-         //         {
-         //            Logger.Current.Error( e, "Failed getting properties of type: " + type.Name );
-         //         }
-         //      }
-         //   }
-         //   catch( Exception )
-         //   {
-         //      Logger.Current.Error( "Failed getting types of assembly: " + assembly.FullName );
-         //   }
-         //}
+            XuaLogger.Current.Error( e, "An error occurred while setting up image hooks." );
+         }
       }
 
       public static void InstallTextHooks()
@@ -129,7 +89,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          }
          catch( Exception e )
          {
-            Logger.Current.Error( e, "An error occurred while setting up hooks for UGUI." );
+            XuaLogger.Current.Error( e, "An error occurred while setting up hooks for UGUI." );
          }
 
          try
@@ -141,7 +101,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          }
          catch( Exception e )
          {
-            Logger.Current.Error( e, "An error occurred while setting up hooks for TextMeshPro." );
+            XuaLogger.Current.Error( e, "An error occurred while setting up hooks for TextMeshPro." );
          }
 
          try
@@ -153,7 +113,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          }
          catch( Exception e )
          {
-            Logger.Current.Error( e, "An error occurred while setting up hooks for NGUI." );
+            XuaLogger.Current.Error( e, "An error occurred while setting up hooks for NGUI." );
          }
 
          try
@@ -161,18 +121,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
             if( Settings.EnableIMGUI )
             {
                _harmony.PatchAll( IMGUIHooks.All );
-
-               // This wont work in "newer" unity versions!
-               try
-               {
-                  _harmony.PatchType( typeof( DoButtonGridHook ) );
-               }
-               catch { }
             }
          }
          catch( Exception e )
          {
-            Logger.Current.Error( e, "An error occurred while setting up hooks for IMGUI." );
+            XuaLogger.Current.Error( e, "An error occurred while setting up hooks for IMGUI." );
          }
 
          try
@@ -184,7 +137,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          }
          catch( Exception e )
          {
-            Logger.Current.Error( e, "An error occurred while setting up hooks for Utage." );
+            XuaLogger.Current.Error( e, "An error occurred while setting up hooks for Utage." );
          }
 
       }
@@ -222,7 +175,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
                                     addMethod.Invoke( component, new object[] { callback } );
                                  }
 
-                                 Logger.Current.Info( eventName + " was hooked by external plugin." );
+                                 XuaLogger.Current.Info( eventName + " was hooked by external plugin." );
                                  return true;
                               }
                               catch { }
@@ -235,7 +188,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          }
          catch( Exception e )
          {
-            Logger.Current.Error( e, $"An error occurred while setting up override hooks for '{eventName}'." );
+            XuaLogger.Current.Error( e, $"An error occurred while setting up override hooks for '{eventName}'." );
          }
 
          return false;

+ 22 - 21
src/XUnity.AutoTranslator.Plugin.Core/Hooks/IMGUIHooks.cs

@@ -7,27 +7,28 @@ using Harmony;
 using UnityEngine;
 using static UnityEngine.GUI;
 
-namespace XUnity.AutoTranslator.Plugin.Core.IMGUI
+namespace XUnity.AutoTranslator.Plugin.Core.Hooks.IMGUI
 {
-   public static class IMGUIHooks
+   internal static class IMGUIHooks
    {
       public static bool HooksOverriden = false;
 
       public static readonly Type[] All = new[] {
-         typeof( BeginGroupHook ),
-         typeof( BoxHook ),
-         typeof( DoRepeatButtonHook ),
-         typeof( DoLabelHook ),
-         typeof( DoButtonHook ),
-         typeof( DoModalWindowHook ),
-         typeof( DoWindowHook ),
-         typeof( DoTextFieldHook ),
-         typeof( DoToggleHook ),
+         typeof( GUI_BeginGroup_Hook ),
+         typeof( GUI_Box_Hook ),
+         typeof( GUI_DoRepeatButton_Hook ),
+         typeof( GUI_DoLabel_Hook ),
+         typeof( GUI_DoButton_Hook ),
+         typeof( GUI_DoModalWindow_Hook ),
+         typeof( GUI_DoWindow_Hook ),
+         typeof( GUI_DoTextField_Hook ),
+         typeof( GUI_DoButtonGrid_Hook ),
+         typeof( GUI_DoToggle_Hook ),
       };
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class BeginGroupHook
+   internal static class GUI_BeginGroup_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -49,7 +50,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.IMGUI
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class BoxHook
+   internal static class GUI_Box_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -72,7 +73,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.IMGUI
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class DoRepeatButtonHook
+   internal static class GUI_DoRepeatButton_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -94,7 +95,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.IMGUI
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class DoLabelHook
+   internal static class GUI_DoLabel_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -116,7 +117,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.IMGUI
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class DoButtonHook
+   internal static class GUI_DoButton_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -138,7 +139,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.IMGUI
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class DoModalWindowHook
+   internal static class GUI_DoModalWindow_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -160,7 +161,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.IMGUI
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class DoWindowHook
+   internal static class GUI_DoWindow_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -182,7 +183,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.IMGUI
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class DoButtonGridHook
+   internal static class GUI_DoButtonGrid_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -207,7 +208,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.IMGUI
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class DoTextFieldHook
+   internal static class GUI_DoTextField_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -229,7 +230,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.IMGUI
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class DoToggleHook
+   internal static class GUI_DoToggle_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {

+ 25 - 174
src/XUnity.AutoTranslator.Plugin.Core/Hooks/ImageHooks.cs

@@ -12,7 +12,7 @@ using XUnity.AutoTranslator.Plugin.Core.Extensions;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Hooks
 {
-   public static class ImageHooks
+   internal static class ImageHooks
    {
       public static readonly Type[] All = new[] {
          typeof( MaskableGraphic_OnEnable_Hook ),
@@ -41,26 +41,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          typeof( UITexture_material_Hook ),
          typeof( UIPanel_clipTexture_Hook ),
          typeof( UIRect_OnInit_Hook ),
-         //typeof( UIFont_dynamicFont_Hook ),
-         //typeof( UIFont_material_Hook ),
-         //typeof( UILabel_bitmapFont_Hook ),
-         //typeof( UILabel_trueTypeFont_Hook ),
       };
    }
 
-   //public static class GenericPrefix_Hook
-   //{
-   //   public static void Prefix( object __instance, object value )
-   //   {
-   //      if( value is Texture2D texture2d )
-   //      {
-   //         AutoTranslationPlugin.Current.Hook_ImageChangedOnComponent( __instance, texture2d, true );
-   //      }
-   //   }
-   //}
-
    [Harmony]
-   public static class SpriteRenderer_sprite_Hook
+   internal static class SpriteRenderer_sprite_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -78,142 +63,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
       }
    }
 
-   //[Harmony]
-   //public static class SpriteRenderer_get_sprite_Hook
-   //{
-   //   static bool Prepare( HarmonyInstance instance )
-   //   {
-   //      return ClrTypes.SpriteRenderer != null;
-   //   }
-
-   //   static MethodBase TargetMethod( HarmonyInstance instance )
-   //   {
-   //      return AccessTools.Property( ClrTypes.SpriteRenderer, "sprite" ).GetGetMethod();
-   //   }
-
-   //   public static void Postfix( object __instance )
-   //   {
-   //      Logger.Current.Error( new StackTrace().ToString() );
-   //      AutoTranslationPlugin.Current.Hook_ImageChangedOnComponent( __instance, null, false, false );
-   //   }
-   //}
-
-   //[Harmony]
-   //public static class SpriteRenderer_get_size_Hook
-   //{
-   //   static bool Prepare( HarmonyInstance instance )
-   //   {
-   //      return ClrTypes.SpriteRenderer != null;
-   //   }
-
-   //   static MethodBase TargetMethod( HarmonyInstance instance )
-   //   {
-   //      return AccessTools.Property( ClrTypes.SpriteRenderer, "size" ).GetGetMethod();
-   //   }
-
-   //   public static void Postfix( object __instance )
-   //   {
-   //      Logger.Current.Error( "SpriteRenderer_get_size_Hook" );
-   //      AutoTranslationPlugin.Current.Hook_ImageChangedOnComponent( __instance, null, false, false );
-   //   }
-   //}
-
-   //[Harmony]
-   //public static class SpriteRenderer_set_size_Hook
-   //{
-   //   static bool Prepare( HarmonyInstance instance )
-   //   {
-   //      return ClrTypes.SpriteRenderer != null;
-   //   }
-
-   //   static MethodBase TargetMethod( HarmonyInstance instance )
-   //   {
-   //      return AccessTools.Property( ClrTypes.SpriteRenderer, "size" ).GetSetMethod();
-   //   }
-
-   //   public static void Postfix( object __instance )
-   //   {
-   //      Logger.Current.Error( "SpriteRenderer_set_size_Hook" );
-   //      AutoTranslationPlugin.Current.Hook_ImageChangedOnComponent( __instance, null, false, false );
-   //   }
-   //}
-
-   //[Harmony]
-   //public static class SpriteRenderer_get_color_Hook
-   //{
-   //   static bool Prepare( HarmonyInstance instance )
-   //   {
-   //      return ClrTypes.SpriteRenderer != null;
-   //   }
-
-   //   static MethodBase TargetMethod( HarmonyInstance instance )
-   //   {
-   //      return AccessTools.Property( ClrTypes.SpriteRenderer, "color" ).GetGetMethod();
-   //   }
-
-   //   public static void Postfix( object __instance )
-   //   {
-   //      Logger.Current.Error( "SpriteRenderer_get_color_Hook" );
-   //      AutoTranslationPlugin.Current.Hook_ImageChangedOnComponent( __instance, null, false, false );
-   //   }
-   //}
-
-   //[Harmony]
-   //public static class SpriteRenderer_set_color_Hook
-   //{
-   //   static bool Prepare( HarmonyInstance instance )
-   //   {
-   //      return ClrTypes.SpriteRenderer != null;
-   //   }
-
-   //   static MethodBase TargetMethod( HarmonyInstance instance )
-   //   {
-   //      return AccessTools.Property( ClrTypes.SpriteRenderer, "color" ).GetSetMethod();
-   //   }
-
-   //   public static void Postfix( object __instance )
-   //   {
-   //      Logger.Current.Error( "SpriteRenderer_set_color_Hook" );
-   //      AutoTranslationPlugin.Current.Hook_ImageChangedOnComponent( __instance, null, false, false );
-   //   }
-   //}
-
-   //[Harmony]
-   //public static class SpriteRenderer_GetSpriteBounds_Hook
-   //{
-   //   static bool Prepare( HarmonyInstance instance )
-   //   {
-   //      return ClrTypes.SpriteRenderer != null;
-   //   }
-
-   //   static MethodBase TargetMethod( HarmonyInstance instance )
-   //   {
-   //      return AccessTools.Method( ClrTypes.SpriteRenderer, "GetSpriteBounds" );
-   //   }
-
-   //   public static void Postfix( object __instance )
-   //   {
-   //      Logger.Current.Error( "SpriteRenderer_GetSpriteBounds_Hook" );
-   //      AutoTranslationPlugin.Current.Hook_ImageChangedOnComponent( __instance, null, false, false );
-   //   }
-   //}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
    [Harmony]
-   public static class CubismRenderer_MainTexture_Hook
+   internal static class CubismRenderer_MainTexture_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -232,7 +83,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class CubismRenderer_TryInitialize_Hook
+   internal static class CubismRenderer_TryInitialize_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -251,7 +102,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class Material_mainTexture_Hook
+   internal static class Material_mainTexture_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -273,7 +124,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class MaskableGraphic_OnEnable_Hook
+   internal static class MaskableGraphic_OnEnable_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -295,7 +146,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class Image_sprite_Hook
+   internal static class Image_sprite_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -314,7 +165,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class Image_overrideSprite_Hook
+   internal static class Image_overrideSprite_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -333,7 +184,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class Image_material_Hook
+   internal static class Image_material_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -352,7 +203,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class RawImage_texture_Hook
+   internal static class RawImage_texture_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -374,7 +225,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class Cursor_SetCursor_Hook
+   internal static class Cursor_SetCursor_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -393,7 +244,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UIAtlas_spriteMaterial_Hook
+   internal static class UIAtlas_spriteMaterial_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -412,7 +263,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UISprite_OnInit_Hook
+   internal static class UISprite_OnInit_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -431,7 +282,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UISprite_material_Hook
+   internal static class UISprite_material_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -450,7 +301,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UISprite_atlas_Hook
+   internal static class UISprite_atlas_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -469,7 +320,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UITexture_mainTexture_Hook
+   internal static class UITexture_mainTexture_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -488,7 +339,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UITexture_material_Hook
+   internal static class UITexture_material_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -507,7 +358,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UIRect_OnInit_Hook
+   internal static class UIRect_OnInit_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -526,7 +377,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UI2DSprite_sprite2D_Hook
+   internal static class UI2DSprite_sprite2D_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -545,7 +396,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UI2DSprite_material_Hook
+   internal static class UI2DSprite_material_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -564,7 +415,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UIPanel_clipTexture_Hook
+   internal static class UIPanel_clipTexture_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -584,7 +435,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
 
 
    [Harmony]
-   public static class UIFont_material_Hook
+   internal static class UIFont_material_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -603,7 +454,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UIFont_dynamicFont_Hook
+   internal static class UIFont_dynamicFont_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -622,7 +473,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UILabel_bitmapFont_Hook
+   internal static class UILabel_bitmapFont_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -641,7 +492,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    }
 
    [Harmony]
-   public static class UILabel_trueTypeFont_Hook
+   internal static class UILabel_trueTypeFont_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {

+ 5 - 5
src/XUnity.AutoTranslator.Plugin.Core/Hooks/NGUIHooks.cs

@@ -10,18 +10,18 @@ using XUnity.AutoTranslator.Plugin.Core.Constants;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Hooks.NGUI
 {
-   public static class NGUIHooks
+   internal static class NGUIHooks
    {
       public static bool HooksOverriden = false;
 
       public static readonly Type[] All = new[] {
-         typeof( TextPropertyHook ),
-         typeof( OnEnableHook )
+         typeof( UILabel_text_Hook ),
+         typeof( UILabel_OnEnable_Hook )
       };
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class TextPropertyHook
+   internal static class UILabel_text_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -44,7 +44,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.NGUI
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class OnEnableHook
+   internal static class UILabel_OnEnable_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {

+ 9 - 8
src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextGetterCompatHooks.cs

@@ -2,19 +2,20 @@
 using System.Reflection;
 using Harmony;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
 
-namespace XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI
+namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextGetterCompat
 {
-   public static class TextGetterCompatHooks
+   internal static class TextGetterCompatHooks
    {
       public static readonly Type[] All = new[] {
-         typeof( TextPropertyGetterHook1 ),
-         typeof( TextPropertyGetterHook2 ),
+         typeof( Text_text_Hook ),
+         typeof( TMP_Text_text_Hook ),
       };
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class TextPropertyGetterHook1
+   internal static class Text_text_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -29,12 +30,12 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI
 
       static void Postfix( object __instance, ref string __result )
       {
-         TextGetterCompatMode.ReplaceTextWithOriginal( __instance, ref __result );
+         TextGetterCompatModeHelper.ReplaceTextWithOriginal( __instance, ref __result );
       }
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class TextPropertyGetterHook2
+   internal static class TMP_Text_text_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -48,7 +49,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI
 
       static void Postfix( object __instance, ref string __result )
       {
-         TextGetterCompatMode.ReplaceTextWithOriginal( __instance, ref __result );
+         TextGetterCompatModeHelper.ReplaceTextWithOriginal( __instance, ref __result );
       }
    }
 }

+ 19 - 19
src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshProHooks.cs

@@ -8,25 +8,25 @@ using XUnity.AutoTranslator.Plugin.Core.Constants;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro
 {
-   public static class TextMeshProHooks
+   internal static class TextMeshProHooks
    {
       public static bool HooksOverriden = false;
 
       public static readonly Type[] All = new[] {
-         typeof( TeshMeshProUGUIOnEnableHook ),
-         typeof( TeshMeshProOnEnableHook ),
-         typeof( TextPropertyHook ),
-         typeof( SetTextHook1 ),
-         typeof( SetTextHook2 ),
-         typeof( SetTextHook3 ),
-         typeof( SetCharArrayHook1 ),
-         typeof( SetCharArrayHook2 ),
-         typeof( SetCharArrayHook3 ),
+         typeof( TeshMeshProUGUI_OnEnable_Hook ),
+         typeof( TeshMeshPro_OnEnable_Hook ),
+         typeof( TMP_Text_text_Hook ),
+         typeof( TMP_Text_SetText_Hook1 ),
+         typeof( TMP_Text_SetText_Hook2 ),
+         typeof( TMP_Text_SetText_Hook3 ),
+         typeof( TMP_Text_SetCharArray_Hook1 ),
+         typeof( TMP_Text_SetCharArray_Hook2 ),
+         typeof( TMP_Text_SetCharArray_Hook3 ),
       };
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class TeshMeshProUGUIOnEnableHook
+   internal static class TeshMeshProUGUI_OnEnable_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -49,7 +49,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class TeshMeshProOnEnableHook
+   internal static class TeshMeshPro_OnEnable_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -72,7 +72,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class TextPropertyHook
+   internal static class TMP_Text_text_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -95,7 +95,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class SetTextHook1
+   internal static class TMP_Text_SetText_Hook1
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -118,7 +118,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class SetTextHook2
+   internal static class TMP_Text_SetText_Hook2
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -141,7 +141,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class SetTextHook3
+   internal static class TMP_Text_SetText_Hook3
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -164,7 +164,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class SetCharArrayHook1
+   internal static class TMP_Text_SetCharArray_Hook1
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -187,7 +187,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class SetCharArrayHook2
+   internal static class TMP_Text_SetCharArray_Hook2
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -210,7 +210,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class SetCharArrayHook3
+   internal static class TMP_Text_SetCharArray_Hook3
    {
       static bool Prepare( HarmonyInstance instance )
       {

+ 5 - 6
src/XUnity.AutoTranslator.Plugin.Core/Hooks/UGUIHooks.cs

@@ -8,19 +8,18 @@ using XUnity.AutoTranslator.Plugin.Core.Constants;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI
 {
-
-   public static class UGUIHooks
+   internal static class UGUIHooks
    {
       public static bool HooksOverriden = false;
 
       public static readonly Type[] All = new[] {
-         typeof( TextPropertyHook ),
-         typeof( OnEnableHook ),
+         typeof( Text_text_Hook ),
+         typeof( Text_OnEnable_Hook ),
       };
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class TextPropertyHook
+   internal static class Text_text_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {
@@ -44,7 +43,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI
    }
 
    [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
-   public static class OnEnableHook
+   internal static class Text_OnEnable_Hook
    {
       static bool Prepare( HarmonyInstance instance )
       {

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно