UNPKG

deepl-node

Version:

deepl-node is the official DeepL Node.js client library

511 lines (510 loc) 26.5 kB
"use strict"; // 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;