diff --git a/packages/nodes-base/credentials/DeepLApi.credentials.ts b/packages/nodes-base/credentials/DeepLApi.credentials.ts new file mode 100644 index 0000000000..fece90546b --- /dev/null +++ b/packages/nodes-base/credentials/DeepLApi.credentials.ts @@ -0,0 +1,15 @@ +import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; + +export class DeepLApi implements ICredentialType { + name = 'deepLApi'; + displayName = 'DeepL API'; + documentationUrl = 'deepL'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/DeepL/DeepL.node.ts b/packages/nodes-base/nodes/DeepL/DeepL.node.ts new file mode 100644 index 0000000000..2d82434f69 --- /dev/null +++ b/packages/nodes-base/nodes/DeepL/DeepL.node.ts @@ -0,0 +1,131 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + deepLApiRequest, +} from './GenericFunctions'; + +import { + textOperations +} from './TextDescription'; + +export class DeepL implements INodeType { + description: INodeTypeDescription = { + displayName: 'DeepL', + name: 'deepL', + icon: 'file:deepl.svg', + group: ['input', 'output'], + version: 1, + description: 'Translate data using DeepL', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + defaults: { + name: 'DeepL', + color: '#0f2b46', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'deepLApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Language', + value: 'language', + }, + ], + default: 'language', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'language', + ], + }, + }, + options: [ + { + name: 'Translate', + value: 'translate', + description: 'Translate data', + }, + ], + default: 'translate', + description: 'The operation to perform', + }, + ...textOperations, + ], + }; + + methods = { + loadOptions: { + async getLanguages(this: ILoadOptionsFunctions) { + const returnData: INodePropertyOptions[] = []; + const languages = await deepLApiRequest.call(this, 'GET', '/languages', {}, { type: 'target' }); + for (const language of languages) { + returnData.push({ + name: language.name, + value: language.language, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const length = items.length; + + const responseData = []; + + for (let i = 0; i < length; i++) { + + const resource = this.getNodeParameter('resource', i) as string; + const operation = this.getNodeParameter('operation', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (resource === 'language') { + + if (operation === 'translate') { + + const text = this.getNodeParameter('text', i) as string; + const translateTo = this.getNodeParameter('translateTo', i) as string; + const qs = { target_lang: translateTo, text } as IDataObject; + + if (additionalFields.sourceLang !== undefined) { + qs.source_lang = ['EN-GB', 'EN-US'].includes(additionalFields.sourceLang as string) + ? 'EN' + : additionalFields.sourceLang; + } + + const response = await deepLApiRequest.call(this, 'GET', '/translate', {}, qs); + responseData.push(response.translations[0]); + } + } + } + + return [this.helpers.returnJsonArray(responseData)]; + } +} diff --git a/packages/nodes-base/nodes/DeepL/GenericFunctions.ts b/packages/nodes-base/nodes/DeepL/GenericFunctions.ts new file mode 100644 index 0000000000..7b8f9a1004 --- /dev/null +++ b/packages/nodes-base/nodes/DeepL/GenericFunctions.ts @@ -0,0 +1,62 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function deepLApiRequest( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: IDataObject = {}, + qs: IDataObject = {}, + uri?: string, + headers: IDataObject = {}, +) { + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://api.deepl.com/v2${resource}`, + json: true, + }; + + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + + if (Object.keys(body).length === 0) { + delete options.body; + } + + const credentials = this.getCredentials('deepLApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + options.qs.auth_key = credentials.apiKey; + + return await this.helpers.request!(options); + + } catch (error) { + if (error?.response?.body?.message) { + // Try to return the error prettier + throw new Error(`DeepL error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} diff --git a/packages/nodes-base/nodes/DeepL/TextDescription.ts b/packages/nodes-base/nodes/DeepL/TextDescription.ts new file mode 100644 index 0000000000..7b2d1d5f1f --- /dev/null +++ b/packages/nodes-base/nodes/DeepL/TextDescription.ts @@ -0,0 +1,125 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const textOperations = [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'Input text to translate.', + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + operation: [ + 'translate', + ], + }, + }, + }, + { + displayName: 'Target Language', + name: 'translateTo', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLanguages', + }, + default: '', + description: 'Language to translate to.', + required: true, + displayOptions: { + show: { + operation: [ + 'translate', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Source Language', + name: 'sourceLang', + type: 'options', + default: '', + description: 'Language to translate from.', + typeOptions: { + loadOptionsMethod: 'getLanguages', + }, + }, + { + displayName: 'Split Sentences', + name: 'splitSentences', + type: 'options', + default: '1', + description: 'How the translation engine should split sentences.', + options: [ + { + name: 'Interpunction Only', + value: 'nonewlines', + description: 'Split text on interpunction only, ignoring newlines.', + }, + { + name: 'No Splitting', + value: '0', + description: 'Treat all text as a single sentence.', + }, + { + name: 'On Punctuation and Newlines', + value: '1', + description: 'Split text on interpunction and newlines.', + }, + ], + }, + { + displayName: 'Preserve Formatting', + name: 'preserveFormatting', + type: 'options', + default: '0', + description: 'Whether the translation engine should respect the original formatting, even if it would usually correct some aspects.', + options: [ + { + name: 'Apply corrections', + value: '0', + description: 'Fix punctuation at the beginning and end of sentences and fixes lower/upper caseing at the beginning.', + }, + { + name: 'Do not correct', + value: '1', + description: 'Keep text as similar as possible to the original.', + }, + ], + }, + { + displayName: 'Formality', + name: 'formality', + type: 'options', + default: 'default', + description: 'How formal or informal the target text should be. May not be supported with all languages.', + options: [ + { + name: 'Formal', + value: 'more', + }, + { + name: 'Informal', + value: 'less', + }, + { + name: 'Neutral', + value: 'default', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/DeepL/deepl.svg b/packages/nodes-base/nodes/DeepL/deepl.svg new file mode 100644 index 0000000000..706dacab12 --- /dev/null +++ b/packages/nodes-base/nodes/DeepL/deepl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 89b4bbc0cb..5e1b5e04db 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -65,6 +65,7 @@ "dist/credentials/CustomerIoApi.credentials.js", "dist/credentials/S3.credentials.js", "dist/credentials/CrateDb.credentials.js", + "dist/credentials/DeepLApi.credentials.js", "dist/credentials/DemioApi.credentials.js", "dist/credentials/DiscourseApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", @@ -319,6 +320,7 @@ "dist/nodes/CustomerIo/CustomerIo.node.js", "dist/nodes/CustomerIo/CustomerIoTrigger.node.js", "dist/nodes/DateTime.node.js", + "dist/nodes/DeepL/DeepL.node.js", "dist/nodes/Demio/Demio.node.js", "dist/nodes/Discord/Discord.node.js", "dist/nodes/Discourse/Discourse.node.js",