deepl-node
Version:
deepl-node is the official DeepL Node.js client library
511 lines (510 loc) • 26.5 kB
JavaScript
// Copyright 2025 DeepL SE (https://www.deepl.com)
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Translator = exports.checkStatusCode = void 0;
const client_1 = require("./client");
const errors_1 = require("./errors");
const glossaryEntries_1 = require("./glossaryEntries");
const parsing_1 = require("./parsing");
const utils_1 = require("./utils");
const fs = __importStar(require("fs"));
const http_1 = require("http");
const path_1 = __importDefault(require("path"));
const os = __importStar(require("os"));
const url_1 = require("url");
const util = __importStar(require("util"));
const documentMinifier_1 = require("./documentMinifier");
/**
* Checks the HTTP status code, and in case of failure, throws an exception with diagnostic information.
* @private
*/
async function checkStatusCode(statusCode, content, usingGlossary = false, inDocumentDownload = false) {
if (200 <= statusCode && statusCode < 400)
return;
if (content instanceof http_1.IncomingMessage) {
try {
content = await (0, utils_1.streamToString)(content);
}
catch (e) {
content = `Error occurred while reading response: ${e}`;
}
}
let message = '';
try {
const jsonObj = JSON.parse(content);
if (jsonObj.message !== undefined) {
message += `, message: ${jsonObj.message}`;
}
if (jsonObj.detail !== undefined) {
message += `, detail: ${jsonObj.detail}`;
}
}
catch (error) {
// JSON parsing errors are ignored, and we fall back to the raw content
message = ', ' + content;
}
switch (statusCode) {
case 403:
throw new errors_1.AuthorizationError(`Authorization failure, check auth_key${message}`);
case 456:
throw new errors_1.QuotaExceededError(`Quota for this billing period has been exceeded${message}`);
case 404:
if (usingGlossary)
throw new errors_1.GlossaryNotFoundError(`Glossary not found${message}`);
throw new errors_1.DeepLError(`Not found, check server_url${message}`);
case 400:
throw new errors_1.DeepLError(`Bad request${message}`);
case 429:
throw new errors_1.TooManyRequestsError(`Too many requests, DeepL servers are currently experiencing high load${message}`);
case 503:
if (inDocumentDownload) {
throw new errors_1.DocumentNotReadyError(`Document not ready${message}`);
}
else {
throw new errors_1.DeepLError(`Service unavailable${message}`);
}
default: {
const statusName = http_1.STATUS_CODES[statusCode] || 'Unknown';
throw new errors_1.DeepLError(`Unexpected status code: ${statusCode} ${statusName}${message}, content: ${content}`);
}
}
}
exports.checkStatusCode = checkStatusCode;
/**
* Wrapper for the DeepL API for language translation.
* Create an instance of Translator to use the DeepL API.
*/
class Translator {
/**
* Construct a Translator object wrapping the DeepL API using your authentication key.
* This does not connect to the API, and returns immediately.
* @param authKey Authentication key as specified in your account.
* @param options Additional options controlling Translator behavior.
*/
constructor(authKey, options) {
var _a;
if (!(0, utils_1.isString)(authKey) || authKey.length === 0) {
throw new errors_1.DeepLError('authKey must be a non-empty string');
}
let serverUrl;
if ((options === null || options === void 0 ? void 0 : options.serverUrl) !== undefined) {
serverUrl = options.serverUrl;
}
else if ((0, utils_1.isFreeAccountAuthKey)(authKey)) {
serverUrl = 'https://api-free.deepl.com';
}
else {
serverUrl = 'https://api.deepl.com';
}
const headers = {
Authorization: `DeepL-Auth-Key ${authKey}`,
'User-Agent': this.constructUserAgentString((options === null || options === void 0 ? void 0 : options.sendPlatformInfo) === false ? false : true, options === null || options === void 0 ? void 0 : options.appInfo),
...((_a = options === null || options === void 0 ? void 0 : options.headers) !== null && _a !== void 0 ? _a : {}),
};
const maxRetries = (options === null || options === void 0 ? void 0 : options.maxRetries) !== undefined ? options.maxRetries : 5;
const minTimeout = (options === null || options === void 0 ? void 0 : options.minTimeout) !== undefined ? options.minTimeout : 5000;
this.httpClient = new client_1.HttpClient(serverUrl, headers, maxRetries, minTimeout, options === null || options === void 0 ? void 0 : options.proxy);
}
/**
* Queries character and document usage during the current billing period.
* @return Fulfills with Usage object on success.
*/
async getUsage() {
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('GET', '/v2/usage');
await checkStatusCode(statusCode, content);
return (0, parsing_1.parseUsage)(content);
}
/**
* Queries source languages supported by DeepL API.
* @return Fulfills with array of Language objects containing available source languages.
*/
async getSourceLanguages() {
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('GET', '/v2/languages');
await checkStatusCode(statusCode, content);
return (0, parsing_1.parseLanguageArray)(content);
}
/**
* Queries target languages supported by DeepL API.
* @return Fulfills with array of Language objects containing available target languages.
*/
async getTargetLanguages() {
const data = new url_1.URLSearchParams({ type: 'target' });
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('GET', '/v2/languages', {
data,
});
await checkStatusCode(statusCode, content);
return (0, parsing_1.parseLanguageArray)(content);
}
/**
* Queries language pairs supported for glossaries by DeepL API.
* @return Fulfills with an array of GlossaryLanguagePair objects containing languages supported for glossaries.
*/
async getGlossaryLanguagePairs() {
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('GET', '/v2/glossary-language-pairs');
await checkStatusCode(statusCode, content);
return (0, parsing_1.parseGlossaryLanguagePairArray)(content);
}
/**
* Translates specified text string or array of text strings into the target language.
* @param texts Text string or array of strings containing input text(s) to translate.
* @param sourceLang Language code of input text language, or null to use auto-detection.
* @param targetLang Language code of language to translate into.
* @param options Optional TranslateTextOptions object containing additional options controlling translation.
* @return Fulfills with a TextResult object or an array of TextResult objects corresponding to input texts; use the `TextResult.text` property to access the translated text.
*/
async translateText(texts, sourceLang, targetLang, options) {
var _a;
const data = (0, utils_1.buildURLSearchParams)(sourceLang, targetLang, options === null || options === void 0 ? void 0 : options.formality, options === null || options === void 0 ? void 0 : options.glossary, options === null || options === void 0 ? void 0 : options.extraRequestParameters);
// Always send show_billed_characters=1, remove when the API default is changed to true
data.append('show_billed_characters', '1');
const singular = (0, utils_1.appendTextsAndReturnIsSingular)(data, texts);
(0, utils_1.validateAndAppendTextOptions)(data, options);
const translatePath = (_a = options === null || options === void 0 ? void 0 : options.__path) !== null && _a !== void 0 ? _a : '/v2/translate';
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('POST', translatePath, { data });
await checkStatusCode(statusCode, content);
const textResults = (0, parsing_1.parseTextResultArray)(content);
return (singular ? textResults[0] : textResults);
}
/**
* Uploads specified document to DeepL to translate into given target language, waits for
* translation to complete, then downloads translated document to specified output path.
* @param inputFile String containing file path of document to be translated, or a Stream,
* Buffer, or FileHandle containing file data. Note: unless file path is used, then
* `options.filename` must be specified.
* @param outputFile String containing file path to create translated document, or Stream or
* FileHandle to write translated document content.
* @param sourceLang Language code of input document, or null to use auto-detection.
* @param targetLang Language code of language to translate into.
* @param options Optional DocumentTranslateOptions object containing additional options controlling translation.
* @return Fulfills with a DocumentStatus object for the completed translation. You can use the
* billedCharacters property to check how many characters were billed for the document.
* @throws {Error} If no file exists at the input file path, or a file already exists at the output file path.
* @throws {DocumentTranslationError} If any error occurs during document upload, translation or
* download. The `documentHandle` property of the error may be used to recover the document.
*/
async translateDocument(inputFile, outputFile, sourceLang, targetLang, options) {
var _a;
// Helper function to open output file if provided as filepath and remove it on error
async function getOutputHandleAndOnError() {
if ((0, utils_1.isString)(outputFile)) {
// Open output file path, fail if file already exists
const outputHandle = await fs.promises.open(outputFile, 'wx');
// Set up error handler to remove created file
const onError = async () => {
try {
// remove created output file
await outputHandle.close();
await util.promisify(fs.unlink)(outputFile);
}
catch {
// Ignore errors
}
};
return { outputHandle, onError };
}
return { outputHandle: outputFile };
}
const { outputHandle, onError } = await getOutputHandleAndOnError();
const documentMinifier = new documentMinifier_1.DocumentMinifier();
const willMinify = ((_a = options === null || options === void 0 ? void 0 : options.enableDocumentMinification) !== null && _a !== void 0 ? _a : false) &&
(0, utils_1.isString)(inputFile) &&
documentMinifier_1.DocumentMinifier.canMinifyFile(inputFile) &&
(0, utils_1.isString)(outputFile);
const fileToUpload = willMinify
? documentMinifier.minifyDocument(inputFile, true)
: inputFile;
let documentHandle = undefined;
try {
documentHandle = await this.uploadDocument(fileToUpload, sourceLang, targetLang, options);
const { status } = await this.isDocumentTranslationComplete(documentHandle);
await this.downloadDocument(documentHandle, outputHandle);
if (willMinify) {
documentMinifier.deminifyDocument(inputFile, outputFile, true);
}
return status;
}
catch (errorUnknown) {
if (onError)
await onError();
const error = errorUnknown instanceof Error ? errorUnknown : undefined;
const message = 'Error occurred while translating document: ' +
((error === null || error === void 0 ? void 0 : error.message) ? error === null || error === void 0 ? void 0 : error.message : errorUnknown);
throw new errors_1.DocumentTranslationError(message, documentHandle, error);
}
}
/**
* Uploads specified document to DeepL to translate into target language, and returns handle associated with the document.
* @param inputFile String containing file path, stream containing file data, or FileHandle.
* Note: if a Buffer, Stream, or FileHandle is used, then `options.filename` must be specified.
* @param sourceLang Language code of input document, or null to use auto-detection.
* @param targetLang Language code of language to translate into.
* @param options Optional DocumentTranslateOptions object containing additional options controlling translation.
* @return Fulfills with DocumentHandle associated with the in-progress translation.
*/
async uploadDocument(inputFile, sourceLang, targetLang, options) {
if ((0, utils_1.isString)(inputFile)) {
const buffer = await fs.promises.readFile(inputFile);
return this.internalUploadDocument(buffer, sourceLang, targetLang, path_1.default.basename(inputFile), options);
}
else {
if ((options === null || options === void 0 ? void 0 : options.filename) === undefined) {
throw new errors_1.DeepLError('options.filename must be specified unless using input file path');
}
if (inputFile instanceof fs.ReadStream) {
const buffer = await (0, utils_1.streamToBuffer)(inputFile);
return this.internalUploadDocument(buffer, sourceLang, targetLang, options.filename, options);
}
else if (inputFile instanceof Buffer) {
return this.internalUploadDocument(inputFile, sourceLang, targetLang, options.filename, options);
}
else {
// FileHandle
const buffer = await inputFile.readFile();
const handle = await this.internalUploadDocument(buffer, sourceLang, targetLang, options.filename, options);
await inputFile.close();
return handle;
}
}
}
/**
* Retrieves the status of the document translation associated with the given document handle.
* @param handle Document handle associated with document.
* @return Fulfills with a DocumentStatus giving the document translation status.
*/
async getDocumentStatus(handle) {
const data = new url_1.URLSearchParams({ document_key: handle.documentKey });
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('POST', `/v2/document/${handle.documentId}`, { data });
await checkStatusCode(statusCode, content, false, true);
return (0, parsing_1.parseDocumentStatus)(content);
}
/**
* Downloads the translated document associated with the given document handle to the specified output file path or stream.handle.
* @param handle Document handle associated with document.
* @param outputFile String containing output file path, or Stream or FileHandle to store file data.
* @return Fulfills with undefined, or rejects if the document translation has not been completed.
*/
async downloadDocument(handle, outputFile) {
if ((0, utils_1.isString)(outputFile)) {
const fileStream = await fs.createWriteStream(outputFile, { flags: 'wx' });
try {
await this.internalDownloadDocument(handle, fileStream);
}
catch (e) {
await new Promise((resolve) => fileStream.close(resolve));
await fs.promises.unlink(outputFile);
throw e;
}
}
else if (outputFile instanceof fs.WriteStream) {
return this.internalDownloadDocument(handle, outputFile);
}
else {
// FileHandle
const dummyFilePath = '';
const outputStream = fs.createWriteStream(dummyFilePath, { fd: outputFile.fd });
await this.internalDownloadDocument(handle, outputStream);
try {
await outputFile.close();
}
catch {
// Ignore errors
}
}
}
/**
* Returns a promise that resolves when the given document translation completes, or rejects if
* there was an error communicating with the DeepL API or the document translation failed.
* @param handle {DocumentHandle} Handle to the document translation.
* @return Fulfills with input DocumentHandle and DocumentStatus when the document translation
* completes successfully, rejects if translation fails or a communication error occurs.
*/
async isDocumentTranslationComplete(handle) {
let status = await this.getDocumentStatus(handle);
while (!status.done() && status.ok()) {
// status.secondsRemaining is currently unreliable, so just poll equidistantly
const secs = 5.0;
await (0, utils_1.timeout)(secs * 1000);
(0, utils_1.logInfo)(`Rechecking document translation status after sleeping for ${secs} seconds.`);
status = await this.getDocumentStatus(handle);
}
if (!status.ok()) {
const message = status.errorMessage || 'unknown error';
throw new errors_1.DeepLError(message);
}
return { handle, status };
}
/**
* Creates a new glossary on the DeepL server with given name, languages, and entries.
* @param name User-defined name to assign to the glossary.
* @param sourceLang Language code of the glossary source terms.
* @param targetLang Language code of the glossary target terms.
* @param entries The source- & target-term pairs to add to the glossary.
* @return Fulfills with a GlossaryInfo containing details about the created glossary.
*/
async createGlossary(name, sourceLang, targetLang, entries) {
if (Object.keys(entries.entries()).length === 0) {
throw new errors_1.DeepLError('glossary entries must not be empty');
}
const tsv = entries.toTsv();
return this.internalCreateGlossary(name, sourceLang, targetLang, 'tsv', tsv);
}
/**
* Creates a new glossary on DeepL server with given name, languages, and CSV data.
* @param name User-defined name to assign to the glossary.
* @param sourceLang Language code of the glossary source terms.
* @param targetLang Language code of the glossary target terms.
* @param csvFile String containing the path of the CSV file to be translated, or a Stream,
* Buffer, or a FileHandle containing CSV file content.
* @return Fulfills with a GlossaryInfo containing details about the created glossary.
*/
async createGlossaryWithCsv(name, sourceLang, targetLang, csvFile) {
let csvContent;
if ((0, utils_1.isString)(csvFile)) {
csvContent = (await fs.promises.readFile(csvFile)).toString();
}
else if (csvFile instanceof fs.ReadStream) {
csvContent = (await (0, utils_1.streamToBuffer)(csvFile)).toString();
}
else if (csvFile instanceof Buffer) {
csvContent = csvFile.toString();
}
else {
// FileHandle
csvContent = (await csvFile.readFile()).toString();
await csvFile.close();
}
return this.internalCreateGlossary(name, sourceLang, targetLang, 'csv', csvContent);
}
/**
* Gets information about an existing glossary.
* @param glossaryId Glossary ID of the glossary.
* @return Fulfills with a GlossaryInfo containing details about the glossary.
*/
async getGlossary(glossaryId) {
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('GET', `/v2/glossaries/${glossaryId}`);
await checkStatusCode(statusCode, content, true);
return (0, parsing_1.parseGlossaryInfo)(content);
}
/**
* Gets information about all existing glossaries.
* @return Fulfills with an array of GlossaryInfos containing details about all existing glossaries.
*/
async listGlossaries() {
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('GET', '/v2/glossaries');
await checkStatusCode(statusCode, content, true);
return (0, parsing_1.parseGlossaryInfoList)(content);
}
/**
* Retrieves the entries stored with the glossary with the given glossary ID or GlossaryInfo.
* @param glossary Glossary ID or GlossaryInfo of glossary to retrieve entries of.
* @return Fulfills with GlossaryEntries holding the glossary entries.
*/
async getGlossaryEntries(glossary) {
glossary = (0, utils_1.isString)(glossary) ? glossary : glossary.glossaryId;
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('GET', `/v2/glossaries/${glossary}/entries`);
await checkStatusCode(statusCode, content, true);
return new glossaryEntries_1.GlossaryEntries({ tsv: content });
}
/**
* Deletes the glossary with the given glossary ID or GlossaryInfo.
* @param glossary Glossary ID or GlossaryInfo of glossary to be deleted.
* @return Fulfills with undefined when the glossary is deleted.
*/
async deleteGlossary(glossary) {
glossary = (0, utils_1.isString)(glossary) ? glossary : glossary.glossaryId;
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('DELETE', `/v2/glossaries/${glossary}`);
await checkStatusCode(statusCode, content, true);
}
/**
* Upload given stream for document translation and returns document handle.
* @private
*/
async internalUploadDocument(fileBuffer, sourceLang, targetLang, filename, options) {
const data = (0, utils_1.buildURLSearchParams)(sourceLang, targetLang, options === null || options === void 0 ? void 0 : options.formality, options === null || options === void 0 ? void 0 : options.glossary, options === null || options === void 0 ? void 0 : options.extraRequestParameters);
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('POST', '/v2/document', {
data,
fileBuffer,
filename,
});
await checkStatusCode(statusCode, content);
return (0, parsing_1.parseDocumentHandle)(content);
}
/**
* Download translated document associated with specified handle to given stream.
* @private
*/
async internalDownloadDocument(handle, outputStream) {
const data = new url_1.URLSearchParams({ document_key: handle.documentKey });
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('POST', `/v2/document/${handle.documentId}/result`, { data }, true);
await checkStatusCode(statusCode, content, false, true);
content.pipe(outputStream);
return new Promise((resolve, reject) => {
outputStream.on('finish', resolve);
outputStream.on('error', reject);
});
}
/**
* Create glossary with given details.
* @private
*/
async internalCreateGlossary(name, sourceLang, targetLang, entriesFormat, entries) {
// Glossaries are only supported for base language types
sourceLang = (0, utils_1.nonRegionalLanguageCode)(sourceLang);
targetLang = (0, utils_1.nonRegionalLanguageCode)(targetLang);
if (!(0, utils_1.isString)(name) || name.length === 0) {
throw new errors_1.DeepLError('glossary name must be a non-empty string');
}
const data = new url_1.URLSearchParams({
name: name,
source_lang: sourceLang,
target_lang: targetLang,
entries_format: entriesFormat,
entries: entries,
});
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff('POST', '/v2/glossaries', { data });
await checkStatusCode(statusCode, content, true);
return (0, parsing_1.parseGlossaryInfo)(content);
}
constructUserAgentString(sendPlatformInfo, appInfo) {
let libraryInfoString = 'deepl-node/1.17.3';
if (sendPlatformInfo) {
const systemType = os.type();
const systemVersion = os.version();
const nodeVersion = process.version.substring(1); // Drop the v in the version number
libraryInfoString += ` (${systemType} ${systemVersion}) node/${nodeVersion}`;
}
if (appInfo) {
libraryInfoString += ` ${appInfo.appName}/${appInfo.appVersion}`;
}
return libraryInfoString;
}
}
exports.Translator = Translator;
;