Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion DeepL/DocumentTranslateOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <see cref="GlossaryId" />.
/// </summary>
public List<string> GlossaryIds { get; } = new List<string>();
public List<string> GlossaryIds { get; set; } = new List<string>();

/// <summary>Specifies the ID of a style rule to use with the translation.</summary>
public string? StyleId { get; set; }
Expand Down
10 changes: 7 additions & 3 deletions DeepL/TextTranslateOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,14 @@ public TextTranslateOptions(TranslationMemoryInfo translationMemory) : this() {

/// <summary>
/// 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 <see cref="GlossaryId" />.
/// order, with the first matching term taking precedence. Cannot be combined with
/// <see cref="GlossaryId" />.
/// </summary>
public List<string> GlossaryIds { get; } = new List<string>();
/// <remarks>
/// If the source language is omitted, DeepL detects it and every listed glossary must contain a
/// dictionary for the detected language pair.
/// </remarks>
public List<string> GlossaryIds { get; set; } = new List<string>();

/// <summary>Specifies the ID of a style rule to use with the translation.</summary>
public string? StyleId { get; set; }
Expand Down
13 changes: 5 additions & 8 deletions DeepL/Translator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,11 @@ public async Task<DocumentHandle> 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,
Expand Down Expand Up @@ -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");
}
Expand Down
8 changes: 0 additions & 8 deletions DeepLTests/GlossaryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,14 +370,6 @@ public async Task TestGlossaryTranslateTextInvalid() {
var glossaryDeEn = glossaryCleanupDeEn.Capture(
await translator.CreateGlossaryAsync(glossaryNameDeEn, "DE", "EN", _testEntries));
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => translator.TranslateTextAsync(
"test",
null,
"DE",
new TextTranslateOptions { GlossaryId = glossaryEnDe.GlossaryId }));
Assert.Contains("sourceLanguageCode is required", exception.Message);

exception = await Assert.ThrowsAsync<ArgumentException>(
() => translator.TranslateTextAsync(
"test",
"DE",
Expand Down
86 changes: 78 additions & 8 deletions DeepLTests/MultilingualGlossaryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -649,14 +649,6 @@ public async Task TestGlossaryTranslateTextInvalid() {
var glossary = glossaryCleanup.Capture(
await client.CreateMultilingualGlossaryAsync(glossaryName, new[] { glossaryDictEnDe, glossaryDictDeEn }));
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => client.TranslateTextAsync(
"test",
null,
"de",
new TextTranslateOptions { GlossaryId = glossary.GlossaryId }));
Assert.Contains("sourceLanguageCode is required", exception.Message);

exception = await Assert.ThrowsAsync<ArgumentException>(
() => client.TranslateTextAsync(
"test",
"de",
Expand All @@ -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<string, string> { { "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<string, string> { { "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<string> { 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<ArgumentException>(
() => 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
Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down
Loading