Skip to content

Commit 518de7a

Browse files
hive-botclaude
andcommitted
feat: align translation memory jobs and imports with the SDK interface
Add waitUntilTranslationMemoryJobDone() (deprecating isTranslationMemoryJobComplete()), accept a TranslationMemoryJob in the job methods and WaitOptions next to the positional timeout, add result, status, done() and ok() to TranslationMemoryJob, add the import option types with extraBodyParameters, and accept a Buffer or ReadStream in importTranslationMemoryFromFilepath(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a61c851 commit 518de7a

6 files changed

Lines changed: 316 additions & 73 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88
### Added
9+
- Added `waitUntilTranslationMemoryJobDone()`, which replaces
10+
`isTranslationMemoryJobComplete()`. It accepts the job as an ID or a
11+
`TranslationMemoryJob`, and `WaitOptions` or the timeout in milliseconds, and
12+
rejects with the job's error message if the job fails or expires.
13+
`getTranslationMemoryJob()` also accepts a `TranslationMemoryJob`.
14+
- Added `result`, `status`, `done()` and `ok()` to `TranslationMemoryJob`:
15+
the single entry of `results`, its status, whether the job completed, and
16+
whether it has not failed or expired.
17+
- Added the `CreateTranslationMemoryImportOptions` and
18+
`ImportTranslationMemoryOptions` types, with `extraBodyParameters` merged last
19+
into the import request body.
20+
- `importTranslationMemoryFromFilepath()` accepts a `Buffer` or `ReadStream`, with
21+
the new `fileName` option (or `displayName`) as the file name.
22+
- `isTranslationMemoryJobComplete()` also accepts `WaitOptions`.
923
- The style rule methods `getStyleRule()`, `updateStyleRuleName()`,
1024
`updateStyleRuleConfiguredRules()`, `deleteStyleRule()` and the four custom
1125
instruction methods accept the style rule as an ID or a `StyleRuleInfo`
@@ -185,6 +199,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
185199
limit and links to the DeepL API usage limits instead.
186200

187201
### Deprecated
202+
- `isTranslationMemoryJobComplete()`: use `waitUntilTranslationMemoryJobDone()`
203+
instead.
188204
- The `'downloaded'` member of `TranslationMemoryJobStatus`: the API does not
189205
report it for translation memory jobs.
190206
- `uploadDocument()`, `getDocumentStatus()` and `downloadDocument()` are

‎README.md‎

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -913,10 +913,20 @@ const job = await deeplClient.importTranslationMemoryFromFilepath('/path/to/lega
913913
displayName: 'Legal TM',
914914
timeoutMs: 300000,
915915
});
916-
console.log(`Created translation memory ${job.results[0].translationMemoryId}`);
917-
console.log(`Skipped segments: ${job.results[0].skippedSegmentCount}`);
916+
console.log(`Created translation memory ${job.result?.translationMemoryId}`);
917+
console.log(`Skipped segments: ${job.result?.skippedSegmentCount}`);
918918
```
919919
920+
The input may also be a `Buffer` or `ReadStream`; then pass the `fileName`
921+
option (otherwise `displayName` is used as the file name). The options
922+
(`ImportTranslationMemoryOptions`) are:
923+
924+
- `displayName`: name of the new translation memory; defaults to the file name.
925+
- `contentType`: MIME type of the file; defaults to `'application/xml'`.
926+
- `fileName`: file name sent to the API; defaults to the base name of the path.
927+
- `timeoutMs`: maximum time to wait for the import, in milliseconds.
928+
- `extraBodyParameters`: extra fields added last to the JSON request body.
929+
920930
The three steps are also available separately, for example to upload the file
921931
yourself or to poll for progress. `createTranslationMemoryImport()` returns an
922932
upload URL that the file must be uploaded to, then `getTranslationMemoryJob()`
@@ -929,15 +939,32 @@ const created = await deeplClient.createTranslationMemoryImport('legal.tmx', fil
929939
});
930940
await deeplClient.uploadTranslationMemoryFile(created, fileBuffer);
931941
932-
const job = await deeplClient.isTranslationMemoryJobComplete(created.jobId, 300000);
942+
const job = await deeplClient.waitUntilTranslationMemoryJobDone(created.jobId, {
943+
timeoutMs: 300000,
944+
});
933945
```
934946
947+
`createTranslationMemoryImport()` accepts `CreateTranslationMemoryImportOptions`:
948+
`contentType`, `displayName` and `extraBodyParameters`.
949+
950+
`getTranslationMemoryJob()` and `waitUntilTranslationMemoryJobDone()` accept the
951+
job ID or a `TranslationMemoryJob`. `waitUntilTranslationMemoryJobDone()` takes
952+
`WaitOptions` (`{ timeoutMs }`) or the timeout in milliseconds as a number,
953+
resolves with the job once its status is `'completed'`, and rejects with a
954+
`DeepLError` carrying the job's error message if it fails or expires.
955+
`isTranslationMemoryJobComplete()` is deprecated in its favour.
956+
957+
A `TranslationMemoryJob` has `result` (the single entry of `results`), `status`
958+
(the status of `result`), `done()` (true once the status is `'completed'`) and
959+
`ok()` (false once the status is `'failed'` or `'expired'`, true while the job
960+
is in progress). `status` may also be a status this library does not know.
961+
935962
The job status is `'awaiting_input'` until the file is uploaded, and stays
936963
`'awaiting_input'` for a while afterwards because the API detects the upload
937-
asynchronously. `isTranslationMemoryJobComplete()` polls through that status
938-
like any other non-terminal one. A job whose file is never uploaded does not
939-
finish on its own, so pass a timeout in milliseconds when that is a
940-
possibility.
964+
asynchronously. `waitUntilTranslationMemoryJobDone()` polls through that status
965+
like any other status that is not final, including statuses it does not know.
966+
A job whose file is never uploaded does not finish on its own, so pass a
967+
timeout when that is a possibility.
941968
942969
#### Exporting a translation memory
943970
@@ -957,7 +984,9 @@ indicated by `reusedExisting`.
957984
958985
```javascript
959986
const created = await deeplClient.createTranslationMemoryExport('YOUR_TM_ID');
960-
const job = await deeplClient.isTranslationMemoryJobComplete(created.jobId, 300000);
987+
const job = await deeplClient.waitUntilTranslationMemoryJobDone(created.jobId, {
988+
timeoutMs: 300000,
989+
});
961990
await deeplClient.downloadTranslationMemoryExport(job, '/path/to/exported.tmx');
962991
```
963992

‎src/deeplClient.ts‎

Lines changed: 79 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ import {
3939
TranslationMemoryJob,
4040
TranslationMemorySegments,
4141
TranslationMemorySegmentsOptions,
42+
CreateTranslationMemoryImportOptions,
43+
ImportTranslationMemoryOptions,
44+
WaitOptions,
4245
LanguageResource,
4346
LanguageResourceName,
4447
GetLanguagesForResourceOptions,
@@ -80,6 +83,7 @@ import {
8083
extractStyleProfileId,
8184
extractTranslationMemoryId,
8285
isString,
86+
streamToBuffer,
8387
logInfo,
8488
resolveExtraBodyParameters,
8589
setExtraBodyParameters,
@@ -1289,16 +1293,17 @@ export class DeepLClient extends Translator {
12891293
*
12901294
* @param fileName: Name of the TMX file to import, for example 'legal.tmx'.
12911295
* @param contentLength: Size of the TMX file in bytes.
1292-
* @param options: Optional contentType (defaults to 'application/xml') and displayName for the
1293-
* resulting translation memory (defaults to the file name).
1296+
* @param options: Optional contentType (defaults to 'application/xml'), displayName for the
1297+
* resulting translation memory (defaults to the file name) and extraBodyParameters, see
1298+
* {@link CreateTranslationMemoryImportOptions}.
12941299
* @returns {Promise<TranslationMemoryImport>} The job ID and upload URL.
12951300
*
12961301
* @throws {DeepLError} If any error occurs while communicating with the DeepL API.
12971302
*/
12981303
async createTranslationMemoryImport(
12991304
fileName: string,
13001305
contentLength: number,
1301-
options?: { contentType?: string; displayName?: string },
1306+
options?: CreateTranslationMemoryImportOptions,
13021307
): Promise<TranslationMemoryImport> {
13031308
if (!fileName) {
13041309
throw new ArgumentError('fileName must not be empty');
@@ -1322,7 +1327,7 @@ export class DeepLClient extends Translator {
13221327
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff<string>(
13231328
'POST',
13241329
'/v3/translation_memories/import',
1325-
{ jsonBody },
1330+
{ jsonBody: addExtraBodyParametersToJson(jsonBody, options) },
13261331
);
13271332

13281333
await checkStatusCode(statusCode, content);
@@ -1403,12 +1408,15 @@ export class DeepLClient extends Translator {
14031408
/**
14041409
* Retrieves the status of a translation memory import or export job.
14051410
*
1406-
* @param jobId: ID of the job to query.
1411+
* @param job: ID of the job to query, or a TranslationMemoryJob object.
14071412
* @returns {Promise<TranslationMemoryJob>} The current status of the job.
14081413
*
14091414
* @throws {DeepLError} If any error occurs while communicating with the DeepL API.
14101415
*/
1411-
async getTranslationMemoryJob(jobId: string): Promise<TranslationMemoryJob> {
1416+
async getTranslationMemoryJob(
1417+
job: string | TranslationMemoryJob,
1418+
): Promise<TranslationMemoryJob> {
1419+
const jobId = typeof job === 'string' ? job : job?.jobId;
14121420
if (!jobId) {
14131421
throw new ArgumentError('jobId must not be empty');
14141422
}
@@ -1422,30 +1430,31 @@ export class DeepLClient extends Translator {
14221430
}
14231431

14241432
/**
1425-
* Polls a translation memory job until it finishes, sleeping between requests, and resolves
1426-
* with the final status.
1433+
* Polls a translation memory job every 5 seconds until it completes, and resolves with the
1434+
* completed job.
14271435
*
14281436
* Note that an import job keeps reporting 'awaiting_input' for a while after its file has been
14291437
* uploaded, because the API detects the upload asynchronously. That status, and statuses this
14301438
* library does not know, are polled through. A job whose file is never uploaded does not finish
1431-
* on its own, so pass timeoutMs when that is a possibility.
1439+
* on its own, so set a timeout when that is a possibility.
14321440
*
1433-
* @param jobId: ID of the job to wait for.
1434-
* @param timeoutMs: (Optional) Maximum time to wait in milliseconds before rejecting. Note that
1435-
* this is not accurate to the millisecond, as the job is only polled every 5 seconds.
1436-
* @returns {Promise<TranslationMemoryJob>} The job once it has finished.
1441+
* @param job: ID of the job to wait for, or a TranslationMemoryJob object.
1442+
* @param options: (Optional) WaitOptions with timeoutMs, or the timeout in milliseconds as a
1443+
* number: maximum time to wait before rejecting. Note that this is not accurate to the
1444+
* millisecond, as the job is only polled every 5 seconds.
1445+
* @returns {Promise<TranslationMemoryJob>} The completed job.
14371446
*
1438-
* @throws {DeepLError} If the job fails, the timeout is exceeded, or any error occurs while
1439-
* communicating with the DeepL API.
1447+
* @throws {DeepLError} If the job fails or expires (with the job's error message), the timeout
1448+
* is exceeded, or any error occurs while communicating with the DeepL API.
14401449
*/
1441-
async isTranslationMemoryJobComplete(
1442-
jobId: string,
1443-
timeoutMs?: number,
1450+
async waitUntilTranslationMemoryJobDone(
1451+
job: string | TranslationMemoryJob,
1452+
options?: WaitOptions | number,
14441453
): Promise<TranslationMemoryJob> {
1454+
const timeoutMs = typeof options === 'number' ? options : options?.timeoutMs;
14451455
const deadline = timeoutMs !== undefined ? Date.now() + timeoutMs : undefined;
1446-
let job = await this.getTranslationMemoryJob(jobId);
1447-
// Unknown statuses and a missing result are polled through; the timeout bounds the wait.
1448-
while (!DeepLClient.isJobFinished(job)) {
1456+
let current = await this.getTranslationMemoryJob(job);
1457+
while (!current.done() && current.ok()) {
14491458
let sleepMs = 5000;
14501459
if (deadline !== undefined) {
14511460
const remainingMs = deadline - Date.now();
@@ -1458,13 +1467,25 @@ export class DeepLClient extends Translator {
14581467
}
14591468
await timeout(sleepMs);
14601469
logInfo(`Rechecking translation memory job status after sleeping for ${sleepMs} ms.`);
1461-
job = await this.getTranslationMemoryJob(jobId);
1470+
current = await this.getTranslationMemoryJob(current.jobId);
14621471
}
1463-
const result = job.results[0];
1464-
if (result.status !== 'completed') {
1465-
throw new DeepLError(result.errorMessage || `Job ${result.status}`);
1472+
if (!current.ok()) {
1473+
throw new DeepLError(current.result?.errorMessage || `Job ${current.status}`);
14661474
}
1467-
return job;
1475+
return current;
1476+
}
1477+
1478+
/**
1479+
* Polls a translation memory job until it completes, and resolves with the completed job.
1480+
*
1481+
* @deprecated Use {@link waitUntilTranslationMemoryJobDone} instead. Will be removed in the
1482+
* next major version.
1483+
*/
1484+
async isTranslationMemoryJobComplete(
1485+
job: string | TranslationMemoryJob,
1486+
timeoutMs?: number | WaitOptions,
1487+
): Promise<TranslationMemoryJob> {
1488+
return this.waitUntilTranslationMemoryJobDone(job, timeoutMs);
14681489
}
14691490

14701491
/**
@@ -1476,7 +1497,7 @@ export class DeepLClient extends Translator {
14761497
* @throws {DeepLError} If the job has no download URL, or any error occurs while downloading.
14771498
*/
14781499
async downloadTranslationMemoryExport(
1479-
job: TranslationMemoryJob,
1500+
job: Omit<TranslationMemoryJob, 'done' | 'ok'>,
14801501
outputFile: string | fs.WriteStream,
14811502
): Promise<void> {
14821503
const downloadUrl = job.results[0]?.downloadUrl;
@@ -1563,32 +1584,47 @@ export class DeepLClient extends Translator {
15631584
* Note that the API detects the upload asynchronously, so the job keeps reporting
15641585
* 'awaiting_input' for a while (usually well under a minute) before it completes.
15651586
*
1566-
* @param inputPath: Path of the TMX file to import.
1587+
* @param input: Path of the TMX file to import, or a Buffer or ReadStream with its content.
15671588
* @param options: Optional displayName for the resulting translation memory (defaults to the
1568-
* file name), contentType (defaults to 'application/xml'), and timeoutMs limiting how long to
1569-
* wait for the import to finish.
1589+
* file name), contentType (defaults to 'application/xml'), fileName (required for a Buffer or
1590+
* stream unless displayName is set), extraBodyParameters, and timeoutMs limiting how long to
1591+
* wait for the import to finish, see {@link ImportTranslationMemoryOptions}.
15701592
* @returns {Promise<TranslationMemoryJob>} The completed import job; its result carries the
15711593
* new translation memory ID.
15721594
*
15731595
* @throws {DeepLError} If the import fails, the timeout is exceeded, or any error occurs while
15741596
* communicating with the DeepL API.
15751597
*/
15761598
async importTranslationMemoryFromFilepath(
1577-
inputPath: string,
1578-
options?: { displayName?: string; contentType?: string; timeoutMs?: number },
1599+
input: string | Buffer | fs.ReadStream,
1600+
options?: ImportTranslationMemoryOptions,
15791601
): Promise<TranslationMemoryJob> {
1580-
const fileBuffer = await fs.promises.readFile(inputPath);
1581-
const created = await this.createTranslationMemoryImport(
1582-
path.basename(inputPath),
1583-
fileBuffer.length,
1584-
{ displayName: options?.displayName, contentType: options?.contentType },
1585-
);
1602+
let fileBuffer: Buffer;
1603+
let fileName: string | undefined;
1604+
if (isString(input)) {
1605+
fileBuffer = await fs.promises.readFile(input);
1606+
fileName = options?.fileName ?? path.basename(input);
1607+
} else {
1608+
fileName = options?.fileName ?? options?.displayName;
1609+
if (!fileName) {
1610+
throw new ArgumentError(
1611+
'options.fileName or options.displayName must be specified unless using an input file path',
1612+
);
1613+
}
1614+
fileBuffer = input instanceof fs.ReadStream ? await streamToBuffer(input) : input;
1615+
}
1616+
const created = await this.createTranslationMemoryImport(fileName, fileBuffer.length, {
1617+
displayName: options?.displayName,
1618+
contentType: options?.contentType,
1619+
extraBodyParameters: options?.extraBodyParameters,
1620+
extraRequestParameters: options?.extraRequestParameters,
1621+
});
15861622
await this.uploadTranslationMemoryFile(
15871623
created,
15881624
fileBuffer,
15891625
options?.contentType ?? 'application/xml',
15901626
);
1591-
return this.isTranslationMemoryJobComplete(created.jobId, options?.timeoutMs);
1627+
return this.waitUntilTranslationMemoryJobDone(created.jobId, options);
15921628
}
15931629

15941630
/**
@@ -1597,7 +1633,8 @@ export class DeepLClient extends Translator {
15971633
*
15981634
* @param translationMemory: Translation memory ID, or TranslationMemoryInfo object.
15991635
* @param outputFile: String containing output file path, or a WriteStream to store file data.
1600-
* @param options: Optional timeoutMs limiting how long to wait for the export to finish.
1636+
* @param options: Optional WaitOptions with timeoutMs limiting how long to wait for the export
1637+
* to finish.
16011638
* @returns {Promise<TranslationMemoryJob>} The completed export job.
16021639
*
16031640
* @throws {DeepLError} If the export fails, the timeout is exceeded, or any error occurs while
@@ -1606,23 +1643,14 @@ export class DeepLClient extends Translator {
16061643
async exportTranslationMemoryToFilepath(
16071644
translationMemory: TranslationMemoryId | TranslationMemoryInfo,
16081645
outputFile: string | fs.WriteStream,
1609-
options?: { timeoutMs?: number },
1646+
options?: WaitOptions,
16101647
): Promise<TranslationMemoryJob> {
16111648
const created = await this.createTranslationMemoryExport(translationMemory);
1612-
const job = await this.isTranslationMemoryJobComplete(created.jobId, options?.timeoutMs);
1649+
const job = await this.waitUntilTranslationMemoryJobDone(created.jobId, options);
16131650
await this.downloadTranslationMemoryExport(job, outputFile);
16141651
return job;
16151652
}
16161653

1617-
/**
1618-
* True once the job has completed, failed or expired.
1619-
* @private
1620-
*/
1621-
private static isJobFinished(job: TranslationMemoryJob): boolean {
1622-
const status = job.results[0]?.status;
1623-
return status === 'completed' || status === 'failed' || status === 'expired';
1624-
}
1625-
16261654
/**
16271655
* Creates a new style rule.
16281656
*

‎src/parsing.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,8 @@ export function parseTranslationMemoryJob(json: string): TranslationMemoryJob {
10621062
translationMemoryId: result.translation_memory_id,
10631063
skippedSegmentCount: result.skipped_segment_count,
10641064
}));
1065+
const result = results[0];
1066+
const status = result?.status;
10651067
return {
10661068
jobId: obj.job_id,
10671069
operation: (obj.operation ?? '') as 'import' | 'export',
@@ -1073,6 +1075,10 @@ export function parseTranslationMemoryJob(json: string): TranslationMemoryJob {
10731075
displayName: obj.parameters?.display_name,
10741076
sourceContentType: obj.source_file?.content_type,
10751077
sourceContentLength: obj.source_file?.content_length,
1078+
result,
1079+
status,
1080+
done: () => status === 'completed',
1081+
ok: () => status !== 'failed' && status !== 'expired',
10761082
};
10771083
} catch (error) {
10781084
throw new DeepLError(`Error parsing response JSON: ${error}`);

0 commit comments

Comments
 (0)