diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 16ec3ca9..06731ae9 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -30,6 +30,7 @@ + diff --git a/src/ResXManager.Tests/Model/TranslatorTests.cs b/src/ResXManager.Tests/Model/TranslatorTests.cs new file mode 100644 index 00000000..780db7ae --- /dev/null +++ b/src/ResXManager.Tests/Model/TranslatorTests.cs @@ -0,0 +1,42 @@ +namespace ResXManager.Tests.Model; + +using System; + +using Xunit; + +public class TranslatorTests +{ + [Fact] + public void GoogleLiteParsesFragmentedResponseCorrectly() + { + const string input = """ + [[["Ei! ","Hey there! ",null,null,10],["Como tá indo? ","How's it going? ",null,null,10],["Isso é ótimo! ","That's great! ",null,null,10],["K, tchau","K, bye",null,null,3,null,null,[[]],[[["66379e56ded86dd057796dbeaebad517","en_pt_2023q1.md"]]]]],null,"en",null,null,null,null,[]] + """; + + var result = Translators.GoogleTranslatorLite.ParseResponse(input); + + Assert.Equal("Ei! Como tá indo? Isso é ótimo! K, tchau", result); + } + + [Fact] + public void GoogleLiteParsesInvalidResponseCorrectly() + { + const string input = $$""" + {"x":[["Ei! "]]} + """; + + var result = Translators.GoogleTranslatorLite.ParseResponse(input); + + Assert.Equal("", result); + } + + [Fact] + public void GoogleLiteThrowsOnBadJson() + { + const string input = $$""" + {"x":Ei!"]]} + """; + + Assert.ThrowsAny(() => Translators.GoogleTranslatorLite.ParseResponse(input)); + } +} diff --git a/src/ResXManager.Tests/ResXManager.Tests.csproj b/src/ResXManager.Tests/ResXManager.Tests.csproj index c8b16b0d..1b55c98c 100644 --- a/src/ResXManager.Tests/ResXManager.Tests.csproj +++ b/src/ResXManager.Tests/ResXManager.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/src/ResXManager.Translators/GoogleTranslatorLite.cs b/src/ResXManager.Translators/GoogleTranslatorLite.cs index 16aae63e..40256e96 100644 --- a/src/ResXManager.Translators/GoogleTranslatorLite.cs +++ b/src/ResXManager.Translators/GoogleTranslatorLite.cs @@ -7,16 +7,14 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Runtime.Serialization; using System.Text; -using System.Text.RegularExpressions; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; using ResXManager.Infrastructure; -using TomsToolbox.Essentials; using TomsToolbox.Wpf.Composition.AttributedModel; [DataTemplate(typeof(GoogleTranslatorLite))] @@ -25,15 +23,10 @@ public class GoogleTranslatorLiteConfiguration : Decorator } [Export(typeof(ITranslator)), Shared] -public class GoogleTranslatorLite : TranslatorBase +public class GoogleTranslatorLite() : TranslatorBase("GoogleLite", "Google Lite", _uri, null) { private static readonly Uri _uri = new("https://translate.google.com/"); - public GoogleTranslatorLite() - : base("GoogleLite", "Google Lite", _uri, null) - { - } - protected override async Task Translate(ITranslationSession translationSession) { foreach (var languageGroup in translationSession.Items.GroupBy(item => item.TargetCulture)) @@ -43,35 +36,24 @@ protected override async Task Translate(ITranslationSession translationSession) var targetCulture = languageGroup.Key.Culture ?? translationSession.NeutralResourcesLanguage; - using var itemsEnumerator = languageGroup.GetEnumerator(); - while (true) + foreach (var sourceItem in languageGroup) { - var sourceItems = itemsEnumerator.Take(1); - if (translationSession.IsCanceled || !sourceItems.Any()) + if (translationSession.IsCanceled) break; var parameters = new List(30); - // ReSharper disable once PossibleNullReferenceException - parameters.AddRange(new[] - { - "client", "dict-chrome-ex", + parameters.AddRange( + [ + "client", "gtx", + "dt", "t", "sl", GoogleLangCode(translationSession.SourceLanguage), "tl", GoogleLangCode(targetCulture), - "q", RemoveKeyboardShortcutIndicators(sourceItems[0].Source) - }); - - // ReSharper disable once AssignNullToNotNullAttribute - var response = await GetHttpResponse( - "https://clients5.google.com/translate_a/t", - parameters, - translationSession.CancellationToken).ConfigureAwait(false); + "q", RemoveKeyboardShortcutIndicators(sourceItem.Source) + ]); - await translationSession.MainThread.StartNew(() => - { - Tuple tuple = new(sourceItems[0], new Translation { TranslatedText = response }); - tuple.Item1.Results.Add(new TranslationMatch(this, tuple.Item2.TranslatedText, Ranking)); - }).ConfigureAwait(false); + var response = await GetHttpResponse("https://translate.googleapis.com/translate_a/single", parameters, translationSession.CancellationToken).ConfigureAwait(false); + await translationSession.MainThread.StartNew(() => { sourceItem.Results.Add(new TranslationMatch(this, response, Ranking)); }).ConfigureAwait(false); } } } @@ -81,8 +63,9 @@ private static string GoogleLangCode(CultureInfo cultureInfo) var iso1 = cultureInfo.TwoLetterISOLanguageName; var name = cultureInfo.Name; + string[] twCultures = ["zh-hant", "zh-cht", "zh-hk", "zh-mo", "zh-tw"]; if (string.Equals(iso1, "zh", StringComparison.OrdinalIgnoreCase)) - return new[] { "zh-hant", "zh-cht", "zh-hk", "zh-mo", "zh-tw" }.Contains(name, StringComparer.OrdinalIgnoreCase) ? "zh-TW" : "zh-CN"; + return twCultures.Contains(name, StringComparer.OrdinalIgnoreCase) ? "zh-TW" : "zh-CN"; if (string.Equals(name, "haw-us", StringComparison.OrdinalIgnoreCase)) return "haw"; @@ -99,17 +82,21 @@ private static async Task GetHttpResponse(string baseUrl, ICollection not available in .NET Framework var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - result = result.Substring(2, result.Length - 4); - return Regex.Unescape(result); + + return ParseResponse(result); } - [DataContract] - private sealed class Translation + public static string ParseResponse(string result) { - [DataMember(Name = "translatedText")] - public string? TranslatedText { get; set; } + var node = JsonNode.Parse(result); + + if ((node is JsonArray level1) && (level1.FirstOrDefault() is JsonArray level2)) + { + return string.Concat(level2.OfType().Select(item => item.FirstOrDefault())); + } + + return string.Empty; } /// Builds the URL from a base, method name, and name/value paired parameters. All parameters are encoded. @@ -122,12 +109,12 @@ private static string BuildUrl(string url, ICollection pairs) if (pairs.Count % 2 != 0) throw new ArgumentException("There must be an even number of strings supplied for parameters."); + if (pairs.Count <= 0) + return string.Empty; + var sb = new StringBuilder(url); - if (pairs.Count > 0) - { - sb.Append('?'); - sb.Append(string.Join("&", pairs.Where((s, i) => i % 2 == 0).Zip(pairs.Where((s, i) => i % 2 == 1), Format))); - } + sb.Append('?'); + sb.Append(string.Join("&", pairs.Where((s, i) => i % 2 == 0).Zip(pairs.Where((s, i) => i % 2 == 1), Format))); return sb.ToString(); static string Format(string? a, string? b) @@ -135,4 +122,4 @@ static string Format(string? a, string? b) return string.Concat(WebUtility.UrlEncode(a), "=", WebUtility.UrlEncode(b)); } } -} \ No newline at end of file +} diff --git a/src/ResXManager.Translators/ResXManager.Translators.csproj b/src/ResXManager.Translators/ResXManager.Translators.csproj index ad29e736..4d1e2e56 100644 --- a/src/ResXManager.Translators/ResXManager.Translators.csproj +++ b/src/ResXManager.Translators/ResXManager.Translators.csproj @@ -58,6 +58,7 @@ +