/** * @author Vigneshwaran Odayappan */ import type { IMessage, IProviderMetadata, ISupportedLanguage, ITranslationResult, MessageAttachment } from '@rocket.chat/core-typings'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import _ from 'underscore'; import { AutoTranslate, TranslationProviderRegistry } from './autotranslate'; import { settings } from '../../settings'; import { i18n } from '../i18n'; import { SystemLogger } from '../logger/system'; interface IGoogleTranslation { translatedText: string; } /** * Represents google translate class * @class * @augments AutoTranslate */ class GoogleAutoTranslate extends AutoTranslate { apiKey: string; apiEndPointUrl: string; /** * setup api reference to Google translate to be used as message translation provider. * @constructor */ constructor() { super(); this.name = 'google-translate'; this.apiEndPointUrl = 'https://translation.googleapis.com/language/translate/v2'; // Get the service provide API key. settings.watch('AutoTranslate_GoogleAPIKey', (value) => { this.apiKey = value; }); } /** * Returns metadata information about the service provider * @private implements super abstract method. * @returns {object} */ _getProviderMetadata(): IProviderMetadata { return { name: this.name, displayName: i18n.t('AutoTranslate_Google'), settings: this._getSettings(), }; } /** * Returns necessary settings information about the translation service provider. * @private implements super abstract method. * @returns {object} */ _getSettings(): IProviderMetadata['settings'] { return { apiKey: this.apiKey, apiEndPointUrl: this.apiEndPointUrl, }; } /** * Returns supported languages for translation by the active service provider. * Google Translate api provides the list of supported languages. * @private implements super abstract method. * @param {string} target : user language setting or 'en' * @returns {object} code : value pair */ async getSupportedLanguages(target: string): Promise { if (!this.apiKey) { return []; } if (this.supportedLanguages[target]) { return this.supportedLanguages[target]; } let result: { data?: { languages: ISupportedLanguage[] } } = {}; const params = { key: this.apiKey, ...(target && { target }), }; 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 request = await fetch(`https://translation.googleapis.com/language/translate/v2/languages`, { ignoreSsrfValidation: true, params, }); if (!request.ok && request.status === 400 && request.statusText === 'INVALID_ARGUMENT') { throw new Error('Failed to fetch supported languages'); } result = (await request.json()) as typeof result; } catch (e: any) { // Fallback: Get the English names of the target languages if (e.message === 'Failed to fetch supported languages') { params.target = 'en'; target = 'en'; if (!this.supportedLanguages[target]) { // 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(`https://translation.googleapis.com/language/translate/v2/languages`, { ignoreSsrfValidation: true, params, }); result = (await request.json()) as typeof result; } } } if (this.supportedLanguages[target]) { return this.supportedLanguages[target]; } this.supportedLanguages[target || 'en'] = result?.data?.languages || []; 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 supportedLanguages = await this.getSupportedLanguages('en'); for (let language of targetLanguages) { if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) { language = language.slice(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: { key: this.apiKey, target: language, format: 'text', q: message.msg.split('\n'), }, }); if (!result.ok) { throw new Error(result.statusText); } const body = await result.json(); if ( result.status === 200 && body.data?.translations && Array.isArray(body.data.translations) && body.data.translations.length > 0 ) { const txt = body.data.translations.map((translation: IGoogleTranslation) => translation.translatedText).join('\n'); translations[language] = this.deTokenize(Object.assign({}, message, { msg: txt })); } } 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 attachment descriptions 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.slice(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: { key: this.apiKey, target: language, format: 'text', q: attachment.description || attachment.text || '', }, }); if (!result.ok) { throw new Error(result.statusText); } const body = await result.json(); if ( result.status === 200 && body.data?.translations && Array.isArray(body.data.translations) && body.data.translations.length > 0 ) { translations[language] = body.data.translations.map((translation: IGoogleTranslation) => translation.translatedText).join('\n'); } } catch (err) { SystemLogger.error({ msg: 'Error translating message', err }); } } return translations; } } // Register Google translation provider. TranslationProviderRegistry.registerProvider(new GoogleAutoTranslate());