From e6998b91df0f2a257d1770d64a0800d0c71f4d81 Mon Sep 17 00:00:00 2001 From: Kevin Abraham Date: Tue, 18 Aug 2026 09:05:28 -0400 Subject: [PATCH] feat: allow a glossary without a source language in text translation Text translation now accepts a glossary with sourceLanguageCode set to null. DeepL detects the source language and applies the glossary's dictionary for the detected language pair. Previously the client rejected this before sending the request, so callers who could not know their input language had to detect it themselves first. The requirement was enforced in CreateCommonHttpParams, which text translation and document upload share. It moves to TranslateDocumentUploadAsync, next to the existing document-only option validation, so the shared helper stays a request-body builder and document translation continues to require a source language when a glossary is used, matching the API. Also makes TextTranslateOptions.GlossaryIds and DocumentTranslateOptions.GlossaryIds settable, so callers can assign them in an object initializer instead of only appending to the list. Tests: adds coverage for glossary and glossary_ids without a source language, and for document translation still throwing. Updates the two existing tests that asserted the old client-side rejection. --- CHANGELOG.md | 9 +++ DeepL/DocumentTranslateOptions.cs | 2 +- DeepL/TextTranslateOptions.cs | 10 ++- DeepL/Translator.cs | 13 ++-- DeepLTests/GlossaryTest.cs | 8 --- DeepLTests/MultilingualGlossaryTest.cs | 86 +++++++++++++++++++++++--- README.md | 19 +++++- 7 files changed, 117 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7484a17..e7d3394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Text translation can now use a glossary without an explicit source language. + When `sourceLanguageCode` is `null`, DeepL detects the source language and + applies the glossary's dictionary for the detected language pair. Document + translation still requires a source language when a glossary is used. +### Changed +- `TextTranslateOptions.GlossaryIds` and `DocumentTranslateOptions.GlossaryIds` + are now settable, so they can be assigned in an object initializer instead of + only appended to. ## [1.22.0] - 2026-08-11 ### Added diff --git a/DeepL/DocumentTranslateOptions.cs b/DeepL/DocumentTranslateOptions.cs index d05a4ab..f7691ef 100644 --- a/DeepL/DocumentTranslateOptions.cs +++ b/DeepL/DocumentTranslateOptions.cs @@ -74,7 +74,7 @@ public DocumentTranslateOptions(TranslationMemoryInfo translationMemory) : this( /// order, with the first matching term taking precedence. Using this option requires the source language to be /// specified, and it cannot be combined with . /// - public List GlossaryIds { get; } = new List(); + public List GlossaryIds { get; set; } = new List(); /// Specifies the ID of a style rule to use with the translation. public string? StyleId { get; set; } diff --git a/DeepL/TextTranslateOptions.cs b/DeepL/TextTranslateOptions.cs index b5ef9bc..d98181e 100644 --- a/DeepL/TextTranslateOptions.cs +++ b/DeepL/TextTranslateOptions.cs @@ -78,10 +78,14 @@ public TextTranslateOptions(TranslationMemoryInfo translationMemory) : this() { /// /// Specifies the IDs of multiple glossaries to use with the translation (maximum of 5). Glossaries are applied in - /// order, with the first matching term taking precedence. Using this option requires the source language to be - /// specified, and it cannot be combined with . + /// order, with the first matching term taking precedence. Cannot be combined with + /// . /// - public List GlossaryIds { get; } = new List(); + /// + /// If the source language is omitted, DeepL detects it and every listed glossary must contain a + /// dictionary for the detected language pair. + /// + public List GlossaryIds { get; set; } = new List(); /// Specifies the ID of a style rule to use with the translation. public string? StyleId { get; set; } diff --git a/DeepL/Translator.cs b/DeepL/Translator.cs index 78ef7d1..79314a2 100644 --- a/DeepL/Translator.cs +++ b/DeepL/Translator.cs @@ -633,6 +633,11 @@ public async Task TranslateDocumentUploadAsync( string targetLanguageCode, DocumentTranslateOptions? options = null, CancellationToken cancellationToken = default) { + // Document translation cannot detect the source language when a glossary is used. + if (sourceLanguageCode == null && (options?.GlossaryId != null || options?.GlossaryIds.Count > 0)) { + throw new ArgumentException($"{nameof(sourceLanguageCode)} is required if using a glossary"); + } + var bodyParams = CreateCommonHttpParams( sourceLanguageCode, targetLanguageCode, @@ -1050,18 +1055,10 @@ private String ConstructUserAgentString(bool sendPlatformInfo = true, AppInfo? a } if (glossaryId != null) { - if (sourceLanguageCode == null) { - throw new ArgumentException($"{nameof(sourceLanguageCode)} is required if using a glossary"); - } - bodyParams.Add(("glossary_id", glossaryId)); } if (hasGlossaryIds) { - if (sourceLanguageCode == null) { - throw new ArgumentException($"{nameof(sourceLanguageCode)} is required if using a glossary"); - } - if (glossaryIds!.Count > 5) { throw new ArgumentException("GlossaryIds must not contain more than 5 glossary IDs"); } diff --git a/DeepLTests/GlossaryTest.cs b/DeepLTests/GlossaryTest.cs index 4d33902..bcddfa0 100644 --- a/DeepLTests/GlossaryTest.cs +++ b/DeepLTests/GlossaryTest.cs @@ -370,14 +370,6 @@ public async Task TestGlossaryTranslateTextInvalid() { var glossaryDeEn = glossaryCleanupDeEn.Capture( await translator.CreateGlossaryAsync(glossaryNameDeEn, "DE", "EN", _testEntries)); var exception = await Assert.ThrowsAsync( - () => translator.TranslateTextAsync( - "test", - null, - "DE", - new TextTranslateOptions { GlossaryId = glossaryEnDe.GlossaryId })); - Assert.Contains("sourceLanguageCode is required", exception.Message); - - exception = await Assert.ThrowsAsync( () => translator.TranslateTextAsync( "test", "DE", diff --git a/DeepLTests/MultilingualGlossaryTest.cs b/DeepLTests/MultilingualGlossaryTest.cs index 4004f74..892949a 100644 --- a/DeepLTests/MultilingualGlossaryTest.cs +++ b/DeepLTests/MultilingualGlossaryTest.cs @@ -649,14 +649,6 @@ public async Task TestGlossaryTranslateTextInvalid() { var glossary = glossaryCleanup.Capture( await client.CreateMultilingualGlossaryAsync(glossaryName, new[] { glossaryDictEnDe, glossaryDictDeEn })); var exception = await Assert.ThrowsAsync( - () => client.TranslateTextAsync( - "test", - null, - "de", - new TextTranslateOptions { GlossaryId = glossary.GlossaryId })); - Assert.Contains("sourceLanguageCode is required", exception.Message); - - exception = await Assert.ThrowsAsync( () => client.TranslateTextAsync( "test", "de", @@ -668,6 +660,84 @@ public async Task TestGlossaryTranslateTextInvalid() { } } + [Fact] + public async Task TestGlossaryTranslateTextWithoutSourceLang() { + var client = CreateTestClient(); + var glossaryCleanup = new GlossaryCleanupUtility(client, nameof(TestGlossaryTranslateTextWithoutSourceLang)); + var glossaryName = glossaryCleanup.GlossaryName; + try { + var entries = new Dictionary { { "artist", "Maler" }, { "prize", "Gewinn" } }; + var glossaryDict = new MultilingualGlossaryDictionaryEntries("en", "de", new GlossaryEntries(entries)); + var glossary = glossaryCleanup.Capture( + await client.CreateMultilingualGlossaryAsync(glossaryName, new[] { glossaryDict })); + + // No source language: DeepL detects it and resolves the glossary's dictionary from the result. + var result = await client.TranslateTextAsync( + "The artist was awarded a prize.", + null, + "de", + new TextTranslateOptions { GlossaryId = glossary.GlossaryId }); + + if (!IsMockServer) { + Assert.Equal("en", result.DetectedSourceLanguageCode); + Assert.Contains("Maler", result.Text); + Assert.Contains("Gewinn", result.Text); + } + } finally { + await glossaryCleanup.Cleanup(); + } + } + + [Fact] + public async Task TestGlossaryIdsTranslateTextWithoutSourceLang() { + var client = CreateTestClient(); + var glossaryCleanup = new GlossaryCleanupUtility(client, nameof(TestGlossaryIdsTranslateTextWithoutSourceLang)); + var glossaryName = glossaryCleanup.GlossaryName; + try { + var entries = new Dictionary { { "artist", "Maler" } }; + var glossaryDict = new MultilingualGlossaryDictionaryEntries("en", "de", new GlossaryEntries(entries)); + var glossary = glossaryCleanup.Capture( + await client.CreateMultilingualGlossaryAsync(glossaryName, new[] { glossaryDict })); + + var options = new TextTranslateOptions { GlossaryIds = new List { glossary.GlossaryId } }; + var result = await client.TranslateTextAsync("The artist was awarded a prize.", null, "de", options); + + if (!IsMockServer) { + Assert.Equal("en", result.DetectedSourceLanguageCode); + Assert.Contains("Maler", result.Text); + } + } finally { + await glossaryCleanup.Cleanup(); + } + } + + [Fact] + public async Task TestGlossaryTranslateDocumentWithoutSourceLangThrows() { + var client = CreateTestClient(); + var glossaryCleanup = new GlossaryCleanupUtility( + client, + nameof(TestGlossaryTranslateDocumentWithoutSourceLangThrows)); + var glossaryName = glossaryCleanup.GlossaryName; + try { + var glossaryDict = new MultilingualGlossaryDictionaryEntries("en", "de", TestEntries); + var glossary = glossaryCleanup.Capture( + await client.CreateMultilingualGlossaryAsync(glossaryName, new[] { glossaryDict })); + + // Document translation cannot detect the source language when a glossary is used. + using var stream = new MemoryStream(Encoding.UTF8.GetBytes("The artist was awarded a prize.")); + var exception = await Assert.ThrowsAsync( + () => client.TranslateDocumentUploadAsync( + stream, + "test.txt", + null, + "de", + new DocumentTranslateOptions { GlossaryId = glossary.GlossaryId })); + Assert.Contains("sourceLanguageCode is required", exception.Message); + } finally { + await glossaryCleanup.Cleanup(); + } + } + // Utility function for determining if a list of MultilingualGlossaryDictionaryEntries objects (that have entries) matches // a list of MultilingualGlossaryDictionaryInfo (that do not contain entries, but just a count of the number of entries for // that glossary dictionary diff --git a/README.md b/README.md index 0351e1e..0abc219 100644 --- a/README.md +++ b/README.md @@ -560,8 +560,7 @@ Console.WriteLine(updatedGlossary.Name); // 'My new glossary name' You can use a stored glossary for text (or document) translation by setting the `TextTranslationOptions` (or `DocumentTranslationOptions`) `GlossaryId` property -to the glossary ID. You must also specify the `source_lang` argument (it is -required when using a glossary): +to the glossary ID: ```c# var resultWithGlossary = await client.TranslateTextAsync( @@ -573,6 +572,22 @@ var resultWithGlossary = await client.TranslateTextAsync( // Without using a glossary: "Der Künstler wurde mit einem Preis ausgezeichnet." ``` +For text translation you may pass `null` as the source language. DeepL detects +the source language and applies the glossary's dictionary for the detected +language pair. The request fails if the glossary has no dictionary for that +pair, and detection is less reliable on very short text, so pass an explicit +source language when the input may be only a few characters long. Document +translation still requires an explicit source language when a glossary is used. + +```c# +var resultWithDetection = await client.TranslateTextAsync( + "The artist was awarded a prize.", + null, + "DE", + new TextTranslateOptions { GlossaryId = glossaryEnToDe.GlossaryId }); +Console.WriteLine(resultWithDetection.DetectedSourceLanguageCode); // "en" +``` + ### Style Rules Style rules allow you to customize your translations using a managed, shared list