Skip to content

Commit 33ca37c

Browse files
hive-botclaude
andcommitted
feat: type DocumentStatus.documentId and TranslationMemoryJob times as required
The API always returns document_id in the document status response and creation_time and updated_time in a translation memory job. A missing or malformed value gives an empty documentId or an invalid Date instead of undefined, never a throw. downloadTranslationMemoryExport() still accepts a job without the timestamps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 431883a commit 33ca37c

6 files changed

Lines changed: 47 additions & 16 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3737
- Added `translateDocumentUpload()`, `translateDocumentGetStatus()` and
3838
`translateDocumentDownload()`, which replace `uploadDocument()`,
3939
`getDocumentStatus()` and `downloadDocument()` and behave the same.
40-
- Added `documentId` to `DocumentStatus`, parsed from the response field
41-
`document_id`; `undefined` when absent or not a string.
40+
- Added `documentId` to `DocumentStatus`, parsed from the required response
41+
field `document_id`.
4242
- Added the `timeoutMs` option to `DocumentTranslateOptions`: the maximum time
4343
`translateDocument()` waits for the translation between upload and download.
4444
When it expires, `translateDocument()` rejects with a `DocumentTranslationError`.
@@ -176,6 +176,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
176176
as a glossary ID.
177177

178178
### Changed
179+
- `TranslationMemoryJob.creationTime` and `updatedTime` are typed as required
180+
`Date`, as the API always returns them.
179181
- `translateText()` accepts the `glossary` and `glossaryIds` options without a
180182
source language; the API detects the source language and uses the glossary's
181183
dictionary for the detected language pair. Document translation with a

‎src/deeplClient.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1528,7 +1528,7 @@ export class DeepLClient extends Translator {
15281528
* never one of its subclasses.
15291529
*/
15301530
async downloadTranslationMemoryExport(
1531-
job: Omit<TranslationMemoryJob, 'done' | 'ok'>,
1531+
job: Omit<TranslationMemoryJob, 'done' | 'ok' | 'creationTime' | 'updatedTime'>,
15321532
outputFile: string | fs.WriteStream,
15331533
): Promise<void> {
15341534
const downloadUrl = job.results[0]?.downloadUrl;

‎src/parsing.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,14 +292,14 @@ class DocumentStatusImpl implements DocumentStatus {
292292
public secondsRemaining?: number;
293293
public billedCharacters?: number;
294294
public errorMessage?: string;
295-
public documentId?: string;
295+
public documentId: string;
296296

297297
constructor(
298298
status: DocumentStatusCode,
299+
documentId: string,
299300
secondsRemaining?: number,
300301
billedCharacters?: number,
301302
errorMessage?: string,
302-
documentId?: string,
303303
) {
304304
this.status = status;
305305
this.secondsRemaining = secondsRemaining;
@@ -542,10 +542,11 @@ export function parseDocumentStatus(json: string): DocumentStatus {
542542
const obj = JSON.parse(json) as DocumentStatusApiResponse;
543543
return new DocumentStatusImpl(
544544
obj.status as DocumentStatusCode,
545+
// Required on the wire; a missing or malformed value gives '', never a throw
546+
parseOptionalString(obj.document_id) ?? '',
545547
obj.seconds_remaining,
546548
obj.billed_characters,
547549
obj.error_message,
548-
parseOptionalString(obj.document_id),
549550
);
550551
} catch (error) {
551552
throw new DeepLError(`Error parsing response JSON: ${error}`);
@@ -1062,8 +1063,9 @@ export function parseTranslationMemoryJob(json: string): TranslationMemoryJob {
10621063
operation: (obj.operation ?? '') as 'import' | 'export',
10631064
product: obj.product ?? 'translation_memory',
10641065
results,
1065-
creationTime: parseOptionalTimestamp(obj.creation_time),
1066-
updatedTime: parseOptionalTimestamp(obj.updated_time),
1066+
// Required on the wire; a missing or malformed value gives an invalid Date, never a throw
1067+
creationTime: new Date(parseOptionalString(obj.creation_time) ?? NaN),
1068+
updatedTime: new Date(parseOptionalString(obj.updated_time) ?? NaN),
10671069
translationMemoryId: obj.parameters?.translation_memory_id,
10681070
displayName: obj.parameters?.display_name,
10691071
sourceContentType: obj.source_file?.content_type,

‎src/types.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -835,9 +835,9 @@ export interface WaitOptions {
835835
*/
836836
export interface DocumentStatus {
837837
/**
838-
* ID of the document, or undefined if the response does not contain it.
838+
* ID of the document.
839839
*/
840-
readonly documentId?: string;
840+
readonly documentId: string;
841841

842842
/**
843843
* One of the status values defined in DocumentStatusCode.
@@ -1309,8 +1309,10 @@ export interface TranslationMemoryJob {
13091309
readonly product: string;
13101310
/** Results of the job; the API returns exactly one. */
13111311
readonly results: TranslationMemoryJobResult[];
1312-
readonly creationTime?: Date;
1313-
readonly updatedTime?: Date;
1312+
/** Time the job was created. */
1313+
readonly creationTime: Date;
1314+
/** Time the job was last updated. */
1315+
readonly updatedTime: Date;
13141316
/** Translation memory an export job reads from. */
13151317
readonly translationMemoryId?: string;
13161318
/** Display name an import job assigns to the new translation memory. */

‎tests/offline/translateDocument.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,14 +222,14 @@ describe('Translator.translateDocumentGetStatus', () => {
222222
]);
223223
});
224224

225-
it('leaves documentId undefined when document_id is not a string', async () => {
225+
it('gives an empty documentId when document_id is not a string', async () => {
226226
nock(serverUrl)
227227
.post('/v2/document/doc-id', keyBody)
228228
.reply(200, { document_id: 42, status: 'done', billed_characters: 11 });
229229

230230
const status = await makeDeeplClient().translateDocumentGetStatus(handle);
231231

232-
expect(status.documentId).toBeUndefined();
232+
expect(status.documentId).toBe('');
233233
expect([status.done(), status.billedCharacters]).toEqual([true, 11]);
234234
});
235235

‎tests/offline/translationMemories.test.ts‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ function jobReply(status: string, extra: Record<string, unknown> = {}) {
2929
job_id: 'job-1',
3030
operation: 'import',
3131
product: 'translation_memory',
32+
creation_time: '2025-01-02T03:04:05Z',
33+
updated_time: '2025-01-03T03:04:05Z',
3234
results: [{ status, ...extra }],
3335
};
3436
}
@@ -314,7 +316,12 @@ describe('DeepLClient.getTranslationMemoryJob', () => {
314316

315317
const job = await makeDeeplClient().getTranslationMemoryJob('job-1');
316318

317-
expect(job).toMatchObject({ jobId: 'job-1', operation: 'import' });
319+
expect(job).toMatchObject({
320+
jobId: 'job-1',
321+
operation: 'import',
322+
creationTime: new Date('2025-01-02T03:04:05Z'),
323+
updatedTime: new Date('2025-01-03T03:04:05Z'),
324+
});
318325
expect(job.results).toEqual([
319326
{
320327
status: 'completed',
@@ -379,6 +386,21 @@ describe('DeepLClient.getTranslationMemoryJob', () => {
379386
]);
380387
});
381388

389+
it('gives an invalid Date for a missing or malformed creation or update time', async () => {
390+
const { updated_time: updatedTime, ...withoutUpdatedTime } = jobReply('processing');
391+
void updatedTime;
392+
nock(serverUrl)
393+
.get('/v3/translation_memories/jobs/job-1')
394+
.reply(200, { ...withoutUpdatedTime, creation_time: 'not a timestamp' });
395+
396+
const job = await makeDeeplClient().getTranslationMemoryJob('job-1');
397+
398+
expect(job.creationTime).toBeInstanceOf(Date);
399+
expect(job.creationTime.getTime()).toBeNaN();
400+
expect(job.updatedTime).toBeInstanceOf(Date);
401+
expect(job.updatedTime.getTime()).toBeNaN();
402+
});
403+
382404
it('throws ArgumentError when the job ID is empty', async () => {
383405
await expect(makeDeeplClient().getTranslationMemoryJob('')).rejects.toThrow(
384406
deepl.ArgumentError,
@@ -472,7 +494,10 @@ describe('DeepLClient.createTranslationMemoryExport', () => {
472494
});
473495

474496
describe('DeepLClient.downloadTranslationMemoryExport', () => {
475-
const job = (downloadUrl?: string): Omit<deepl.TranslationMemoryJob, 'done' | 'ok'> => ({
497+
// A hand-built job without done(), ok() or the timestamps, as callers may pass one
498+
const job = (
499+
downloadUrl?: string,
500+
): Omit<deepl.TranslationMemoryJob, 'done' | 'ok' | 'creationTime' | 'updatedTime'> => ({
476501
jobId: 'job-2',
477502
operation: 'export',
478503
product: 'translation_memory',

0 commit comments

Comments
 (0)