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