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