Skip to content

Commit

Permalink
Feature/RocketChat#175 auto translate with deepl (RocketChat#311)
Browse files Browse the repository at this point in the history
This brings the ability to use DeepL for autotranslation of messages.
Doing that, it also introduces a pluggable service registry pattern for translation providers with an abstract superclass handling (most) of the common parts.
  • Loading branch information
ThomasRoehl authored and mrsimpson committed Apr 25, 2018
1 parent e28fb6d commit 4709af2
Show file tree
Hide file tree
Showing 23 changed files with 938 additions and 132 deletions.
1 change: 1 addition & 0 deletions .meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,5 @@ assistify:defaults
chatpal:search
rocketchat:version-check
meteorhacks:aggregate
assistify:deepl-translation
overture8:wordcloud2
5 changes: 2 additions & 3 deletions .meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ accounts-twitter@1.4.1
aldeed:simple-schema@1.5.4
allow-deny@1.1.0
assistify:ai@0.2.0
assistify:deepl-translation@0.0.1
assistify:defaults@0.0.1
assistify:help-request@0.1.0

autoupdate@1.4.0
babel-compiler@7.0.7
babel-runtime@1.2.2
Expand All @@ -26,7 +26,6 @@ caching-html-compiler@1.1.2
callback-hook@1.1.0
cfs:http-methods@0.0.32
chatpal:search@0.0.1

check@1.3.1
coffeescript@1.0.17
dandv:caret-position@2.1.1
Expand Down Expand Up @@ -116,8 +115,8 @@ oauth2@1.2.0
observe-sequence@1.0.16
ordered-dict@1.1.0
ostrio:cookies@2.2.4
pauli:accounts-linkedin@2.1.5
overture8:wordcloud2@1.0.0
pauli:accounts-linkedin@2.1.5
pauli:linkedin-oauth@1.2.0
percolate:synced-cron@1.3.2
promise@0.10.2
Expand Down
Empty file.
20 changes: 20 additions & 0 deletions packages/assistify-deepl-translation/package.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Package.describe({
name: 'assistify:deepl-translation',
version: '0.0.1',
// Brief, one-line summary of the package.
summary: 'Empowers RocketChat by integrating DEEPL text translation engine',
// URL to the Git repository containing the source code for this package.
git: 'https://github.com/assistify',
// By default, Meteor will default to using README.md for documentation.
// To avoid submitting documentation, set this field to null.
documentation: 'README.md'
});

Package.onUse(function(api) {
api.versionsFrom('1.6.0.1');
api.use('ecmascript');
api.use('rocketchat:lib');
api.use('rocketchat:autotranslate');
api.addFiles('server/deeplTranslate.js', 'server');
});

182 changes: 182 additions & 0 deletions packages/assistify-deepl-translation/server/deeplTranslate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* @author Vigneshwaran Odayappan <vickyokrm@gmail.com>
*/

import {TranslationProviderRegistry, AutoTranslate} from 'meteor/rocketchat:autotranslate';
import {RocketChat} from 'meteor/rocketchat:lib';
import _ from 'underscore';

/**
* Represents DEEPL translate class
* @class
* @augments AutoTranslate
*/
class DeeplAutoTranslate extends AutoTranslate {
/**
* setup api reference to deepl translate to be used as message translation provider.
* @constructor
*/
constructor() {
super();
this.name = 'deepl-translate';
this.apiEndPointUrl = 'https://api.deepl.com/v1/translate';
// self register & de-register callback - afterSaveMessage based on the activeProvider
RocketChat.settings.get('AutoTranslate_ServiceProvider', (key, value) => {
if (this.name === value) {
this._registerAfterSaveMsgCallBack(this.name);
} else {
this._unRegisterAfterSaveMsgCallBack(this.name);
}
});
}

/**
* Returns metadata information about the service provide
* @private implements super abstract method.
* @return {object}
*/
_getProviderMetadata() {
return {
name: this.name,
displayName: TAPi18n.__('AutoTranslate_DeepL'),
settings: this._getSettings()
};
}

/**
* Returns necessary settings information about the translation service provider.
* @private implements super abstract method.
* @return {object}
*/
_getSettings() {
return {
apiKey: this.apiKey,
apiEndPointUrl: this.apiEndPointUrl
};
}

/**
* Returns supported languages for translation by the active service provider.
* @private implements super abstract method.
* @param {string} target
* @returns {object} code : value pair
*/
_getSupportedLanguages(target) {
if (this.autoTranslateEnabled && this.apiKey) {
if (this.supportedLanguages[target]) {
return this.supportedLanguages[target];
}
return this.supportedLanguages[target] = [
{
'language': 'EN',
'name': TAPi18n.__('English', { lng: target })
},
{
'language': 'DE',
'name': TAPi18n.__('German', { lng: target })
},
{
'language': 'FR',
'name': TAPi18n.__('French', { lng: target })
},
{
'language': 'ES',
'name': TAPi18n.__('Spanish', { lng: target })
},
{
'language': 'IT',
'name': TAPi18n.__('Italian', { lng: target })
},
{
'language': 'NL',
'name': TAPi18n.__('Dutch', { lng: target })
},
{
'language': 'PL',
'name': TAPi18n.__('Polish', { lng: target })
}
];
}
}

/**
* Send Request REST API call to the service provider.
* Returns translated message for each target language in target languages.
* @private
* @param {object} targetMessage
* @param {object} targetLanguages
* @returns {object} translations: Translated messages for each language
*/
_sendRequestTranslateMessage(targetMessage, targetLanguages) {
const translations = {};
let msgs = targetMessage.msg.split('\n');
msgs = msgs.map(msg => encodeURIComponent(msg));
const query = `text=${ msgs.join('&text=') }`;
const supportedLanguages = this._getSupportedLanguages('en');
targetLanguages.forEach(language => {
if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, {language})) {
language = language.substr(0, 2);
}
let result;
try {
result = HTTP.get(this.apiEndPointUrl, {
params: {
auth_key: this.apiKey,
target_lang: language
}, query
});
} catch (e) {
console.log('Error translating message', e);
}
if (result.statusCode === 200 && result.data && result.data.translations && Array.isArray(result.data.translations) && result.data.translations.length > 0) {
// store translation only when the source and target language are different.
if (result.data.translations.map(translation => translation.detected_source_language).join() !== language) {
const txt = result.data.translations.map(translation => translation.text).join('\n');
translations[language] = this.deTokenize(Object.assign({}, targetMessage, {msg: txt}));
}
}
});
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
*/
_sendRequestTranslateMessageAttachments(attachment, targetLanguages) {
const translations = {};
const query = `text=${ encodeURIComponent(attachment.description || attachment.text) }`;
const supportedLanguages = this._getSupportedLanguages('en');
targetLanguages.forEach(language => {
if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, {language})) {
language = language.substr(0, 2);
}
let result;
try {
result = HTTP.get(this.apiEndPointUrl, {
params: {
auth_key: this.apiKey,
target_lang: language
}, query
});
} catch (e) {
console.log('Error translating message attachment', e);
}
if (result.statusCode === 200 && result.data && result.data.translations && Array.isArray(result.data.translations) && result.data.translations.length > 0) {
if (result.data.translations.map(translation => translation.detected_source_language).join() !== language) {
translations[language] = result.data.translations.map(translation => translation.text).join('\n');
}
}
});
return translations;
}
}


Meteor.startup(() => {
TranslationProviderRegistry.registerProvider(new DeeplAutoTranslate());
RocketChat.AutoTranslate = TranslationProviderRegistry.getActiveServiceProvider();
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* globals RocketChat */
Meteor.startup(function() {
Tracker.autorun(function() {
if (RocketChat.settings.get('AutoTranslate_Enabled') && RocketChat.authz.hasAtLeastOnePermission(['auto-translate'])) {
Expand Down
36 changes: 28 additions & 8 deletions packages/rocketchat-autotranslate/client/lib/autotranslate.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* globals RocketChat */
import _ from 'underscore';

RocketChat.AutoTranslate = {
Expand All @@ -7,11 +8,11 @@ RocketChat.AutoTranslate = {
getLanguage(rid) {
let subscription = {};
if (rid) {
subscription = RocketChat.models.Subscriptions.findOne({ rid }, { fields: { autoTranslateLanguage: 1 } });
subscription = RocketChat.models.Subscriptions.findOne({rid}, {fields: {autoTranslateLanguage: 1}});
}
const language = subscription && subscription.autoTranslateLanguage || Meteor.user().language || window.defaultUserLanguage();
if (language.indexOf('-') !== -1) {
if (!_.findWhere(this.supportedLanguages, { language })) {
if (!_.findWhere(this.supportedLanguages, {language})) {
return language.substr(0, 2);
}
}
Expand Down Expand Up @@ -41,11 +42,15 @@ RocketChat.AutoTranslate = {
Meteor.call('autoTranslate.getSupportedLanguages', 'en', (err, languages) => {
this.supportedLanguages = languages || [];
});

Tracker.autorun(() => {
if (RocketChat.settings.get('AutoTranslate_Enabled') && RocketChat.authz.hasAtLeastOnePermission(['auto-translate'])) {
RocketChat.callbacks.add('renderMessage', (message) => {
const subscription = RocketChat.models.Subscriptions.findOne({ rid: message.rid }, { fields: { autoTranslate: 1, autoTranslateLanguage: 1 } });
const subscription = RocketChat.models.Subscriptions.findOne({rid: message.rid}, {
fields: {
autoTranslate: 1,
autoTranslateLanguage: 1
}
});
const autoTranslateLanguage = this.getLanguage(message.rid);
if (message.u && message.u._id !== Meteor.userId()) {
if (!message.translations) {
Expand All @@ -69,15 +74,23 @@ RocketChat.AutoTranslate = {

RocketChat.callbacks.add('streamMessage', (message) => {
if (message.u && message.u._id !== Meteor.userId()) {
const subscription = RocketChat.models.Subscriptions.findOne({ rid: message.rid }, { fields: { autoTranslate: 1, autoTranslateLanguage: 1 } });
const subscription = RocketChat.models.Subscriptions.findOne({rid: message.rid}, {
fields: {
autoTranslate: 1,
autoTranslateLanguage: 1
}
});
const language = this.getLanguage(message.rid);
if (subscription && subscription.autoTranslate === true && ((message.msg && (!message.translations || !message.translations[language])))) { // || (message.attachments && !_.find(message.attachments, attachment => { return attachment.translations && attachment.translations[language]; }))
RocketChat.models.Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } });
RocketChat.models.Messages.update({_id: message._id}, {$set: {autoTranslateFetching: true}});
} else if (this.messageIdsToWait[message._id] !== undefined && subscription && subscription.autoTranslate !== true) {
RocketChat.models.Messages.update({ _id: message._id }, { $set: { autoTranslateShowInverse: true }, $unset: { autoTranslateFetching: true } });
RocketChat.models.Messages.update({_id: message._id}, {
$set: {autoTranslateShowInverse: true},
$unset: {autoTranslateFetching: true}
});
delete this.messageIdsToWait[message._id];
} else if (message.autoTranslateFetching === true) {
RocketChat.models.Messages.update({ _id: message._id }, { $unset: { autoTranslateFetching: true } });
RocketChat.models.Messages.update({_id: message._id}, {$unset: {autoTranslateFetching: true}});
}
}
}, RocketChat.callbacks.priority.HIGH - 3, 'autotranslate-stream');
Expand All @@ -86,7 +99,14 @@ RocketChat.AutoTranslate = {
RocketChat.callbacks.remove('streamMessage', 'autotranslate-stream');
}
});

Tracker.autorun(() => {
if (RocketChat.settings.get('AutoTranslate_ServiceProvider') && RocketChat.authz.hasAtLeastOnePermission(['auto-translate'])) {
Meteor.call('autoTranslate.refreshProviderSettings');
}
});
}

};

Meteor.startup(function() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* globals ChatSubscription */
/* globals ChatSubscription, RocketChat */
import _ from 'underscore';
import toastr from 'toastr';

Expand Down
8 changes: 6 additions & 2 deletions packages/rocketchat-autotranslate/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ Package.onUse(function(api) {

api.addFiles([
'server/settings.js',
'server/autotranslate.js',
'server/permissions.js',
'server/autotranslate.js',
'server/googleTranslate.js',
'server/models/Messages.js',
'server/models/Settings.js',
'server/models/Subscriptions.js',
'server/methods/saveSettings.js',
'server/methods/translateMessage.js',
'server/methods/getSupportedLanguages.js'
'server/methods/getSupportedLanguages.js',
'server/methods/refreshProviderSettings.js'
], 'server');
api.mainModule('server/index.js', 'server');
});
Loading

0 comments on commit 4709af2

Please sign in to comment.