diff --git a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/Model/Interface/IMtTranslationOptions.cs b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/Model/Interface/IMtTranslationOptions.cs index 5bf6bdccbe..ab72a8254d 100644 --- a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/Model/Interface/IMtTranslationOptions.cs +++ b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/Model/Interface/IMtTranslationOptions.cs @@ -21,6 +21,7 @@ public interface IMtTranslationOptions string JsonFilePath { get; set; } string ProjectName { get; set; } string GlossaryPath { get; set; } + string PeUrl { get; set; } // Microsoft private endpont url string ApiKey { get; set; } //Google Key string ClientId { get; set; } // Microsoft key string Region { get; set; } // Microsoft Region diff --git a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MstConnect/ApiConnecterWithPe.cs b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MstConnect/ApiConnecterWithPe.cs new file mode 100644 index 0000000000..2ca573a83a --- /dev/null +++ b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MstConnect/ApiConnecterWithPe.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Newtonsoft.Json; +using NLog; +using RestSharp; +using Sdl.Community.MtEnhancedProvider.Model; +using Sdl.Community.MtEnhancedProvider.Service; + +namespace Sdl.Community.MtEnhancedProvider.MstConnect +{ + internal class ApiConnecterWithPe + { + private const string OcpApimSubscriptionKeyHeader = "Ocp-Apim-Subscription-Key"; + + private List _supportedLangs; + + private readonly string _peUrl; + private string _subscriptionKey; + private string _region; + private readonly Logger _logger = LogManager.GetCurrentClassLogger(); + private HtmlUtil _htmlUtil; + + /// + /// This class allows connection to the Microsoft Translation API + /// + /// Microsoft API key + /// Region + internal ApiConnecterWithPe(string peUrl, string subscriptionKey, string region, HtmlUtil htmlUtil) + { + _peUrl = peUrl; + _subscriptionKey = subscriptionKey; + _region = region; + _htmlUtil = htmlUtil; + + if (_supportedLangs == null) + { + _supportedLangs = GetSupportedLanguages(); //if the class variable has not been set + } + } + + /// + /// translates the text input + /// + internal string Translate(string sourceLang, string targetLang, string textToTranslate, string categoryId) + { + //convert our language codes + var sourceLc = ConvertLangCode(sourceLang); + var targetLc = ConvertLangCode(targetLang); + + var translatedText = string.Empty; + try + { + //search for words like this + var rgx = new Regex("(\\<\\w+[üäåëöøßşÿÄÅÆĞ]*[^\\d\\W\\\\/\\\\]+\\>)"); + var words = rgx.Matches(textToTranslate); + if (words.Count > 0) + { + textToTranslate = ReplaceCharacters(textToTranslate, words); + } + + const string path = "/translate?api-version=3.0"; + var category = categoryId == "" ? "general" : categoryId; + var languageParams = $"&from={sourceLc}&to={targetLc}&textType=html&category={category}"; + + var uri = string.Concat(_peUrl, path, languageParams); + var body = new object[] + { + new + { + Text =textToTranslate + } + }; + var requestBody = JsonConvert.SerializeObject(body); + using (var httpClient = new HttpClient()) + { + using (var httpRequest = new HttpRequestMessage()) + { + httpRequest.Method = HttpMethod.Post; + httpRequest.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); + httpRequest.RequestUri = new Uri(uri); + httpRequest.Headers.Add(OcpApimSubscriptionKeyHeader, _subscriptionKey); + + var response = httpClient.SendAsync(httpRequest).Result; + var responseBody = response.Content.ReadAsStringAsync().Result; + if (response.IsSuccessStatusCode) + { + var responseTranslation = JsonConvert.DeserializeObject>(responseBody); + translatedText = _htmlUtil.HtmlDecode(responseTranslation[0]?.Translations[0]?.Text); + } + else + { + var responseMessage = JsonConvert.DeserializeObject(responseBody); + throw new Exception(responseMessage.Error.Message); + } + } + } + } + catch (WebException exception) + { + var mesg = ProcessWebException(exception, PluginResources.MsApiFailedGetLanguagesMessage); + _logger.Error($"{MethodBase.GetCurrentMethod().Name}\n {exception.Message}\n { exception.StackTrace}"); + throw new Exception(mesg); + } + return translatedText; + } + + private string ReplaceCharacters(string textToTranslate, MatchCollection matches) + { + var indexes = new List(); + foreach (Match match in matches) + { + if (match.Index.Equals(0)) + { + indexes.Add(match.Length); + } + else + { + //check if there is any text after PI + var remainingText = textToTranslate.Substring(match.Index + match.Length); + if (!string.IsNullOrEmpty(remainingText)) + { + //get the position where PI starts to split before + indexes.Add(match.Index); + //split after PI + indexes.Add(match.Index + match.Length); + } + else + { + indexes.Add(match.Index); + } + } + } + var splitText = textToTranslate.SplitAt(indexes.ToArray()).ToList(); + var positions = new List(); + for (var i = 0; i < splitText.Count; i++) + { + if (!splitText[i].Contains("tg")) + { + positions.Add(i); + } + } + + foreach (var position in positions) + { + var originalString = splitText[position]; + var start = Regex.Replace(originalString, "<", "<"); + var finalString = Regex.Replace(start, ">", ">"); + splitText[position] = finalString; + } + var finalText = string.Empty; + foreach (var text in splitText) + { + finalText += text; + } + return finalText; + } + + /// + /// Checks of lang pair is supported by MS + /// + internal bool IsSupportedLangPair(string sourceLang, string targetLang) + { + //convert our language codes + var source = ConvertLangCode(sourceLang); + var target = ConvertLangCode(targetLang); + + var sourceSupported = false; + var targetSupported = false; + + //check to see if both the source and target languages are supported + foreach (var lang in _supportedLangs) + { + if (lang.Equals(source)) sourceSupported = true; + if (lang.Equals(target)) targetSupported = true; + } + + if (sourceSupported && targetSupported) return true; //if both are supported return true + + //otherwise return false + return false; + } + + private List GetSupportedLanguages() + { + var languageCodeList = new List(); + try + { + var uri = new Uri(_peUrl); + var client = new RestClient(uri); + + var request = new RestRequest("languages", Method.Get); + request.AddParameter("api-version", "3.0"); + request.AddParameter("scope", "translation"); + request.AddHeader(OcpApimSubscriptionKeyHeader, _subscriptionKey); + + var languageResponse = client.ExecuteAsync(request).Result; + + if (!languageResponse.IsSuccessful) throw new Exception("Error on connecting to translator. " + languageResponse.Content); + + var languages = JsonConvert.DeserializeObject(languageResponse.Content); + if (languages != null) + { + foreach (var language in languages.Translation) + { + languageCodeList.Add(language.Key); + } + } + } + catch (WebException exception) + { + var mesg = ProcessWebException(exception, PluginResources.MsApiFailedGetLanguagesMessage); + _logger.Error($"{MethodBase.GetCurrentMethod().Name}\n{exception.Message}\n { exception.StackTrace}"); + throw new Exception(mesg); + } + return languageCodeList; + } + + private string ProcessWebException(WebException e, string message) + { + _logger.Error($"{MethodBase.GetCurrentMethod().Name}\n{e.Response}\n {message}"); + + // Obtain detailed error information + string strResponse; + using (var response = (HttpWebResponse)e.Response) + { + using (var responseStream = response.GetResponseStream()) + { + using (var sr = new StreamReader(responseStream, Encoding.ASCII)) + { + strResponse = sr.ReadToEnd(); + } + } + } + return $"Http status code={e.Status}, error message={strResponse}"; + } + + internal void EnsureConnectivity() + { + var result = Translate("en", "de", "Hello World!", ""); + + if (string.IsNullOrEmpty(result)) + { + throw new InvalidOperationException("No Connection could be established"); + } + } + + private string ConvertLangCode(string languageCode) + { + //takes the language code input and converts it to one that MS Translate can use + if (languageCode.Contains("sr-Cyrl")) return "sr-Cyrl"; + if (languageCode.Contains("sr-Latn")) return "sr-Latn"; + + var ci = new CultureInfo(languageCode); //construct a CultureInfo object with the language code + + //deal with chinese..MS Translator has different ones + if (new[] { "zh-TW", "zh-HK", "zh-MO", "zh-Hant", "zh-CHT" }.Contains(ci.Name)) return "zh-Hant"; + if (new[] { "zh-CN", "zh-SG", "zh-Hans-HK", "zh-Hans-MO", "zh-Hans", "zh-CHS" }.Contains(ci.Name)) return "zh-Hans"; + + return ci.TwoLetterISOLanguageName; + + } + } +} diff --git a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationOptions.cs b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationOptions.cs index b8a7c2cd8b..c8de3d16d0 100644 --- a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationOptions.cs +++ b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationOptions.cs @@ -31,7 +31,8 @@ public class MtTranslationOptions: IMtTranslationOptions private string _apiKey; private string _clientid; const string MsTranslatorString = "Microsoft Translator"; //these strings should not be localized or changed and are therefore hard-coded as constants - const string GTranslateString = "Google Translate"; //these strings should not be localized or changed and are therefore hard-coded as constants + const string MsTranslatorWithPeString = "Microsoft Translator with Private Endpoint"; //these strings should not be localized or changed and are therefore hard-coded as constants + const string GTranslateString = "Google Translate"; //these strings should not be localized or changed and are therefore hard-coded as constants //The translation method affects when/if the plugin gets called by Studio public static readonly TranslationMethod ProviderTranslationMethod = TranslationMethod.MachineTranslation; @@ -158,6 +159,7 @@ public enum ProviderType { GoogleTranslate = 1, MicrosoftTranslator = 2, + MicrosoftTranslatorWithPe = 3, None = 0 } @@ -169,7 +171,9 @@ public static string GetProviderTypeDescription(ProviderType type) return GTranslateString; //these strings should not be localized and are therefore hard-coded case ProviderType.MicrosoftTranslator: return MsTranslatorString; //these strings should not be localized and are therefore hard-coded - } + case ProviderType.MicrosoftTranslatorWithPe: + return MsTranslatorWithPeString; //these strings should not be localized and are therefore hard-coded + } return ""; } @@ -185,7 +189,9 @@ public static ProviderType GetProviderType(string typeString) return ProviderType.GoogleTranslate; case MsTranslatorString: return ProviderType.MicrosoftTranslator; - default: + case MsTranslatorWithPeString: + return ProviderType.MicrosoftTranslatorWithPe; + default: return ProviderType.None; } } @@ -247,6 +253,13 @@ public string ApiKey set => _apiKey = value; } + // The Microsoft private endpoint url + public string PeUrl + { + get => GetStringParameter("peurl"); + set => SetStringParameter("peurl", value); + } + [JsonIgnore] //User for Microsoft authentication public string ClientId diff --git a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProvider.cs b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProvider.cs index 64ed73db4d..9b72dd96b7 100644 --- a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProvider.cs +++ b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProvider.cs @@ -34,6 +34,7 @@ public class MtTranslationProvider : ITranslationProvider private MtTranslationProviderGTApiConnecter _gtConnect; private GoogleV3Connecter _googleV3Connecter; private ApiConnecter _mstConnect; + private ApiConnecterWithPe _mstConnectWithPe; private readonly HtmlUtil _htmlUtil; public MtTranslationProvider(IMtTranslationOptions options, RegionsProvider regionProvider, HtmlUtil htmlUtil) @@ -143,6 +144,16 @@ public bool SupportsLanguageDirection(LanguagePair languageDirection) return _mstConnect.IsSupportedLangPair(languageDirection.SourceCulture.Name, languageDirection.TargetCulture.Name); } + if (Options.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslatorWithPe) + { + if (_mstConnectWithPe == null) //construct ApiConnecter if necessary + { + _mstConnectWithPe = new ApiConnecterWithPe(Options.PeUrl, Options.ClientId, Options.Region, _htmlUtil); + } + + return _mstConnectWithPe.IsSupportedLangPair(languageDirection.SourceCulture.Name, languageDirection.TargetCulture.Name); + } + if (Options.SelectedGoogleVersion == Enums.GoogleApiVersion.V2) { if (_gtConnect == null) //instantiate GtApiConnecter if necessary diff --git a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderFactory.cs b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderFactory.cs index 93295dbf8b..e57a2b16e8 100644 --- a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderFactory.cs +++ b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderFactory.cs @@ -40,7 +40,7 @@ public ITranslationProvider CreateTranslationProvider(Uri translationProviderUri var htmlUtil = new HtmlUtil(); //start with MT...check if we are using MT - if (loadOptions.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslator) + if (loadOptions.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslator || loadOptions.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslatorWithPe) { // The credential is saved with a different URI scheme than that of the plugin! // We will need to make this known and/or provide a workaround in identifying the credentials diff --git a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderLanguageDirection.cs b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderLanguageDirection.cs index 0f21651f0c..2d55950c4b 100644 --- a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderLanguageDirection.cs +++ b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderLanguageDirection.cs @@ -22,6 +22,7 @@ public class MtTranslationProviderLanguageDirection : ITranslationProviderLangua private GoogleV3Connecter _googleV3Connecter; private TranslationUnit _inputTu; private ApiConnecter _mstConnect; + private ApiConnecterWithPe _mstConnectWithPe; private SegmentEditor _postLookupSegmentEditor; private SegmentEditor _preLookupSegmentEditor; @@ -219,19 +220,34 @@ private string LookupMst(string sourcetext, IMtTranslationOptions options, strin var sourcelang = _languageDirection.SourceCulture.ToString(); var targetlang = _languageDirection.TargetCulture.ToString(); - //instantiate ApiConnecter if necessary - if (_mstConnect == null) + if (options.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslator) { - _mstConnect = new ApiConnecter(_options.ClientId, options.Region, _htmlUtil); + //instantiate ApiConnecter if necessary + if (_mstConnect == null) + { + _mstConnect = new ApiConnecter(_options.ClientId, options.Region, _htmlUtil); + } + else + { + //reset key in case it has been changed in dialog since GtApiConnecter was instantiated + _mstConnect.ResetCrd(options.ClientId, options.Region); + } + var translatedText = _mstConnect.Translate(sourcelang, targetlang, sourcetext, catId); + return translatedText; } - else + else if(options.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslatorWithPe) { - //reset key in case it has been changed in dialog since GtApiConnecter was instantiated - _mstConnect.ResetCrd(options.ClientId, options.Region); + //instantiate ApiConnecter if necessary + if (_mstConnectWithPe == null) + { + _mstConnectWithPe = new ApiConnecterWithPe(_options.PeUrl, _options.ClientId, options.Region, _htmlUtil); + } + + var translatedText = _mstConnectWithPe.Translate(sourcelang, targetlang, sourcetext, catId); + return translatedText; } - var translatedText = _mstConnect.Translate(sourcelang, targetlang, sourcetext, catId); - return translatedText; + throw new InvalidOperationException("No proper provider specified in options."); } /// @@ -278,7 +294,8 @@ public SearchResults SearchSegment(SearchSettings settings, Segment segment) { translatedText = LookupGt(tagplacer.PreparedSourceText, _options, "html"); } - else if (_options.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslator) + else if (_options.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslator + || _options.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslatorWithPe) { translatedText = LookupMst(tagplacer.PreparedSourceText, _options, "text/html"); } @@ -314,6 +331,9 @@ public SearchResults SearchSegment(SearchSettings settings, Segment segment) case MtTranslationOptions.ProviderType.MicrosoftTranslator: translatedText = LookupMst(sourcetext, _options, "text/plain"); break; + case MtTranslationOptions.ProviderType.MicrosoftTranslatorWithPe: + translatedText = LookupMst(sourcetext, _options, "text/plain"); + break; } //now do post-edit if that option is checked if (_options.UsePostEdit) diff --git a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderWinFormsUI.cs b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderWinFormsUI.cs index 4f4106018e..c30f360db4 100644 --- a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderWinFormsUI.cs +++ b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/MtTranslationProviderWinFormsUI.cs @@ -130,7 +130,8 @@ public TranslationProviderDisplayInfo GetDisplayInfo(Uri translationProviderUri, } info.SearchResultImage = PluginResources.my_image; } - else if (options.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslator) + else if (options.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslator + || options.SelectedProvider == MtTranslationOptions.ProviderType.MicrosoftTranslatorWithPe) { info.Name = PluginResources.Microsoft_NiceName; info.TooltipText = PluginResources.Microsoft_Tooltip; @@ -196,6 +197,11 @@ private void UpdateProviderCredentials(ITranslationProviderCredentialStore crede SetCredentialsOnCredentialStore(credentialStore, PluginResources.UriMs, options.ClientId, options.PersistMicrosoftCreds); break; + case MtTranslationOptions.ProviderType.MicrosoftTranslatorWithPe: + //set mst cred + SetCredentialsOnCredentialStore(credentialStore, PluginResources.UriMs, options.ClientId, + options.PersistMicrosoftCreds); + break; } } diff --git a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/PluginResources.Designer.cs b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/PluginResources.Designer.cs index d2019676c8..c4d2ec1950 100644 --- a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/PluginResources.Designer.cs +++ b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/PluginResources.Designer.cs @@ -478,6 +478,24 @@ public static string MicrosoftApiDescription { } } + /// + /// Looks up a localized string similar to Microsoft Private Endpoint Url. + /// + public static string MicrosoftPeDescription { + get { + return ResourceManager.GetString("MicrosoftPeDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Microsoft Translator with Private Endpoint. + /// + public static string MicrosoftWithPe { + get { + return ResourceManager.GetString("MicrosoftWithPe", resourceCulture); + } + } + /// /// Looks up a localized string similar to Authentication error: bad credentials. /// @@ -561,6 +579,15 @@ public static string PersistMicrosoft { } } + /// + /// Looks up a localized string similar to Private Endpont Url not valid. + /// + public static string PeUrlError { + get { + return ResourceManager.GetString("PeUrlError", resourceCulture); + } + } + /// /// Looks up a localized string similar to MT Enhanced Trados Plugin - Choose between Google Translate or Microsoft Translator. /// diff --git a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/PluginResources.resx b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/PluginResources.resx index 0520778edd..366edd71b8 100644 --- a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/PluginResources.resx +++ b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/PluginResources.resx @@ -405,4 +405,13 @@ Please enter your Google Translate API Key + + Microsoft Translator with Private Endpoint + + + Microsoft Private Endpoint Url + + + Private Endpont Url not valid + \ No newline at end of file diff --git a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/Sdl.Community.MtEnhancedProvider.csproj b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/Sdl.Community.MtEnhancedProvider.csproj index 54faa0e8ec..c86e2575d3 100644 --- a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/Sdl.Community.MtEnhancedProvider.csproj +++ b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/Sdl.Community.MtEnhancedProvider.csproj @@ -177,7 +177,19 @@ MSBuild:Compile - + + + True + True + PluginResources.resx + + + + + PublicResXFileCodeGenerator + PluginResources.Designer.cs + + true {B008D046-5127-4120-A5BA-41E4E27F35A8} @@ -197,6 +209,6 @@ my_icon.ico True - C:\Code\SDLCOM - Studio 2022\SdlCommunity.snk + SdlCommunity.snk \ No newline at end of file diff --git a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/View/ProviderControl.xaml b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/View/ProviderControl.xaml index 74daf54f1b..83a9e1eff8 100644 --- a/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/View/ProviderControl.xaml +++ b/MT Enhanced Provider/Sdl.Community.MtEnhancedProvider/View/ProviderControl.xaml @@ -37,7 +37,7 @@ SelectedItem="{Binding SelectedTranslationOption}" DisplayMemberPath="Name" DockPanel.Dock="Left" - Width="200"/> + Width="266"/>