From 1cadd6000a24e53a2ebfed1a1375167f00ae4236 Mon Sep 17 00:00:00 2001 From: Stefan Sorgen Date: Thu, 18 Jun 2026 10:06:41 +0200 Subject: [PATCH 1/3] added DeepL config options for using glossary by id or name and tag handling --- .../DeepL/DeepLTranslationOptions.cs | 6 ++ .../DeepL/DeepLTranslationService.cs | 90 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/text/Squidex.Text/Translations/DeepL/DeepLTranslationOptions.cs b/text/Squidex.Text/Translations/DeepL/DeepLTranslationOptions.cs index eb6f27c..26fe935 100644 --- a/text/Squidex.Text/Translations/DeepL/DeepLTranslationOptions.cs +++ b/text/Squidex.Text/Translations/DeepL/DeepLTranslationOptions.cs @@ -11,6 +11,12 @@ public sealed class DeepLTranslationOptions { public string AuthKey { get; set; } + public string? GlossaryById { get; set; } + + public string? GlossaryByName { get; set; } + + public string? TagHandling { get; set; } + public decimal CostsPerCharacterInEUR { get; set; } = 20m / 1_000_000; public Dictionary Mapping { get; set; } diff --git a/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs b/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs index cd0fc50..fb0a4a0 100644 --- a/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs +++ b/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs @@ -19,6 +19,8 @@ public sealed class DeepLTranslationService(IHttpClientFactory httpClientFactory private const string UrlPaid = "https://api.deepl.com/v2/translate"; private const string UrlFree = "https://api-free.deepl.com/v2/translate"; private readonly DeepLTranslationOptions options = options.Value; + private string? resolvedGlossaryId; + private bool glossaryResolved; private sealed class TranslationsDto { @@ -35,6 +37,36 @@ private sealed class TranslationDto public string DetectedSourceLanguage { get; set; } } + private sealed class GlossariesDto + { + [JsonPropertyName("glossaries")] + public GlossaryDto[] Glossaries { get; set; } + } + + private sealed class GlossaryDto + { + [JsonPropertyName("glossary_id")] + public string GlossaryId { get; set; } // "def3a26b-3e84-45b3-84ae-0c0aaf3525f7" + + [JsonPropertyName("name")] + public string Name { get; set; } // "My Glossary" + + [JsonPropertyName("ready")] + public bool Ready { get; set; } // true + + [JsonPropertyName("source_lang")] + public string SourceLang { get; set; } // "EN" + + [JsonPropertyName("target_lang")] + public string TargetLang { get; set; } // "DE" + + [JsonPropertyName("creation_time")] + public DateTime CreationTime { get; set; } // "2021-08-03T14:16:18.329Z" + + [JsonPropertyName("entry_count")] + public int EntryCount { get; set; } // 1 + } + public bool IsConfigured { get; } = !string.IsNullOrWhiteSpace(options.Value.AuthKey); public async Task> TranslateAsync(IEnumerable texts, string targetLanguage, string? sourceLanguage = null, @@ -80,6 +112,64 @@ public async Task> TranslateAsync(IEnumerable(glossaryJsonString)!; + + foreach (var glossary in glossaryJsonResponse.Glossaries) + { + if (options.GlossaryByName.Equals(glossary.Name, StringComparison.Ordinal)) + { + if (glossary.Ready) + { + glossaryId = glossary.GlossaryId; + resolvedGlossaryId = glossaryId; + } + else + { + glossaryResolved = false; + } + + break; + } + } + } + catch (Exception ex) + { + glossaryResolved = false; + AddError(TranslationResult.Failed(ex)); + } + } + + if (!string.IsNullOrWhiteSpace(glossaryId)) + { + parameters.Add(new KeyValuePair("glossary_id", glossaryId)); + } + + if (!string.IsNullOrWhiteSpace(options.TagHandling)) + { + parameters.Add(new KeyValuePair("tag_handling", options.TagHandling)); + } + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, url) { Content = new FormUrlEncodedContent(parameters!), From 71cc4e7ba617290fdb2880d483b59db62cfc3751 Mon Sep 17 00:00:00 2001 From: Stefan Sorgen Date: Thu, 18 Jun 2026 11:21:59 +0200 Subject: [PATCH 2/3] DeepL: invalidate resolved glossary if lookup by name and changed id --- .../Translations/DeepL/DeepLTranslationService.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs b/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs index fb0a4a0..22326ab 100644 --- a/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs +++ b/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs @@ -198,8 +198,14 @@ public async Task> TranslateAsync(IEnumerable Date: Thu, 18 Jun 2026 12:28:20 +0200 Subject: [PATCH 3/3] DeepL: added dictionary cache for different glossary languages with same name --- .../DeepL/DeepLTranslationService.cs | 89 +++++++++++-------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs b/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs index 22326ab..d78c5ff 100644 --- a/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs +++ b/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Text.Json; @@ -19,7 +20,7 @@ public sealed class DeepLTranslationService(IHttpClientFactory httpClientFactory private const string UrlPaid = "https://api.deepl.com/v2/translate"; private const string UrlFree = "https://api-free.deepl.com/v2/translate"; private readonly DeepLTranslationOptions options = options.Value; - private string? resolvedGlossaryId; + private readonly ConcurrentDictionary glossaryCache = new (); private bool glossaryResolved; private sealed class TranslationsDto @@ -55,10 +56,10 @@ private sealed class GlossaryDto public bool Ready { get; set; } // true [JsonPropertyName("source_lang")] - public string SourceLang { get; set; } // "EN" + public string SourceLang { get; set; } // "en" [JsonPropertyName("target_lang")] - public string TargetLang { get; set; } // "DE" + public string TargetLang { get; set; } // "de" [JsonPropertyName("creation_time")] public DateTime CreationTime { get; set; } // "2021-08-03T14:16:18.329Z" @@ -91,9 +92,12 @@ public async Task> TranslateAsync(IEnumerable> { - new KeyValuePair("target_lang", GetLanguageCode(targetLanguage)), + new KeyValuePair("target_lang", targetCode), }; foreach (var text in textsArray) @@ -101,9 +105,9 @@ public async Task> TranslateAsync(IEnumerable("text", text)); } - if (sourceLanguage != null) + if (sourceCode != null) { - parameters.Add(new KeyValuePair("source_lang", GetLanguageCode(sourceLanguage))); + parameters.Add(new KeyValuePair("source_lang", sourceCode)); } var url = @@ -115,48 +119,46 @@ public async Task> TranslateAsync(IEnumerable(glossaryJsonString)!; + var glossaryUrl = url.Replace("/translate", "/glossaries"); + using var glossaryHttpRequest = new HttpRequestMessage(HttpMethod.Get, glossaryUrl); + using var glossaryHttpResponse = await httpClient.SendAsync(glossaryHttpRequest, ct); - foreach (var glossary in glossaryJsonResponse.Glossaries) + try { - if (options.GlossaryByName.Equals(glossary.Name, StringComparison.Ordinal)) + glossaryHttpResponse.EnsureSuccessStatusCode(); + + var glossaryJsonString = await glossaryHttpResponse.Content.ReadAsStringAsync(ct); + var glossaryJsonResponse = JsonSerializer.Deserialize(glossaryJsonString)!; + + foreach (var glossary in glossaryJsonResponse.Glossaries) { - if (glossary.Ready) - { - glossaryId = glossary.GlossaryId; - resolvedGlossaryId = glossaryId; - } - else + if (options.GlossaryByName.Equals(glossary.Name, StringComparison.Ordinal) && glossary.Ready) { - glossaryResolved = false; + var key = $"{GetLanguageCode(glossary.SourceLang)}_{GetLanguageCode(glossary.TargetLang)}"; + glossaryCache[key] = glossary.GlossaryId; } - - break; } + + glossaryCache.TryGetValue(langPairKey, out glossaryId); + } + catch (Exception ex) + { + glossaryResolved = false; + AddError(TranslationResult.Failed(ex)); } - } - catch (Exception ex) - { - glossaryResolved = false; - AddError(TranslationResult.Failed(ex)); } } @@ -237,10 +239,19 @@ private HttpClient CreateClient() private void InvalidateGlossaryCacheIfResolved(string? glossaryId) { - if (!string.IsNullOrWhiteSpace(glossaryId) && glossaryId == resolvedGlossaryId) + if (string.IsNullOrWhiteSpace(glossaryId)) { - resolvedGlossaryId = null; - glossaryResolved = false; + return; + } + + foreach (var kvp in glossaryCache) + { + if (kvp.Value == glossaryId) + { + glossaryCache.TryRemove(kvp.Key, out _); + glossaryResolved = false; + break; + } } }