Skip to content

Commit 35f9327

Browse files
hive-botclaude
andcommitted
feat: allow a glossary without a source language in text translation
translateText() no longer rejects the glossary and glossaryIds options when sourceLang is null; the API detects the source language and uses the glossary's dictionary for the detected language pair. Document translation with a glossary still requires sourceLang and keeps the client-side check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e36e14b commit 35f9327

7 files changed

Lines changed: 46 additions & 17 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
172172
as a glossary ID.
173173

174174
### Changed
175+
- `translateText()` accepts the `glossary` and `glossaryIds` options without a
176+
source language; the API detects the source language and uses the glossary's
177+
dictionary for the detected language pair. Document translation with a
178+
glossary still requires a source language.
175179
- `Formality`, `SentenceSplittingMode`, `TagHandlingMode`, `TagHandlingVersion`
176180
and `ModelType` accept any string in addition to their listed values, so values
177181
the API adds later can be passed without a type cast.

‎README.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ console.log(await deeplClient.translateText('How are you?', null, 'de', { formal
193193
`getMultilingualGlossary()`/`getGlossary()`.
194194
- `glossaryIds`: specifies up to 5 glossaries to use with translation, as an array of
195195
glossary ID strings, `MultilingualGlossaryInfo`, or `GlossaryInfo` objects. Glossaries are
196-
applied in the order given, with the first matching term taking precedence. Requires
197-
`sourceLang` to be set, and cannot be combined with the singular `glossary` option.
196+
applied in the order given, with the first matching term taking precedence. Cannot be
197+
combined with the singular `glossary` option.
198198
- `styleRule`: specifies a style rule to use with translation, either as a string
199199
containing the style rule ID, or a `StyleRuleInfo` as returned by `getAllStyleRules()`.
200200
- `styleProfile`: specifies a style profile to use with translation, either as a
@@ -326,7 +326,9 @@ statuses this library does not know.
326326

327327
- `formality`: same as in [Text translation options](#text-translation-options).
328328
- `glossary`: same as in [Text translation options](#text-translation-options).
329+
Requires `sourceLang` to be set.
329330
- `glossaryIds`: same as in [Text translation options](#text-translation-options).
331+
Requires `sourceLang` to be set.
330332
- `styleRule`: same as in [Text translation options](#text-translation-options).
331333
- `styleProfile`: same as in [Text translation options](#text-translation-options).
332334
A style profile that does not exist fails the translation with a
@@ -485,7 +487,11 @@ for (const glossaryDict of glossary.dictionaries) {
485487

486488
To use a glossary when translating text and documents, include the ID
487489
(or `MultilingualGlossaryInfo` object returned by `listMultilingualGlossaries()` or `createMultilingualGlossary()`) in
488-
the function call. The source and target languages must match the glossary.
490+
the function call. The glossary needs a dictionary for the source and target
491+
languages. For text translation, `sourceLang` may be `null`: DeepL detects the
492+
source language and uses the dictionary for the detected language pair, and the
493+
request fails if the glossary has none. Document translation with a glossary
494+
requires `sourceLang`.
489495

490496
```javascript
491497
const resultWithGlossary = await deeplClient.translateText(

‎src/translator.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,14 @@ export class Translator {
717717
options?.glossary,
718718
options?.glossaryIds,
719719
);
720+
// Text translation detects the source language for a glossary; document translation
721+
// with a glossary requires sourceLang.
722+
const hasGlossary =
723+
options?.glossary !== undefined ||
724+
(options?.glossaryIds !== undefined && options.glossaryIds.length > 0);
725+
if (hasGlossary && sourceLang === null) {
726+
throw new DeepLError('sourceLang is required if using a glossary');
727+
}
720728
appendStyleAndTranslationMemoryOptions(data, options ?? {});
721729
if (options?.outputFormat !== undefined) {
722730
data.append('output_format', options.outputFormat);

‎src/types.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,9 @@ export interface TranslateTextOptions extends BaseRequestOptions {
204204
* precedence. Accepts glossary ID strings, GlossaryInfo, or
205205
* MultilingualGlossaryInfo objects (the `.glossaryId` is extracted).
206206
*
207-
* Requires `sourceLang` to be set. Cannot be combined with the singular
208-
* `glossary` option.
207+
* Without `sourceLang`, DeepL detects the source language and uses the
208+
* glossary's dictionary for the detected language pair. Cannot be combined
209+
* with the singular `glossary` option.
209210
*/
210211
glossaryIds?: Array<GlossaryId | GlossaryInfo | MultilingualGlossaryInfo>;
211212

‎src/utils.ts‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,8 @@ export function buildURLSearchParams(
191191
sourceLang = standardizeLanguageCode(sourceLang);
192192
}
193193

194-
// Treat an empty glossaryIds array as "no glossaries" so it doesn't enforce
195-
// the sourceLang requirement or append an empty glossary_ids parameter.
194+
// Treat an empty glossaryIds array as "no glossaries" so it doesn't append an
195+
// empty glossary_ids parameter.
196196
const hasGlossaryIds = glossaryIds !== undefined && glossaryIds.length > 0;
197197

198198
if (glossary !== undefined && hasGlossaryIds) {
@@ -201,10 +201,6 @@ export function buildURLSearchParams(
201201
);
202202
}
203203

204-
if ((glossary !== undefined || hasGlossaryIds) && sourceLang === null) {
205-
throw new DeepLError('sourceLang is required if using a glossary');
206-
}
207-
208204
if (glossaryIds !== undefined && glossaryIds.length > 5) {
209205
throw new DeepLError('glossaryIds option supports at most 5 glossaries.');
210206
}

‎tests/offline/translateDocument.test.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,19 @@ describe('Translator.uploadDocument', () => {
6767
expect(result).toEqual(handle);
6868
});
6969

70+
it.each([
71+
['glossary', { glossary: 'g-1' }],
72+
['glossaryIds', { glossaryIds: ['g-1'] }],
73+
])('throws DeepLError when %s is set without a source language', async (_, options) => {
74+
// No request is declared: nock rejects any request with a different error.
75+
await expect(
76+
makeTranslator().uploadDocument(Buffer.from('x'), null, 'de', {
77+
filename: 'example.txt',
78+
...options,
79+
}),
80+
).rejects.toThrow(new deepl.DeepLError('sourceLang is required if using a glossary'));
81+
});
82+
7083
it('sends extraBodyParameters as multipart parts', async () => {
7184
const upload = expectUpload();
7285

‎tests/offline/translateText.test.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,12 +383,13 @@ describe('Translator.translateText', () => {
383383
});
384384

385385
it.each([
386-
['glossary', { glossary: 'g-1' }],
387-
['glossaryIds', { glossaryIds: ['g-1'] }],
388-
])('throws DeepLError when %s is set without a source language', async (_, options) => {
389-
await expect(makeTranslator().translateText('abc', null, 'de', options)).rejects.toThrow(
390-
/sourceLang is required/,
391-
);
386+
['glossary', { glossary: 'g-1' }, { glossary_id: 'g-1' }],
387+
['glossaryIds', { glossaryIds: ['g-1'] }, { glossary_ids: 'g-1' }],
388+
])('sends %s without a source language', async (_, options, glossaryParam) => {
389+
// nock matches the exact body, so a source_lang field would fail the match.
390+
expectTranslateBody({ ...baseBody, ...glossaryParam });
391+
392+
await makeTranslator().translateText('abc', null, 'de', options);
392393
});
393394

394395
it('throws DeepLError when translationMemoryThreshold is set without a translation memory', async () => {

0 commit comments

Comments
 (0)