From 571c8cfe6ba3f187a6764c8b8346ac24c41e920e Mon Sep 17 00:00:00 2001 From: randoman <738b86bb93c44695854182cc459afcbb@lonestar.no> Date: Fri, 11 Jan 2019 22:16:38 +0100 Subject: [PATCH] * FEATURE - Support legitimate Bing translate API (requires key) * FEATURE - Documented custom endpoint * BUG FIX - Fixed bug in older versions of the Unity engine where the plugin would crash on startup due to missing APIs in relation to the SceneManagement namespace * BUG FIX - Fixed bug that could happen in Utage-based games, that would cause dialogue to sometimes be cut off mid-sentence * BUG FIX - Incorrect handling of 'null' default values in configuration * BUG FIX - Various other minor fixes * MISC - Added configuration option to support ignoring texts starting with certain characters, IgnoreTextStartingWith * MISC - Template reparation for IMGUI translations * MISC - Big update to README file to fully describe the features of the plugin --- CHANGELOG.md | 13 +- README.md | 111 ++++++-- src/XUnity.AutoTranslator.Patcher/Patcher.cs | 2 +- .../BepInLogger.cs | 3 +- .../XUnity.AutoTranslator.Plugin.BepIn.csproj | 2 +- .../AutoTranslationPlugin.cs | 65 +++-- .../Configuration/IniKeyExtensions.cs | 22 +- .../Configuration/Settings.cs | 8 +- .../Constants/ClrTypes.cs | 2 + .../Constants/KnownEndpointNames.cs | 4 + .../Constants/PluginData.cs | 2 +- .../Extensions/ObjectExtensions.cs | 4 +- .../Extensions/StringExtensions.cs | 13 + .../Features.cs | 11 + .../KnownEndpoints.cs | 13 +- .../Logger.cs | 11 +- .../TemplatedString.cs | 66 ++++- .../Web/BaiduTranslateEndpoint.cs | 18 +- .../Web/BingTranslateEndpoint.cs | 245 ++++++++++++++++++ .../Web/BingTranslateLegitimateEndpoint.cs | 91 +++++++ .../Web/DefaultEndpoint.cs | 6 +- .../Web/GoogleTranslateLegitimateEndpoint.cs | 36 +-- .../Web/WatsonTranslateEndpoint.cs | 20 +- .../Web/YandexTranslateEndpoint.cs | 13 +- .../XUnity.AutoTranslator.Plugin.Core.csproj | 2 +- .../XUnity.AutoTranslator.Plugin.IPA.csproj | 2 +- ...AutoTranslator.Plugin.UnityInjector.csproj | 2 +- .../XUnity.AutoTranslator.Setup.csproj | 2 +- 28 files changed, 673 insertions(+), 116 deletions(-) create mode 100644 src/XUnity.AutoTranslator.Plugin.Core/Web/BingTranslateEndpoint.cs create mode 100644 src/XUnity.AutoTranslator.Plugin.Core/Web/BingTranslateLegitimateEndpoint.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 04dc7cb8..779f6a64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,15 @@ -### 2.16.0 +### 2.17.0 + * FEATURE - Support legitimate Bing translate API (requires key) + * FEATURE - Documented custom endpoint + * BUG FIX - Fixed bug in older versions of the Unity engine where the plugin would crash on startup due to missing APIs in relation to the SceneManagement namespace + * BUG FIX - Fixed bug that could happen in Utage-based games, that would cause dialogue to sometimes be cut off mid-sentence + * BUG FIX - Incorrect handling of 'null' default values in configuration + * BUG FIX - Various other minor fixes + * MISC - Added configuration option to support ignoring texts starting with certain characters, IgnoreTextStartingWith + * MISC - Template reparation for IMGUI translations + * MISC - Big update to README file to fully describe the features of the plugin + +### 2.16.0 * FEATURE - Support image dumping and loading (not automatic!). Disabled by default * BUG FIX - Fixed toggle translation which was broken in 2.15.4 * BUG FIX - Updated TKK retrieval logic diff --git a/README.md b/README.md index 04e1d4f9..b2d1798e 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,89 @@ # XUnity Auto Translator ## Index - * [Notice](#notice) + * [Introduction](#introduction) + * [Translators](#translators) * [Text Frameworks](#text-frameworks) * [Plugin Frameworks](#plugin-frameworks) * [Configuration](#configuration) * [Key Mapping](#key-mapping) * [Installation](#installation) * [Translating Mods](#translating-mods) + * [Manual Translations](#manual-translations) + * [Regarding Redistribution](#regarding-redistribution) * [Texture Translation](#texture-translation) * [Integrating with Auto Translator](#integrating-with-auto-translator) - -## Notice -The latest version (2.16.0+) now also supports basic image loading/dumping. These are not automatically translated and the feature is disabled by default. - -This feature is primarily meant for games with little to no mod support to enable full translations without needing to modify resource files. - -If you are going to make use of this feature, please make sure you read and understand the [Texture Translation](#texture-translation) section! + +## Introduction +This is a plugin that is capable of using various online translators to provide on-the-fly translations for various Unity-based games. + +It does (obviously) go to the internet, in order to provide the translation, so if you are not comfortable with that, don't use it. + +Oh, and it is also capable of doing some basic image loading/dumping. Go to the [Texture Translation](#texture-translation) section to find out more. + +If you intend on redistributing this plugin as part of a translation suite for a game, please read [this section](#regarding-redistribution). + +## Translators +The supported online 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. + * [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. + * 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 + * 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.** + +### 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. + +Also, make sure you monitor the service's request usage/quotas, especially when it is being used with unknown games. This plugin makes no guarantees about how many translation requests will be sent out, although it will attempt to keep the number to a minimum. It does so by using the [spam prevention mechanisms](#spam-prevention) discussed below. + +### Spam Prevention +The plugin employs the following spam prevention mechanisms: + 1. When it sees a new text, it will always wait one second before it queues a translation request, to check if that same text changes. It will not send out any request until the text has not changed for 1 second. (Utage (VN Game Engine) is an exception here, as those texts may come from a resource lookup) + 2. It will never send out more than 8000 requests (max 200 characters each (configurable)) during a single game session. + 3. It will never send out more than 1 request at a time (no concurrency!). + 4. If it detects an increasing number of queued translations (3500), the plugin will shutdown. + 5. If the service returns no result for five consecutive requests, the plugin will shutdown. + 6. If the plugin detects that the game queues translations every frame, the plugin will shutdown after 90 frames. + 7. If the plugin detects text that "scrolls" into place, the plugin will shutdown. This is detected by inspecting all requests that are queued for translation. ((1) will genenerally prevent this from happening) + 8. If the plugin consistently queues translations every second for more than 60 seconds, the plugin will shutdown. + 9. For the supported languages, each translatable line must pass a symbol check that detects if the line includes characters from the source language. + 10. It will never attempt a translation for a text that is already considered a translation for something else. + 11. All queued translations are kept track of. If two different components that require the same translation and both are queued for translation at the same time, only a single request is sent. + 12. It employs an internal dictionary of manual translations (~10000 in total) for commonly used phrases (Japanese-to-English only) to prevent sending out translation requests for these. + 13. Some endpoints support batching of translations so far fewer requests are sent. This does not increase the total number of translations per session (2). + 14. All translation results are cached in memory and stored on disk to prevent making the same translation request twice. ## Text Frameworks -This is an auto translation mod that hooks into the unity game engine and attempts to provide translations for the following text frameworks for Unity: - * UGUI - * NGUI - * IMGUI - * TextMeshPro - * Utage (VN Game Engine) - -It does go to the internet, in order to provide the translation, so if you are not comfortable with that, don't use it. - +The following text frameworks are supported. + * [UGUI](https://docs.unity3d.com/Manual/UISystem.html) + * [NGUI](https://assetstore.unity.com/packages/tools/gui/ngui-next-gen-ui-2413) + * [IMGUI](https://docs.unity3d.com/Manual/GUIScriptingGuide.html) (disabled by default) + * [TextMeshPro](http://digitalnativestudios.com/textmeshpro/docs/) + * [Utage (VN Game Engine)](http://madnesslabo.net/utage/?lang=en) + ## Plugin Frameworks The mod can be installed into the following Plugin Managers: - * [BepInEx](https://github.com/bbepis/BepInEx) + * [BepInEx](https://github.com/bbepis/BepInEx) (preferred approach) * [IPA](https://github.com/Eusth/IPA) * UnityInjector -Installations instructions for both methods can be found below. +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! @@ -43,7 +92,7 @@ The default configuration file, looks as such (2.6.0+): ```ini [Service] -Endpoint=GoogleTranslate ;Endpoint to use. Can be ["GoogleTranslate", "GoogleTranslateLegitimate", "BaiduTranslate", "YandexTranslate", "WatsonTranslate", "ExciteTranslate", ""]. If empty, it simply means: Only use cached translations +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 [General] Language=en ;The language to translate into @@ -57,12 +106,13 @@ OutputFile=Translation\_AutoGeneratedTranslations.{lang}.txt ;File to insert g EnableUGUI=True ;Enable or disable UGUI translation EnableNGUI=True ;Enable or disable NGUI translation EnableTextMeshPro=True ;Enable or disable TextMeshPro translation -EnableIMGUI=False ;Enable of disable IMGUI translation +EnableIMGUI=False ;Enable or disable IMGUI translation +EnableUtage=True ;Enable or disable Utage-specific translation AllowPluginHookOverride=True ;Allow other text translation plugins to override this plugin's hooks [Behaviour] Delay=0 ;Delay to wait before attempting to translate a text in seconds -MaxCharactersPerTranslation=150 ;Max characters per text to translate +MaxCharactersPerTranslation=200 ;Max characters per text to translate IgnoreWhitespaceInDialogue=True ;Whether or not to ignore whitespace, including newlines, in dialogue keys IgnoreWhitespaceInNGUI=True ;Whether or not to ignore whitespace, including newlines, in NGUI MinDialogueChars=20 ;The length of the text for it to be considered a dialogue @@ -77,6 +127,7 @@ OverrideFont= ;Overrides the fonts used for texts when updati WhitespaceRemovalStrategy=TrimPerNewline ;Indicates how whitespace/newline removal should be handled before attempting translation. Can be ["TrimPerNewline", "AllOccurrences"] ResizeUILineSpacingScale= ;A decimal value that the default line spacing should be scaled by during UI resizing, for example: 0.80. NOTE: Only works for UGUI ForceUIResizing=True ;Indicates whether the UI resize behavior should be applied to all UI components regardless of them being translated. +IgnoreTextStartingWith=\u180e; ;Indicates that the plugin should ignore any strings starting with certain characters. This is a list seperated by ';'. [Texture] TextureDirectory=Translation\Texture ;Directory to dump textures to, and root of directories to load images from @@ -93,6 +144,9 @@ UserAgent= ;Override the user agent used by APIs requiring [GoogleLegitimate] GoogleAPIKey= ;OPTIONAL, needed if GoogleTranslateLegitimate is configured +[BingLegitimate] +OcpApimSubscriptionKey= ;OPTIONAL, needed if BingTranslateLegitimate is configured + [Baidu] BaiduAppId= ;OPTIONAL, needed if BaiduTranslate is configured BaiduAppSecret= ;OPTIONAL, needed if BaiduTranslate is configured @@ -199,6 +253,19 @@ The file structure should like like this ## Translating Mods Often other mods UI are implemented through IMGUI. As you can see above, this is disabled by default. By changing the "EnableIMGUI" value to "True", it will start translating IMGUI as well, which likely means that other mods UI will be translated. +## Manual Translations +When you use this plugin, you can always go to the file `Translation\_AutoGeneratedTranslations.{lang}.txt` (OutputFile) to edit any auto generated translations and they will show up the next time you run the game. + +It is also worth noting that this plugin will read all text files (*.txt) in the `Translation` (Directory), so if you want to provide a manual translation, you can simply cut out texts from the `Translation\_AutoGeneratedTranslations.{lang}.txt` (OutputFile) and place them in new text files in order to replace them with a manual translation. + +In this context, the `Translation\_AutoGeneratedTranslations.{lang}.txt` (OutputFile) will always have the lowest priority when reading translations. So if the same translation is present in two places, it will not be the one from the (OutputFile) that is used. + +## Regarding Redistribution +Redistributing this plugin for various games is absolutely encouraged. However, if you do so, please keep the following in mind: + * **IMPORTANT: Distribute the _AutoGeneratedTranslations.{lang}.txt file along with the redistribution with as many translations as possible to ensure the online translator is hit as little as possible.** + * Ensure you keep the plugin up-to-date, as much as reasonably possible. + * If you use image loading feature, make sure you read [this section](#texture-translation). + ## Texture Translation From version 2.16.0+ this mod provides basic capabilities to replace images. It is a feature that is disabled by default. There is no automatic translation of these images though. diff --git a/src/XUnity.AutoTranslator.Patcher/Patcher.cs b/src/XUnity.AutoTranslator.Patcher/Patcher.cs index e549f2b2..c4a6d4c1 100644 --- a/src/XUnity.AutoTranslator.Patcher/Patcher.cs +++ b/src/XUnity.AutoTranslator.Patcher/Patcher.cs @@ -29,7 +29,7 @@ public override string Version { get { - return "2.16.0"; + return "2.17.0"; } } diff --git a/src/XUnity.AutoTranslator.Plugin.BepIn/BepInLogger.cs b/src/XUnity.AutoTranslator.Plugin.BepIn/BepInLogger.cs index f90d0584..ca9b5c8b 100644 --- a/src/XUnity.AutoTranslator.Plugin.BepIn/BepInLogger.cs +++ b/src/XUnity.AutoTranslator.Plugin.BepIn/BepInLogger.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using XUnity.AutoTranslator.Plugin.Core; +using XUnity.AutoTranslator.Plugin.Core.Constants; namespace XUnity.AutoTranslator.Plugin.BepIn { @@ -15,7 +16,7 @@ public BepInLogger() protected override void Log( LogLevel level, string message ) { - BepInEx.Logger.CurrentLogger.Log( Convert( level ), "[XUnity.AutoTranslator] " + message ); + BepInEx.Logger.CurrentLogger.Log( Convert( level ), "[XUnity.AutoTranslator " + PluginData.Version + "] " + message ); } public BepInEx.Logging.LogLevel Convert( LogLevel level ) diff --git a/src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj b/src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj index b9c52082..f57b8395 100644 --- a/src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj +++ b/src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj @@ -2,7 +2,7 @@ net35 - 2.16.0 + 2.17.0 diff --git a/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs b/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs index cbfb9fe8..f3093067 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs @@ -92,7 +92,7 @@ public class AutoTranslationPlugin : MonoBehaviour private Dictionary _translatedImages = new Dictionary( StringComparer.InvariantCultureIgnoreCase ); private HashSet _untranslatedImages = new HashSet(); - private object _advEngine; + private Component _advEngine; private float? _nextAdvUpdate; private IKnownEndpoint _endpoint; @@ -186,7 +186,7 @@ public void Initialize() _overrideFont = _hasOverrideFont; } - if( Settings.EnableTextureScanOnSceneLoad && ( Settings.EnableTextureDumping || Settings.EnableTextureTranslation ) ) + if( Features.SupportsScenes && Settings.EnableTextureScanOnSceneLoad && ( Settings.EnableTextureDumping || Settings.EnableTextureTranslation ) ) { try { @@ -279,12 +279,17 @@ private void SaveTranslationsLoop( object state ) } public void EnableSceneLoadScan() + { + EnableSceneLoadScanInternal(); + } + + public void EnableSceneLoadScanInternal() { // specified in own method, because of chance that this has changed through Unity lifetime - SceneManager.sceneLoaded += SceneManager_SceneLoaded; + SceneManager.sceneLoaded += ( arg1, arg2 ) => SceneManager_SceneLoaded(); } - private void SceneManager_SceneLoaded( Scene scene, LoadSceneMode arg1 ) + private void SceneManager_SceneLoaded() { Logger.Current.Info( "SceneLoading..." ); var startTime = Time.realtimeSinceStartup; @@ -365,7 +370,7 @@ private void RegisterImageFromFile( string fullFileName ) var data = File.ReadAllBytes( fullFileName ); var currentHash = HashHelper.Compute( data ); var isModified = StringComparer.InvariantCultureIgnoreCase.Compare( originalHash, currentHash ) != 0; - + // only load images that someone has modified! if( Settings.LoadUnmodifiedTextures || isModified ) { @@ -697,8 +702,8 @@ private void CheckThresholds() _unstartedJobs.Clear(); _completedJobs.Clear(); _ongoingJobs.Clear(); - Settings.IsShutdown = true; + Settings.IsShutdown = true; Logger.Current.Error( $"SPAM DETECTED: More than {Settings.MaxTranslationsQueuedPerSecond} translations per seconds queued for a {Settings.MaxSecondsAboveTranslationThreshold} second period. Shutting down plugin." ); } } @@ -976,7 +981,10 @@ private void SetText( object ui, string text, bool isTranslated, TextTranslation /// private bool IsTranslatable( string str ) { - return _symbolCheck( str ) && str.Length <= Settings.MaxCharactersPerTranslation && !_reverseTranslations.ContainsKey( str ); + return _symbolCheck( str ) + && str.Length <= Settings.MaxCharactersPerTranslation + && !_reverseTranslations.ContainsKey( str ) + && !Settings.IgnoreTextStartingWith.Any( x => str.StartsWithStrict( x ) ); } private bool IsShortText( string str ) @@ -1718,7 +1726,7 @@ public void Update() KickoffTranslations(); FinishTranslations(); - if( _nextAdvUpdate.HasValue && Time.time > _nextAdvUpdate ) + if( ClrTypes.AdvEngine != null && _nextAdvUpdate.HasValue && Time.time > _nextAdvUpdate ) { _nextAdvUpdate = null; UpdateUtageText(); @@ -1826,15 +1834,6 @@ private void KickoffTranslations() public void OnBatchTranslationCompleted( TranslationBatch batch, string translatedTextBatch ) { - if( !Settings.IsShutdown ) - { - if( Settings.TranslationCount > Settings.MaxTranslationsBeforeShutdown ) - { - Settings.IsShutdown = true; - Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." ); - } - } - _consecutiveErrors = 0; var succeeded = batch.MatchWithTranslations( translatedTextBatch ); @@ -1881,11 +1880,6 @@ public void OnBatchTranslationCompleted( TranslationBatch batch, string translat Logger.Current.Error( "A batch operation failed. Disabling batching and restarting failed jobs." ); } - } - - private void OnSingleTranslationCompleted( TranslationJob job, string translatedText ) - { - Settings.TranslationCount++; if( !Settings.IsShutdown ) { @@ -1893,8 +1887,17 @@ private void OnSingleTranslationCompleted( TranslationJob job, string translated { Settings.IsShutdown = true; Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." ); + + _unstartedJobs.Clear(); + _completedJobs.Clear(); + _ongoingJobs.Clear(); } } + } + + private void OnSingleTranslationCompleted( TranslationJob job, string translatedText ) + { + Settings.TranslationCount++; _consecutiveErrors = 0; @@ -1913,6 +1916,19 @@ private void OnSingleTranslationCompleted( TranslationJob job, string translated AddTranslation( job.Key, job.TranslatedText ); job.State = TranslationJobState.Succeeded; _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() ); + + if( !Settings.IsShutdown ) + { + if( Settings.TranslationCount > Settings.MaxTranslationsBeforeShutdown ) + { + Settings.IsShutdown = true; + Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." ); + + _unstartedJobs.Clear(); + _completedJobs.Clear(); + _ongoingJobs.Clear(); + } + } } private void OnTranslationFailed( TranslationJob job ) @@ -2041,9 +2057,10 @@ private void FinishTranslations() private void UpdateUtageText() { - if( _advEngine == null ) + // After an object is destroyed, an equality check with null will return true. The variable does not go to null, you can still call GetInstanceID() on it, but the "==" operator is overloaded and behaves as expected. + if( _advEngine == null || _advEngine?.gameObject == null ) { - _advEngine = GameObject.FindObjectOfType( Constants.ClrTypes.AdvEngine ); + _advEngine = (Component)GameObject.FindObjectOfType( Constants.ClrTypes.AdvEngine ); } if( _advEngine != null ) diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Configuration/IniKeyExtensions.cs b/src/XUnity.AutoTranslator.Plugin.Core/Configuration/IniKeyExtensions.cs index 2eda2169..3ae7179e 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Configuration/IniKeyExtensions.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Configuration/IniKeyExtensions.cs @@ -13,10 +13,10 @@ public static class IniKeyExtensions public static T GetOrDefault( this IniKey that, T defaultValue, bool allowEmpty = false ) { var typeOfT = typeof( T ).UnwrapNullable(); - if( !allowEmpty ) + if( allowEmpty ) { var value = that.Value; - if( string.IsNullOrEmpty( value ) ) + if( value == null ) // we want to use the default value, because it is null, the config has just been created { if( defaultValue != null ) { @@ -37,14 +37,20 @@ public static T GetOrDefault( this IniKey that, T defaultValue, bool allowEmp } else { - if( typeOfT.IsEnum ) + // 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 ) ) { - return (T)Enum.Parse( typeOfT, that.Value, true ); - } - else - { - return (T)Convert.ChangeType( that.Value, typeOfT, CultureInfo.InvariantCulture ); + 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 diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs b/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs index 22bda310..ef7c168c 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs @@ -5,6 +5,7 @@ using System.Text; using XUnity.AutoTranslator.Plugin.Core.Constants; using XUnity.AutoTranslator.Plugin.Core.Debugging; +using XUnity.AutoTranslator.Plugin.Core.Extensions; using XUnity.AutoTranslator.Plugin.Core.Utilities; namespace XUnity.AutoTranslator.Plugin.Core.Configuration @@ -65,6 +66,7 @@ public static class Settings 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; @@ -78,6 +80,7 @@ public static class Settings public static WhitespaceHandlingStrategy WhitespaceRemovalStrategy; public static float? ResizeUILineSpacingScale; public static bool ForceUIResizing; + public static string[] IgnoreTextStartingWith; public static string TextureDirectory; public static bool EnableTextureTranslation; @@ -139,7 +142,8 @@ public static void Configure() OverrideFont = Config.Current.Preferences[ "Behaviour" ][ "OverrideFont" ].GetOrDefault( string.Empty ); ResizeUILineSpacingScale = Config.Current.Preferences[ "Behaviour" ][ "ResizeUILineSpacingScale" ].GetOrDefault( 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 ]; TextureDirectory = Config.Current.Preferences[ "Texture" ][ "TextureDirectory" ].GetOrDefault( @"Translation\Texture" ); EnableTextureTranslation = Config.Current.Preferences[ "Texture" ][ "EnableTextureTranslation" ].GetOrDefault( false ); @@ -166,6 +170,8 @@ public static void Configure() GoogleAPIKey = Config.Current.Preferences[ "GoogleLegitimate" ][ "GoogleAPIKey" ].GetOrDefault( "" ); + BingOcpApimSubscriptionKey = Config.Current.Preferences[ "BingLegitimate" ][ "OcpApimSubscriptionKey" ].GetOrDefault( "" ); + BaiduAppId = Config.Current.Preferences[ "Baidu" ][ "BaiduAppId" ].GetOrDefault( "" ); BaiduAppSecret = Config.Current.Preferences[ "Baidu" ][ "BaiduAppSecret" ].GetOrDefault( "" ); diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Constants/ClrTypes.cs b/src/XUnity.AutoTranslator.Plugin.Core/Constants/ClrTypes.cs index 91c392fc..5afdae3f 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Constants/ClrTypes.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Constants/ClrTypes.cs @@ -34,6 +34,8 @@ public static class ClrTypes public static readonly Type Typewriter = FindType( "Typewriter" ); public static readonly Type TextEditor = FindType( "UnityEngine.TextEditor" ); public static readonly Type CustomYieldInstruction = FindType( "UnityEngine.CustomYieldInstruction" ); + public static readonly Type SceneManager = FindType( "UnityEngine.SceneManager" ); + public static readonly Type Scene = FindType( "UnityEngine.SceneManagement.Scene" ); // Utage public static readonly Type UguiNovelText = FindType( "Utage.UguiNovelText" ); diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownEndpointNames.cs b/src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownEndpointNames.cs index ed2afd9b..707bfa5e 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownEndpointNames.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownEndpointNames.cs @@ -20,5 +20,9 @@ public static class KnownEndpointNames public const string WatsonTranslate = "WatsonTranslate"; public const string ExciteTranslate = "ExciteTranslate"; + + public const string BingTranslate = "BingTranslate"; + + public const string BingTranslateLegitimate = "BingTranslateLegitimate"; } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs b/src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs index 8f7ded82..f52b2223 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs @@ -11,6 +11,6 @@ public static class PluginData public const string Name = "XUnity Auto Translator"; - public const string Version = "2.16.0"; + public const string Version = "2.17.0"; } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Extensions/ObjectExtensions.cs b/src/XUnity.AutoTranslator.Plugin.Core/Extensions/ObjectExtensions.cs index 8d9da3b9..f4b1663c 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Extensions/ObjectExtensions.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Extensions/ObjectExtensions.cs @@ -39,7 +39,6 @@ public static bool IsKnownImageType( this object ui ) || ( 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.UIFont != null && ClrTypes.UIFont.IsAssignableFrom( type ) ) || ( ClrTypes.UIPanel != null && ClrTypes.UIPanel.IsAssignableFrom( type ) ); } @@ -47,6 +46,9 @@ 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 diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs b/src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs index 7ecdd289..bcf4ae2b 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs @@ -269,6 +269,19 @@ public static bool ContainsNumbers( this string text ) return false; } + public static bool StartsWithStrict( this string str, string prefix ) + { + var len = Math.Min( str.Length, prefix.Length ); + if( len < prefix.Length ) return false; + + for( int i = 0 ; i < len ; i++ ) + { + if( str[ i ] != prefix[ i ] ) return false; + } + + return true; + } + public static string UnescapeJson( this string str ) { if( str == null ) return null; diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Features.cs b/src/XUnity.AutoTranslator.Plugin.Core/Features.cs index 273f2129..ccce5bbf 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Features.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Features.cs @@ -12,6 +12,8 @@ public static class Features public static readonly bool SupportsCustomYieldInstruction = false; + public static readonly bool SupportsScenes = false; + static Features() { try @@ -31,6 +33,15 @@ static Features() { } + + try + { + SupportsScenes = ClrTypes.Scene != null && ClrTypes.SceneManager != null; + } + catch( Exception ) + { + + } } } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/KnownEndpoints.cs b/src/XUnity.AutoTranslator.Plugin.Core/KnownEndpoints.cs index 696e1667..1f9e8959 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/KnownEndpoints.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/KnownEndpoints.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using XUnity.AutoTranslator.Plugin.Core.Configuration; using XUnity.AutoTranslator.Plugin.Core.Constants; using XUnity.AutoTranslator.Plugin.Core.Web; @@ -20,15 +21,19 @@ public static IKnownEndpoint FindEndpoint( string identifier ) return new GoogleTranslateEndpoint(); //return new GoogleTranslateHackEndpoint(); case KnownEndpointNames.GoogleTranslateLegitimate: - return new GoogleTranslateLegitimateEndpoint(); + return new GoogleTranslateLegitimateEndpoint( Settings.GoogleAPIKey ); case KnownEndpointNames.BaiduTranslate: - return new BaiduTranslateEndpoint(); + return new BaiduTranslateEndpoint( Settings.BaiduAppId, Settings.BaiduAppSecret ); case KnownEndpointNames.YandexTranslate: - return new YandexTranslateEndpoint(); + return new YandexTranslateEndpoint( Settings.YandexAPIKey ); case KnownEndpointNames.WatsonTranslate: - return new WatsonTranslateEndpoint(); + return new WatsonTranslateEndpoint( Settings.WatsonAPIUrl, Settings.WatsonAPIUsername, Settings.WatsonAPIPassword ); case KnownEndpointNames.ExciteTranslate: return new ExciteTranslateEndpoint(); + //case KnownEndpointNames.BingTranslate: + // return new BingTranslateEndpoint(); + case KnownEndpointNames.BingTranslateLegitimate: + return new BingTranslateLegitimateEndpoint( Settings.BingOcpApimSubscriptionKey ); default: return new DefaultEndpoint( identifier ); } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Logger.cs b/src/XUnity.AutoTranslator.Plugin.Core/Logger.cs index e91106d8..4279f87e 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Logger.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Logger.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using XUnity.AutoTranslator.Plugin.Core.Configuration; +using XUnity.AutoTranslator.Plugin.Core.Constants; namespace XUnity.AutoTranslator.Plugin.Core { @@ -70,15 +71,15 @@ protected string GetPrefix( LogLevel level ) switch( level ) { case LogLevel.Debug: - return "[DEBUG][XUnity.AutoTranslator]: "; + return "[DEBUG][XUnity.AutoTranslator " + PluginData.Version + "]: "; case LogLevel.Info: - return "[INFO][XUnity.AutoTranslator]: "; + return "[INFO][XUnity.AutoTranslator " + PluginData.Version + "]: "; case LogLevel.Warn: - return "[WARN][XUnity.AutoTranslator]: "; + return "[WARN][XUnity.AutoTranslator " + PluginData.Version + "]: "; case LogLevel.Error: - return "[ERROR][XUnity.AutoTranslator]: "; + return "[ERROR][XUnity.AutoTranslator " + PluginData.Version + "]: "; default: - return "[UNKNOW][XUnity.AutoTranslator]: "; + return "[UNKNOW][XUnity.AutoTranslator " + PluginData.Version + "]: "; } } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/TemplatedString.cs b/src/XUnity.AutoTranslator.Plugin.Core/TemplatedString.cs index b726fd86..824f317f 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/TemplatedString.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/TemplatedString.cs @@ -23,10 +23,70 @@ public string Untemplate( string text ) return text; } - public string RepairTemplate( string text ) + public string RepairTemplate( string translatedTemplate ) { - // TODO: Implement template repairation. The web services might have mangled our parameterization - return text; + foreach( var argument in Arguments.Keys ) + { + if( !translatedTemplate.Contains( argument ) ) + { + var permutations = CreatePermutations( argument ); + foreach( var permutation in permutations ) + { + if( translatedTemplate.Contains( permutation ) ) + { + translatedTemplate = translatedTemplate.Replace( permutation, argument ); + break; + } + } + } + } + + return translatedTemplate; + } + + public static string[] CreatePermutations( string argument ) + { + var b0_1 = argument.Insert( 2, " " ); // {{ A}} + var b0_2 = argument.Insert( 3, " " ); // {{A }} + + var b1 = argument.Substring( 1 ); // {A}} + var b1_1 = b1.Insert( 1, " " ); // { A}} + var b1_2 = b1.Insert( 2, " " ); // {A }} + + var b2 = argument.Substring( 0, argument.Length - 1 ); // {{A} + var b2_1 = b2.Insert( 2, " " ); // {{ A} + var b2_2 = b2.Insert( 3, " " ); // {{A } + + var b3 = argument.Substring( 1, argument.Length - 2 ); // {A} + var b3_1 = b3.Insert( 1, " " ); // { A} + var b3_2 = b3.Insert( 2, " " ); // {A } + + return new string[] + { + b0_1, + b0_1, + b2, + b2_1, + b2_2, + b1, + b1_1, + b1_2, + b3, + b3_1, + b3_2, + b0_1.ToLower(), + b0_2.ToLower(), + argument.ToLower(), + b2.ToLower(), + b2_1.ToLower(), + b2_2.ToLower(), + b1.ToLower(), + b1_1.ToLower(), + b1_2.ToLower(), + b3.ToLower(), + b3_1.ToLower(), + b3_2.ToLower(), + }; } } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Web/BaiduTranslateEndpoint.cs b/src/XUnity.AutoTranslator.Plugin.Core/Web/BaiduTranslateEndpoint.cs index 52b022a9..694aefbd 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Web/BaiduTranslateEndpoint.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Web/BaiduTranslateEndpoint.cs @@ -15,12 +15,22 @@ namespace XUnity.AutoTranslator.Plugin.Core.Web public class BaiduTranslateEndpoint : KnownHttpEndpoint { 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(); - public BaiduTranslateEndpoint() + private string _appId; + private string _appSecret; + + public BaiduTranslateEndpoint( string baiduAppId, string baiduAppSecret ) { + if( string.IsNullOrEmpty( baiduAppId ) ) throw new ArgumentException( "The BaiduTranslate endpoint requires an App Id which has not been provided.", nameof( baiduAppId ) ); + if( string.IsNullOrEmpty( baiduAppSecret ) ) throw new ArgumentException( "The BaiduTranslate endpoint requires an App Secret which has not been provided.", nameof( baiduAppSecret ) ); + + _appId = baiduAppId; + _appSecret = baiduAppSecret; + ServicePointManager.ServerCertificateValidationCallback += Security.AlwaysAllowByHosts( "api.fanyi.baidu.com" ); + + SetupServicePoints( "http://api.fanyi.baidu.com" ); } private static string CreateMD5( string input ) @@ -81,9 +91,9 @@ public override bool TryExtractTranslated( string result, out string translated public override string GetServiceUrl( string untranslatedText, string from, string to ) { string salt = DateTime.UtcNow.Millisecond.ToString(); - var md5 = CreateMD5( Settings.BaiduAppId + untranslatedText + salt + Settings.BaiduAppSecret ); + var md5 = CreateMD5( _appId + untranslatedText + salt + _appSecret ); - return string.Format( HttpServicePointTemplateUrl, WWW.EscapeURL( untranslatedText ), from, to, Settings.BaiduAppId, salt, md5 ); + return string.Format( HttpServicePointTemplateUrl, WWW.EscapeURL( untranslatedText ), from, to, _appId, salt, md5 ); } } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Web/BingTranslateEndpoint.cs b/src/XUnity.AutoTranslator.Plugin.Core/Web/BingTranslateEndpoint.cs new file mode 100644 index 00000000..2dcb5d66 --- /dev/null +++ b/src/XUnity.AutoTranslator.Plugin.Core/Web/BingTranslateEndpoint.cs @@ -0,0 +1,245 @@ +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 Harmony; +using SimpleJSON; +using UnityEngine; +using XUnity.AutoTranslator.Plugin.Core.Configuration; +using XUnity.AutoTranslator.Plugin.Core.Constants; +using XUnity.AutoTranslator.Plugin.Core.Extensions; + +namespace XUnity.AutoTranslator.Plugin.Core.Web +{ + public class BingTranslateEndpoint : KnownHttpEndpoint + { + 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 string DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36"; + private static readonly System.Random RandomNumbers = new System.Random(); + + private static readonly 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(); + + // Configure service points / service point manager + ServicePointManager.ServerCertificateValidationCallback += Security.AlwaysAllowByHosts( "www.bing.com" ); + SetupServicePoints( "https://www.bing.com" ); + } + + public override bool SupportsLineSplitting => false; + + public override void ApplyHeaders( WebHeaderCollection headers ) + { + headers[ HttpRequestHeader.UserAgent ] = Settings.GetUserAgent( DefaultUserAgent ); + + if( AcceptLanguage != null ) + { + headers[ HttpRequestHeader.AcceptLanguage ] = AcceptLanguage; + } + if( Accept != null ) + { + headers[ HttpRequestHeader.Accept ] = Accept; + } + if( Referer != null ) + { + headers[ HttpRequestHeader.Referer ] = Referer; + } + if( Origin != null ) + { + headers[ "Origin" ] = Origin; + } + if( AcceptCharset != null ) + { + headers[ HttpRequestHeader.AcceptCharset ] = AcceptCharset; + } + if( ContentType != null ) + { + headers[ HttpRequestHeader.ContentType ] = ContentType; + } + } + + public override IEnumerator OnBeforeTranslate( int translationCount ) + { + 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 IEnumerator SetupIGAndIID() + { + string error = null; + DownloadResult downloadResult = null; + + _cookieContainer = new CookieContainer(); + _translationCount = 0; + + var client = GetClient(); + try + { + ApplyHeaders( client.Headers ); + client.Headers.Remove( HttpRequestHeader.Referer ); + client.Headers.Remove( "Origin" ); + client.Headers.Remove( HttpRequestHeader.ContentType ); + downloadResult = client.GetDownloadResult( new Uri( HttpsTranslateUserSite ) ); + } + catch( Exception e ) + { + error = e.ToString(); + } + + if( downloadResult != null ) + { + if( Features.SupportsCustomYieldInstruction ) + { + yield return downloadResult; + } + else + { + while( !downloadResult.IsCompleted ) + { + yield return new WaitForSeconds( 0.2f ); + } + } + + error = downloadResult.Error; + if( downloadResult.Succeeded && downloadResult.Result != null ) + { + try + { + var html = downloadResult.Result; + + _ig = Lookup( "ig\":\"", html ); + _iid = Lookup( ".init(\"/feedback/submission?\",\"", html ); + + if( _ig == null || _iid == null ) + { + Logger.Current.Warn( "An error occurred while setting up BingTranslate IG/IID. Proceeding without..." ); + } + } + catch( Exception e ) + { + error = e.ToString(); + } + } + } + + if( error != null ) + { + Logger.Current.Warn( "An error occurred while setting up BingTranslate IG. Proceeding without..." + Environment.NewLine + error ); + } + } + + 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; + } + + public override bool TryExtractTranslated( string result, out string translated ) + { + try + { + var obj = JSON.Parse( result ); + + var code = obj[ "statusCode" ].AsInt; + if( code != 200 ) + { + translated = null; + return false; + } + + var token = obj[ "translationResponse" ].ToString(); + token = token.Substring( 1, token.Length - 2 ).UnescapeJson(); + + translated = token; + + var success = !string.IsNullOrEmpty( translated ); + return success; + } + catch + { + translated = null; + return false; + } + } + + public override string GetRequestObject( string untranslatedText, string from, string to ) + { + return string.Format( RequestTemplate, WWW.EscapeURL( untranslatedText ), from, to ); + + } + + public override string GetServiceUrl( string untranslatedText, string from, string to ) + { + _translationCount++; + + if( _ig == null || _iid == null ) + { + return HttpsServicePointTemplateUrlWithoutIG; + } + else + { + return string.Format( HttpsServicePointTemplateUrl, _ig, _iid, _translationCount ); + } + } + + public override void WriteCookies( HttpWebResponse response ) + { + _cookieContainer.Add( response.Cookies ); + } + + public override CookieContainer ReadCookies() + { + return _cookieContainer; + } + } +} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Web/BingTranslateLegitimateEndpoint.cs b/src/XUnity.AutoTranslator.Plugin.Core/Web/BingTranslateLegitimateEndpoint.cs new file mode 100644 index 00000000..e4457d21 --- /dev/null +++ b/src/XUnity.AutoTranslator.Plugin.Core/Web/BingTranslateLegitimateEndpoint.cs @@ -0,0 +1,91 @@ +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 Harmony; +using SimpleJSON; +using UnityEngine; +using XUnity.AutoTranslator.Plugin.Core.Configuration; +using XUnity.AutoTranslator.Plugin.Core.Constants; +using XUnity.AutoTranslator.Plugin.Core.Extensions; + +namespace XUnity.AutoTranslator.Plugin.Core.Web +{ + public class BingTranslateLegitimateEndpoint : KnownHttpEndpoint + { + private static readonly string HttpsServicePointTemplateUrl = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from={0}&to={1}"; + private static readonly string RequestTemplate = "[{{\"Text\":\"{0}\"}}]"; + private static readonly System.Random RandomNumbers = new System.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 BingTranslateLegitimateEndpoint( string key ) + { + if( string.IsNullOrEmpty( key ) ) throw new ArgumentException( "The BingTranslateLegitimate endpoint requires an API key which has not been provided.", nameof( key ) ); + + _key = key; + + // Configure service points / service point manager + ServicePointManager.ServerCertificateValidationCallback += Security.AlwaysAllowByHosts( "api.cognitive.microsofttranslator.com" ); + SetupServicePoints( "https://api.cognitive.microsofttranslator.com" ); + } + + public override bool SupportsLineSplitting => false; + + public override void ApplyHeaders( WebHeaderCollection headers ) + { + if( Accept != null ) + { + headers[ HttpRequestHeader.Accept ] = Accept; + } + if( ContentType != null ) + { + headers[ HttpRequestHeader.ContentType ] = ContentType; + } + + headers[ "Ocp-Apim-Subscription-Key" ] = _key; + } + + public override bool TryExtractTranslated( string result, out string translated ) + { + try + { + var arr = JSON.Parse( result ); + + var token = arr.AsArray[ 0 ]?.AsObject[ "translations" ]?.AsArray[ 0 ]?.AsObject[ "text" ]?.ToString(); + token = token.Substring( 1, token.Length - 2 ).UnescapeJson(); + + translated = token; + + var success = !string.IsNullOrEmpty( translated ); + return success; + } + catch + { + translated = null; + return false; + } + } + + public override string GetRequestObject( string untranslatedText, string from, string to ) + { + return string.Format( RequestTemplate, untranslatedText.EscapeJson() ); + + } + + public override string GetServiceUrl( string untranslatedText, string from, string to ) + { + return string.Format( HttpsServicePointTemplateUrl, from, to ); + } + } +} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Web/DefaultEndpoint.cs b/src/XUnity.AutoTranslator.Plugin.Core/Web/DefaultEndpoint.cs index 83c9e0fb..ba77940e 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Web/DefaultEndpoint.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Web/DefaultEndpoint.cs @@ -14,7 +14,11 @@ public class DefaultEndpoint : KnownHttpEndpoint public DefaultEndpoint( string endpoint ) { _endpoint = endpoint; - ServicePointManager.ServerCertificateValidationCallback += Security.AlwaysAllowByHosts( new Uri( _endpoint ).Host ); + + var uri = new Uri( endpoint ); + ServicePointManager.ServerCertificateValidationCallback += Security.AlwaysAllowByHosts( uri.Host ); + + SetupServicePoints( uri.Scheme + "://" + uri.Host + ":" + uri.Port ); } public override void ApplyHeaders( WebHeaderCollection headers ) diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Web/GoogleTranslateLegitimateEndpoint.cs b/src/XUnity.AutoTranslator.Plugin.Core/Web/GoogleTranslateLegitimateEndpoint.cs index b5efa82c..b661b85b 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Web/GoogleTranslateLegitimateEndpoint.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Web/GoogleTranslateLegitimateEndpoint.cs @@ -19,8 +19,14 @@ public class GoogleTranslateLegitimateEndpoint : KnownHttpEndpoint { private static readonly string HttpsServicePointTemplateUrl = "https://translation.googleapis.com/language/translate/v2?key={0}"; - public GoogleTranslateLegitimateEndpoint() + private string _key; + + public GoogleTranslateLegitimateEndpoint( string key ) { + if( string.IsNullOrEmpty( key ) ) throw new ArgumentException( "The GoogleTranslateLegitimate endpoint requires an API key which has not been provided.", nameof( key ) ); + + _key = key; + // Configure service points / service point manager ServicePointManager.ServerCertificateValidationCallback += Security.AlwaysAllowByHosts( "translation.googleapis.com" ); SetupServicePoints( "https://translation.googleapis.com" ); @@ -63,7 +69,7 @@ public override bool TryExtractTranslated( string result, out string translated public override string GetServiceUrl( string untranslatedText, string from, string to ) { - return string.Format( HttpsServicePointTemplateUrl, WWW.EscapeURL( Settings.GoogleAPIKey ) ); + return string.Format( HttpsServicePointTemplateUrl, WWW.EscapeURL( _key ) ); } public override string GetRequestObject( string untranslatedText, string from, string to ) @@ -83,30 +89,4 @@ public override bool ShouldGetSecondChanceAfterFailure() return false; } } - - //public class GRequest - //{ - // public string q { get; set; } - - // public string target { get; set; } - - // public string source { get; set; } - - // public string format { get; set; } - //} - - //public class GResponse - //{ - // public GData data { get; set; } - //} - - //public class GData - //{ - // public List translations { get; set; } - //} - - //public class GTranslation - //{ - // public string translatedText { get; set; } - //} } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Web/WatsonTranslateEndpoint.cs b/src/XUnity.AutoTranslator.Plugin.Core/Web/WatsonTranslateEndpoint.cs index e9655b18..358e3635 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Web/WatsonTranslateEndpoint.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Web/WatsonTranslateEndpoint.cs @@ -15,9 +15,21 @@ public class WatsonTranslateEndpoint : KnownWwwEndpoint { private static readonly string HttpsServicePointTemplateUrl = Settings.WatsonAPIUrl.TrimEnd( '/' ) + "/v2/translate?model_id={0}-{1}&text={2}"; - public WatsonTranslateEndpoint() + private string _url; + private string _username; + private string _password; + + public WatsonTranslateEndpoint( string url, string username, string password ) { - //ServicePointManager.ServerCertificateValidationCallback += Security.AlwaysAllowByHosts( new Uri( Settings.WatsonAPIUrl ).Host ); + if( string.IsNullOrEmpty( url ) ) throw new ArgumentException( "The WatsonTranslate endpoint requires a url which has not been provided.", nameof( url ) ); + if( string.IsNullOrEmpty( username ) ) throw new ArgumentException( "The WatsonTranslate endpoint requires a username which has not been provided.", nameof( username ) ); + if( string.IsNullOrEmpty( password ) ) throw new ArgumentException( "The WatsonTranslate endpoint requires a password which has not been provided.", nameof( password ) ); + + _url = url; + _username = username; + _password = password; + + //ServicePointManager.ServerCertificateValidationCallback += Security.AlwaysAllowByHosts( new Uri( _url ).Host ); } public override void ApplyHeaders( Dictionary headers ) @@ -25,7 +37,7 @@ public override void ApplyHeaders( Dictionary headers ) headers[ "User-Agent" ] = Settings.GetUserAgent( "curl/7.55.1" ); headers[ "Accept" ] = "application/json"; headers[ "Accept-Charset" ] = "UTF-8"; - headers[ "Authorization" ] = "Basic " + System.Convert.ToBase64String( System.Text.Encoding.ASCII.GetBytes( Settings.WatsonAPIUsername + ":" + Settings.WatsonAPIPassword ) ); + headers[ "Authorization" ] = "Basic " + System.Convert.ToBase64String( System.Text.Encoding.ASCII.GetBytes( _username + ":" + _password ) ); } //public override void ApplyHeaders( WebHeaderCollection headers ) @@ -33,7 +45,7 @@ public override void ApplyHeaders( Dictionary headers ) // headers[ HttpRequestHeader.UserAgent ] = "curl/7.55.1"; // headers[ HttpRequestHeader.Accept ] = "application/json"; // headers[ HttpRequestHeader.AcceptCharset ] = "UTF-8"; - // headers[ HttpRequestHeader.Authorization ] = "Basic " + System.Convert.ToBase64String( System.Text.Encoding.ASCII.GetBytes( Settings.WatsonAPIUsername + ":" + Settings.WatsonAPIPassword ) ); + // headers[ HttpRequestHeader.Authorization ] = "Basic " + System.Convert.ToBase64String( System.Text.Encoding.ASCII.GetBytes( _username + ":" + _password ) ); //} public override bool TryExtractTranslated( string result, out string translated ) diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Web/YandexTranslateEndpoint.cs b/src/XUnity.AutoTranslator.Plugin.Core/Web/YandexTranslateEndpoint.cs index 0a277b02..0a957fd2 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Web/YandexTranslateEndpoint.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Web/YandexTranslateEndpoint.cs @@ -14,9 +14,18 @@ namespace XUnity.AutoTranslator.Plugin.Core.Web public class YandexTranslateEndpoint : KnownHttpEndpoint { private static readonly string HttpsServicePointTemplateUrl = "https://translate.yandex.net/api/v1.5/tr.json/translate?key={3}&text={2}&lang={0}-{1}&format=plain"; - public YandexTranslateEndpoint() + + private string _key; + + public YandexTranslateEndpoint( string key ) { + if( string.IsNullOrEmpty( key ) ) throw new ArgumentException( "The YandexTranslate endpoint requires an API key which has not been provided.", nameof( key ) ); + + _key = key; + ServicePointManager.ServerCertificateValidationCallback += Security.AlwaysAllowByHosts( "translate.yandex.net" ); + + SetupServicePoints( "https://translate.yandex.net" ); } public override void ApplyHeaders( WebHeaderCollection headers ) @@ -69,7 +78,7 @@ public override bool TryExtractTranslated( string result, out string translated public override string GetServiceUrl( string untranslatedText, string from, string to ) { - return string.Format( HttpsServicePointTemplateUrl, from, to, WWW.EscapeURL( untranslatedText ), Settings.YandexAPIKey ); + return string.Format( HttpsServicePointTemplateUrl, from, to, WWW.EscapeURL( untranslatedText ), _key ); } } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj b/src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj index 252dd8a3..9e1c12d5 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj +++ b/src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj @@ -2,7 +2,7 @@ net35 - 2.16.0 + 2.17.0 diff --git a/src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj b/src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj index 82125f41..531daee3 100644 --- a/src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj +++ b/src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj @@ -2,7 +2,7 @@ net35 - 2.16.0 + 2.17.0 diff --git a/src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj b/src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj index b6184783..b998f44c 100644 --- a/src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj +++ b/src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj @@ -2,7 +2,7 @@ net35 - 2.16.0 + 2.17.0 diff --git a/src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj b/src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj index 64e4f693..71e7c52c 100644 --- a/src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj +++ b/src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj @@ -4,7 +4,7 @@ Exe net40 SetupReiPatcherAndAutoTranslator - 2.16.0 + 2.17.0