From d1278af724cc3994f35290ada5f735326284434c Mon Sep 17 00:00:00 2001 From: tkefauver Date: Sat, 4 Jan 2025 17:50:51 -0500 Subject: [PATCH 1/5] Improved fix for Google Lite translator --- .../GoogleTranslatorLite.cs | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/ResXManager.Translators/GoogleTranslatorLite.cs b/src/ResXManager.Translators/GoogleTranslatorLite.cs index 16aae63e..365e370f 100644 --- a/src/ResXManager.Translators/GoogleTranslatorLite.cs +++ b/src/ResXManager.Translators/GoogleTranslatorLite.cs @@ -53,16 +53,17 @@ protected override async Task Translate(ITranslationSession translationSession) var parameters = new List(30); // ReSharper disable once PossibleNullReferenceException parameters.AddRange(new[] - { - "client", "dict-chrome-ex", + { + "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", + "https://translate.googleapis.com/translate_a/single", parameters, translationSession.CancellationToken).ConfigureAwait(false); @@ -101,8 +102,37 @@ 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); + var sb = new StringBuilder(); + var str_count = 0; + var array_start_count = 0; + using var reader = new JsonTextReader(new StringReader(result)); + while (reader.Read()) + { + if (reader.Value != null) + { + if (reader.TokenType == JsonToken.String && array_start_count == 3) + { + if (str_count % 2 == 0) + { + sb.Append(reader.Value.ToString()); + } + str_count++; + } + } + else + { + if (reader.TokenType == JsonToken.StartArray) + { + array_start_count++; + } + else if (reader.TokenType == JsonToken.EndArray) + { + array_start_count--; + + } + } + } + return Regex.Unescape(sb.ToString()); } [DataContract] From 1480b7c33dc10e9c51e77513f26f274c8f76dc5f Mon Sep 17 00:00:00 2001 From: tkefauver Date: Sat, 4 Jan 2025 18:02:35 -0500 Subject: [PATCH 2/5] Added missing usings statements --- src/ResXManager.Translators/GoogleTranslatorLite.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ResXManager.Translators/GoogleTranslatorLite.cs b/src/ResXManager.Translators/GoogleTranslatorLite.cs index 365e370f..ae0ea7f3 100644 --- a/src/ResXManager.Translators/GoogleTranslatorLite.cs +++ b/src/ResXManager.Translators/GoogleTranslatorLite.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Composition; using System.Globalization; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -14,6 +15,8 @@ using System.Threading.Tasks; using System.Windows.Controls; +using Newtonsoft.Json; + using ResXManager.Infrastructure; using TomsToolbox.Essentials; From 5c018c6fe3368a018f4e507cf112a1f6e0e8dfc0 Mon Sep 17 00:00:00 2001 From: tkefauver Date: Wed, 8 Jan 2025 19:14:26 +0000 Subject: [PATCH 3/5] Converted Google Lite response handling to use JsonNode. --- .../GoogleTranslatorLite.cs | 49 +++++++------------ .../ResXManager.Translators.csproj | 1 + 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/src/ResXManager.Translators/GoogleTranslatorLite.cs b/src/ResXManager.Translators/GoogleTranslatorLite.cs index ae0ea7f3..9913045f 100644 --- a/src/ResXManager.Translators/GoogleTranslatorLite.cs +++ b/src/ResXManager.Translators/GoogleTranslatorLite.cs @@ -4,19 +4,17 @@ using System.Collections.Generic; using System.Composition; using System.Globalization; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Runtime.Serialization; using System.Text; +using System.Text.Json.Nodes; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; -using Newtonsoft.Json; - using ResXManager.Infrastructure; using TomsToolbox.Essentials; @@ -56,13 +54,13 @@ protected override async Task Translate(ITranslationSession translationSession) var parameters = new List(30); // ReSharper disable once PossibleNullReferenceException parameters.AddRange(new[] - { + { "client", "gtx", "dt", "t", "sl", GoogleLangCode(translationSession.SourceLanguage), "tl", GoogleLangCode(targetCulture), "q", RemoveKeyboardShortcutIndicators(sourceItems[0].Source) - }); + }); // ReSharper disable once AssignNullToNotNullAttribute var response = await GetHttpResponse( @@ -105,35 +103,26 @@ private static async Task GetHttpResponse(string baseUrl, ICollection not available in .NET Framework var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + if (JsonNode.Parse(result) is not { } jn0 || + jn0.AsArray() is not { } jarr0 || + jarr0.FirstOrDefault() is not { } jn1 || + jn1.AsArray() is not { } jarr1) + { + return result; + } var sb = new StringBuilder(); - var str_count = 0; - var array_start_count = 0; - using var reader = new JsonTextReader(new StringReader(result)); - while (reader.Read()) + for (var i = 0; i < jarr1.Count; i++) { - if (reader.Value != null) + if (jarr1.ElementAt(i) is not { } item_i || + item_i.AsArray() is not { } item_i_arr || + !item_i_arr.Any() || + item_i_arr.FirstOrDefault() is not { } item_i_0 || + item_i_0.ToString() is not { } text) { - if (reader.TokenType == JsonToken.String && array_start_count == 3) - { - if (str_count % 2 == 0) - { - sb.Append(reader.Value.ToString()); - } - str_count++; - } - } - else - { - if (reader.TokenType == JsonToken.StartArray) - { - array_start_count++; - } - else if (reader.TokenType == JsonToken.EndArray) - { - array_start_count--; - - } + continue; } + sb.Append(text); } return Regex.Unescape(sb.ToString()); } 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 @@ + From 3adab8c76f4561c877b3e4dc9d0e035988a52357 Mon Sep 17 00:00:00 2001 From: tom-englert Date: Thu, 9 Jan 2025 08:22:08 +0100 Subject: [PATCH 4/5] Fix build and simplify code --- src/Directory.Packages.props | 1 + .../Model/TranslatorTests.cs | 42 +++++++++++++++++++ .../ResXManager.Tests.csproj | 1 + .../GoogleTranslatorLite.cs | 32 ++++++-------- 4 files changed, 56 insertions(+), 20 deletions(-) create mode 100644 src/ResXManager.Tests/Model/TranslatorTests.cs 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 9913045f..7ad6655e 100644 --- a/src/ResXManager.Translators/GoogleTranslatorLite.cs +++ b/src/ResXManager.Translators/GoogleTranslatorLite.cs @@ -104,27 +104,19 @@ private static async Task GetHttpResponse(string baseUrl, ICollection not available in .NET Framework var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - if (JsonNode.Parse(result) is not { } jn0 || - jn0.AsArray() is not { } jarr0 || - jarr0.FirstOrDefault() is not { } jn1 || - jn1.AsArray() is not { } jarr1) - { - return result; - } - var sb = new StringBuilder(); - for (var i = 0; i < jarr1.Count; i++) + return ParseResponse(result); + } + + public static string ParseResponse(string result) + { + var node = JsonNode.Parse(result); + + if ((node is JsonArray level1) && (level1.FirstOrDefault() is JsonArray level2)) { - if (jarr1.ElementAt(i) is not { } item_i || - item_i.AsArray() is not { } item_i_arr || - !item_i_arr.Any() || - item_i_arr.FirstOrDefault() is not { } item_i_0 || - item_i_0.ToString() is not { } text) - { - continue; - } - sb.Append(text); + return string.Concat(level2.OfType().Select(item => item.FirstOrDefault())); } - return Regex.Unescape(sb.ToString()); + + return string.Empty; } [DataContract] @@ -157,4 +149,4 @@ static string Format(string? a, string? b) return string.Concat(WebUtility.UrlEncode(a), "=", WebUtility.UrlEncode(b)); } } -} \ No newline at end of file +} From b93e1e9dfdc6388cd29f5eaa8e6515e3199ad6c3 Mon Sep 17 00:00:00 2001 From: tom-englert Date: Thu, 9 Jan 2025 09:13:02 +0100 Subject: [PATCH 5/5] Cleanup code --- .../GoogleTranslatorLite.cs | 59 +++++-------------- 1 file changed, 16 insertions(+), 43 deletions(-) diff --git a/src/ResXManager.Translators/GoogleTranslatorLite.cs b/src/ResXManager.Translators/GoogleTranslatorLite.cs index 7ad6655e..40256e96 100644 --- a/src/ResXManager.Translators/GoogleTranslatorLite.cs +++ b/src/ResXManager.Translators/GoogleTranslatorLite.cs @@ -7,17 +7,14 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Runtime.Serialization; using System.Text; using System.Text.Json.Nodes; -using System.Text.RegularExpressions; 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))] @@ -26,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)) @@ -44,36 +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[] - { + parameters.AddRange( + [ "client", "gtx", "dt", "t", "sl", GoogleLangCode(translationSession.SourceLanguage), "tl", GoogleLangCode(targetCulture), - "q", RemoveKeyboardShortcutIndicators(sourceItems[0].Source) - }); + "q", RemoveKeyboardShortcutIndicators(sourceItem.Source) + ]); - // ReSharper disable once AssignNullToNotNullAttribute - var response = await GetHttpResponse( - "https://translate.googleapis.com/translate_a/single", - parameters, - translationSession.CancellationToken).ConfigureAwait(false); - - 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); } } } @@ -83,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"; @@ -101,7 +82,6 @@ private static async Task GetHttpResponse(string baseUrl, ICollection not available in .NET Framework var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); return ParseResponse(result); @@ -119,13 +99,6 @@ public static string ParseResponse(string result) return string.Empty; } - [DataContract] - private sealed class Translation - { - [DataMember(Name = "translatedText")] - public string? TranslatedText { get; set; } - } - /// Builds the URL from a base, method name, and name/value paired parameters. All parameters are encoded. /// The base URL. /// The name/value paired parameters. @@ -136,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)