/** * @author Vigneshwaran Odayappan */ import type { IMessage, MessageAttachment, IProviderMetadata, ITranslationResult, ISupportedLanguage } from '@rocket.chat/core-typings'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import _ from 'underscore'; import { TranslationProviderRegistry, AutoTranslate } from './autotranslate'; import { settings } from '../../settings'; import { i18n } from '../i18n'; import { SystemLogger } from '../logger/system'; const proApiEndpoint = 'https://api.deepl.com/v2/translate'; const freeApiEndpoint = 'https://api-free.deepl.com/v2/translate'; interface IDeepLTranslation { detected_source_language: string; text: string; } /** * DeepL translation service provider class representation. * Encapsulates the service provider settings and information. * Provides languages supported by the service provider. * Resolves API call to service provider to resolve the translation request. * @class * @augments AutoTranslate */ class DeeplAutoTranslate extends AutoTranslate { apiKey: string; apiEndPointUrl: string; private supportedLanguageEndpointUrl: string; /** * setup api reference to deepl translate to be used as message translation provider. * @constructor */ constructor() { super(); this.name = 'deepl-translate'; this.apiEndPointUrl = proApiEndpoint; // Get the service provide API key. settings.watch('AutoTranslate_DeepLAPIKey', (value) => { this.apiKey = value; // if the api key ends with `:fx` it is a free api key if (/:fx$/.test(value)) { this.apiEndPointUrl = freeApiEndpoint; this.supportedLanguageEndpointUrl = `https://api-free.deepl.com/v2/languages`; return; } this.apiEndPointUrl = proApiEndpoint; this.supportedLanguageEndpointUrl = `https://api.deepl.com/v2/languages`; }); } /** * Returns metadata information about the service provide * @private implements super abstract method. * @return {object} */ _getProviderMetadata(): IProviderMetadata { return { name: this.name, displayName: i18n.t('AutoTranslate_DeepL'), settings: this._getSettings(), }; } /** * Returns necessary settings information about the translation service provider. * @private implements super abstract method. * @return {object} */ _getSettings(): IProviderMetadata['settings'] { return { apiKey: this.apiKey, apiEndPointUrl: this.apiEndPointUrl, }; } /** * Returns supported languages for translation by the active service provider. * Deepl does not provide an endpoint yet to retrieve the supported languages. * So each supported languages are explicitly maintained. * @private implements super abstract method. * @param {string} target * @returns {object} code : value pair */ async getSupportedLanguages(target: string): Promise { if (!this.apiKey) { return []; } if (this.supportedLanguages[target]) { return this.supportedLanguages[target]; } let result: (ISupportedLanguage & { supports_formality?: boolean })[] = []; // SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check. const request = await fetch(this.supportedLanguageEndpointUrl, { ignoreSsrfValidation: true, params: { type: 'target' }, headers: { Authorization: `DeepL-Auth-Key ${this.apiKey}`, }, }); if (!request.ok) { throw new Error('Failed to fetch supported languages'); } result = (await request.json()) as typeof result; if (this.supportedLanguages[target]) { return this.supportedLanguages[target]; } this.supportedLanguages[target || 'en'] = result.map(({ language, ...other }) => ({ ...other, language: new Intl.Locale(language).toString(), })); return this.supportedLanguages[target || 'en']; } /** * Send Request REST API call to the service provider. * Returns translated message for each target language in target languages. * @private * @param {object} message * @param {object} targetLanguages * @returns {object} translations: Translated messages for each language */ async _translateMessage(message: IMessage, targetLanguages: string[]): Promise { const translations: { [k: string]: string } = {}; const msgs = message.msg.split('\n'); const supportedLanguages = await this.getSupportedLanguages('en'); for (let language of targetLanguages) { if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) { language = language.substr(0, 2); } try { // SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check. const result = await fetch(this.apiEndPointUrl, { ignoreSsrfValidation: true, params: { target_lang: language, text: msgs }, headers: { Authorization: `DeepL-Auth-Key ${this.apiKey}`, }, method: 'POST', }); if (!result.ok) { throw new Error(result.statusText); } const body = await result.json(); if (result.status === 200 && body.translations && Array.isArray(body.translations) && body.translations.length > 0) { // store translation only when the source and target language are different. // multiple lines might contain different languages => Mix the text between source and detected target if neccessary const translatedText = body.translations .map((translation: IDeepLTranslation, index: number) => translation.detected_source_language !== language ? translation.text : msgs[index], ) .join('\n'); translations[language] = this.deTokenize(Object.assign({}, message, { msg: translatedText })); } } catch (err) { SystemLogger.error({ msg: 'Error translating message', err }); } } return translations; } /** * Returns translated message attachment description in target languages. * @private * @param {object} attachment * @param {object} targetLanguages * @returns {object} translated messages for each target language */ async _translateAttachmentDescriptions(attachment: MessageAttachment, targetLanguages: string[]): Promise { const translations: { [k: string]: string } = {}; const supportedLanguages = await this.getSupportedLanguages('en'); for (let language of targetLanguages) { if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) { language = language.substr(0, 2); } try { // SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check. const result = await fetch(this.apiEndPointUrl, { ignoreSsrfValidation: true, params: { auth_key: this.apiKey, target_lang: language, text: attachment.description || attachment.text || '', }, }); if (!result.ok) { throw new Error(result.statusText); } const body = await result.json(); if (result.status === 200 && body.translations && Array.isArray(body.translations) && body.translations.length > 0) { if (body.translations.map((translation: IDeepLTranslation) => translation.detected_source_language).join() !== language) { translations[language] = body.translations.map((translation: IDeepLTranslation) => translation.text); } } } catch (err) { SystemLogger.error({ msg: 'Error translating message attachment', err }); } } return translations; } } // Register DeepL translation provider to the registry. TranslationProviderRegistry.registerProvider(new DeeplAutoTranslate());