Skip to content

Commit 4709af2

Browse files
ThomasRoehlmrsimpson
authored andcommitted
Feature/RocketChat#175 auto translate with deepl (RocketChat#311)
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.
1 parent e28fb6d commit 4709af2

23 files changed

+938
-132
lines changed

.meteor/packages

+1
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,5 @@ assistify:defaults
190190
chatpal:search
191191
rocketchat:version-check
192192
meteorhacks:aggregate
193+
assistify:deepl-translation
193194
overture8:wordcloud2

.meteor/versions

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ accounts-twitter@1.4.1
99
aldeed:simple-schema@1.5.4
1010
allow-deny@1.1.0
1111
assistify:ai@0.2.0
12+
assistify:deepl-translation@0.0.1
1213
assistify:defaults@0.0.1
1314
assistify:help-request@0.1.0
14-
1515
autoupdate@1.4.0
1616
babel-compiler@7.0.7
1717
babel-runtime@1.2.2
@@ -26,7 +26,6 @@ caching-html-compiler@1.1.2
2626
callback-hook@1.1.0
2727
cfs:http-methods@0.0.32
2828
chatpal:search@0.0.1
29-
3029
check@1.3.1
3130
coffeescript@1.0.17
3231
dandv:caret-position@2.1.1
@@ -116,8 +115,8 @@ oauth2@1.2.0
116115
observe-sequence@1.0.16
117116
ordered-dict@1.1.0
118117
ostrio:cookies@2.2.4
119-
pauli:accounts-linkedin@2.1.5
120118
overture8:wordcloud2@1.0.0
119+
pauli:accounts-linkedin@2.1.5
121120
pauli:linkedin-oauth@1.2.0
122121
percolate:synced-cron@1.3.2
123122
promise@0.10.2

packages/assistify-deepl-translation/README.md

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Package.describe({
2+
name: 'assistify:deepl-translation',
3+
version: '0.0.1',
4+
// Brief, one-line summary of the package.
5+
summary: 'Empowers RocketChat by integrating DEEPL text translation engine',
6+
// URL to the Git repository containing the source code for this package.
7+
git: 'https://github.com/assistify',
8+
// By default, Meteor will default to using README.md for documentation.
9+
// To avoid submitting documentation, set this field to null.
10+
documentation: 'README.md'
11+
});
12+
13+
Package.onUse(function(api) {
14+
api.versionsFrom('1.6.0.1');
15+
api.use('ecmascript');
16+
api.use('rocketchat:lib');
17+
api.use('rocketchat:autotranslate');
18+
api.addFiles('server/deeplTranslate.js', 'server');
19+
});
20+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* @author Vigneshwaran Odayappan <vickyokrm@gmail.com>
3+
*/
4+
5+
import {TranslationProviderRegistry, AutoTranslate} from 'meteor/rocketchat:autotranslate';
6+
import {RocketChat} from 'meteor/rocketchat:lib';
7+
import _ from 'underscore';
8+
9+
/**
10+
* Represents DEEPL translate class
11+
* @class
12+
* @augments AutoTranslate
13+
*/
14+
class DeeplAutoTranslate extends AutoTranslate {
15+
/**
16+
* setup api reference to deepl translate to be used as message translation provider.
17+
* @constructor
18+
*/
19+
constructor() {
20+
super();
21+
this.name = 'deepl-translate';
22+
this.apiEndPointUrl = 'https://api.deepl.com/v1/translate';
23+
// self register & de-register callback - afterSaveMessage based on the activeProvider
24+
RocketChat.settings.get('AutoTranslate_ServiceProvider', (key, value) => {
25+
if (this.name === value) {
26+
this._registerAfterSaveMsgCallBack(this.name);
27+
} else {
28+
this._unRegisterAfterSaveMsgCallBack(this.name);
29+
}
30+
});
31+
}
32+
33+
/**
34+
* Returns metadata information about the service provide
35+
* @private implements super abstract method.
36+
* @return {object}
37+
*/
38+
_getProviderMetadata() {
39+
return {
40+
name: this.name,
41+
displayName: TAPi18n.__('AutoTranslate_DeepL'),
42+
settings: this._getSettings()
43+
};
44+
}
45+
46+
/**
47+
* Returns necessary settings information about the translation service provider.
48+
* @private implements super abstract method.
49+
* @return {object}
50+
*/
51+
_getSettings() {
52+
return {
53+
apiKey: this.apiKey,
54+
apiEndPointUrl: this.apiEndPointUrl
55+
};
56+
}
57+
58+
/**
59+
* Returns supported languages for translation by the active service provider.
60+
* @private implements super abstract method.
61+
* @param {string} target
62+
* @returns {object} code : value pair
63+
*/
64+
_getSupportedLanguages(target) {
65+
if (this.autoTranslateEnabled && this.apiKey) {
66+
if (this.supportedLanguages[target]) {
67+
return this.supportedLanguages[target];
68+
}
69+
return this.supportedLanguages[target] = [
70+
{
71+
'language': 'EN',
72+
'name': TAPi18n.__('English', { lng: target })
73+
},
74+
{
75+
'language': 'DE',
76+
'name': TAPi18n.__('German', { lng: target })
77+
},
78+
{
79+
'language': 'FR',
80+
'name': TAPi18n.__('French', { lng: target })
81+
},
82+
{
83+
'language': 'ES',
84+
'name': TAPi18n.__('Spanish', { lng: target })
85+
},
86+
{
87+
'language': 'IT',
88+
'name': TAPi18n.__('Italian', { lng: target })
89+
},
90+
{
91+
'language': 'NL',
92+
'name': TAPi18n.__('Dutch', { lng: target })
93+
},
94+
{
95+
'language': 'PL',
96+
'name': TAPi18n.__('Polish', { lng: target })
97+
}
98+
];
99+
}
100+
}
101+
102+
/**
103+
* Send Request REST API call to the service provider.
104+
* Returns translated message for each target language in target languages.
105+
* @private
106+
* @param {object} targetMessage
107+
* @param {object} targetLanguages
108+
* @returns {object} translations: Translated messages for each language
109+
*/
110+
_sendRequestTranslateMessage(targetMessage, targetLanguages) {
111+
const translations = {};
112+
let msgs = targetMessage.msg.split('\n');
113+
msgs = msgs.map(msg => encodeURIComponent(msg));
114+
const query = `text=${ msgs.join('&text=') }`;
115+
const supportedLanguages = this._getSupportedLanguages('en');
116+
targetLanguages.forEach(language => {
117+
if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, {language})) {
118+
language = language.substr(0, 2);
119+
}
120+
let result;
121+
try {
122+
result = HTTP.get(this.apiEndPointUrl, {
123+
params: {
124+
auth_key: this.apiKey,
125+
target_lang: language
126+
}, query
127+
});
128+
} catch (e) {
129+
console.log('Error translating message', e);
130+
}
131+
if (result.statusCode === 200 && result.data && result.data.translations && Array.isArray(result.data.translations) && result.data.translations.length > 0) {
132+
// store translation only when the source and target language are different.
133+
if (result.data.translations.map(translation => translation.detected_source_language).join() !== language) {
134+
const txt = result.data.translations.map(translation => translation.text).join('\n');
135+
translations[language] = this.deTokenize(Object.assign({}, targetMessage, {msg: txt}));
136+
}
137+
}
138+
});
139+
return translations;
140+
}
141+
142+
/**
143+
* Returns translated message attachment description in target languages.
144+
* @private
145+
* @param {object} attachment
146+
* @param {object} targetLanguages
147+
* @returns {object} translated messages for each target language
148+
*/
149+
_sendRequestTranslateMessageAttachments(attachment, targetLanguages) {
150+
const translations = {};
151+
const query = `text=${ encodeURIComponent(attachment.description || attachment.text) }`;
152+
const supportedLanguages = this._getSupportedLanguages('en');
153+
targetLanguages.forEach(language => {
154+
if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, {language})) {
155+
language = language.substr(0, 2);
156+
}
157+
let result;
158+
try {
159+
result = HTTP.get(this.apiEndPointUrl, {
160+
params: {
161+
auth_key: this.apiKey,
162+
target_lang: language
163+
}, query
164+
});
165+
} catch (e) {
166+
console.log('Error translating message attachment', e);
167+
}
168+
if (result.statusCode === 200 && result.data && result.data.translations && Array.isArray(result.data.translations) && result.data.translations.length > 0) {
169+
if (result.data.translations.map(translation => translation.detected_source_language).join() !== language) {
170+
translations[language] = result.data.translations.map(translation => translation.text).join('\n');
171+
}
172+
}
173+
});
174+
return translations;
175+
}
176+
}
177+
178+
179+
Meteor.startup(() => {
180+
TranslationProviderRegistry.registerProvider(new DeeplAutoTranslate());
181+
RocketChat.AutoTranslate = TranslationProviderRegistry.getActiveServiceProvider();
182+
});

packages/rocketchat-autotranslate/client/lib/actionButton.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* globals RocketChat */
12
Meteor.startup(function() {
23
Tracker.autorun(function() {
34
if (RocketChat.settings.get('AutoTranslate_Enabled') && RocketChat.authz.hasAtLeastOnePermission(['auto-translate'])) {

packages/rocketchat-autotranslate/client/lib/autotranslate.js

+28-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* globals RocketChat */
12
import _ from 'underscore';
23

34
RocketChat.AutoTranslate = {
@@ -7,11 +8,11 @@ RocketChat.AutoTranslate = {
78
getLanguage(rid) {
89
let subscription = {};
910
if (rid) {
10-
subscription = RocketChat.models.Subscriptions.findOne({ rid }, { fields: { autoTranslateLanguage: 1 } });
11+
subscription = RocketChat.models.Subscriptions.findOne({rid}, {fields: {autoTranslateLanguage: 1}});
1112
}
1213
const language = subscription && subscription.autoTranslateLanguage || Meteor.user().language || window.defaultUserLanguage();
1314
if (language.indexOf('-') !== -1) {
14-
if (!_.findWhere(this.supportedLanguages, { language })) {
15+
if (!_.findWhere(this.supportedLanguages, {language})) {
1516
return language.substr(0, 2);
1617
}
1718
}
@@ -41,11 +42,15 @@ RocketChat.AutoTranslate = {
4142
Meteor.call('autoTranslate.getSupportedLanguages', 'en', (err, languages) => {
4243
this.supportedLanguages = languages || [];
4344
});
44-
4545
Tracker.autorun(() => {
4646
if (RocketChat.settings.get('AutoTranslate_Enabled') && RocketChat.authz.hasAtLeastOnePermission(['auto-translate'])) {
4747
RocketChat.callbacks.add('renderMessage', (message) => {
48-
const subscription = RocketChat.models.Subscriptions.findOne({ rid: message.rid }, { fields: { autoTranslate: 1, autoTranslateLanguage: 1 } });
48+
const subscription = RocketChat.models.Subscriptions.findOne({rid: message.rid}, {
49+
fields: {
50+
autoTranslate: 1,
51+
autoTranslateLanguage: 1
52+
}
53+
});
4954
const autoTranslateLanguage = this.getLanguage(message.rid);
5055
if (message.u && message.u._id !== Meteor.userId()) {
5156
if (!message.translations) {
@@ -69,15 +74,23 @@ RocketChat.AutoTranslate = {
6974

7075
RocketChat.callbacks.add('streamMessage', (message) => {
7176
if (message.u && message.u._id !== Meteor.userId()) {
72-
const subscription = RocketChat.models.Subscriptions.findOne({ rid: message.rid }, { fields: { autoTranslate: 1, autoTranslateLanguage: 1 } });
77+
const subscription = RocketChat.models.Subscriptions.findOne({rid: message.rid}, {
78+
fields: {
79+
autoTranslate: 1,
80+
autoTranslateLanguage: 1
81+
}
82+
});
7383
const language = this.getLanguage(message.rid);
7484
if (subscription && subscription.autoTranslate === true && ((message.msg && (!message.translations || !message.translations[language])))) { // || (message.attachments && !_.find(message.attachments, attachment => { return attachment.translations && attachment.translations[language]; }))
75-
RocketChat.models.Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } });
85+
RocketChat.models.Messages.update({_id: message._id}, {$set: {autoTranslateFetching: true}});
7686
} else if (this.messageIdsToWait[message._id] !== undefined && subscription && subscription.autoTranslate !== true) {
77-
RocketChat.models.Messages.update({ _id: message._id }, { $set: { autoTranslateShowInverse: true }, $unset: { autoTranslateFetching: true } });
87+
RocketChat.models.Messages.update({_id: message._id}, {
88+
$set: {autoTranslateShowInverse: true},
89+
$unset: {autoTranslateFetching: true}
90+
});
7891
delete this.messageIdsToWait[message._id];
7992
} else if (message.autoTranslateFetching === true) {
80-
RocketChat.models.Messages.update({ _id: message._id }, { $unset: { autoTranslateFetching: true } });
93+
RocketChat.models.Messages.update({_id: message._id}, {$unset: {autoTranslateFetching: true}});
8194
}
8295
}
8396
}, RocketChat.callbacks.priority.HIGH - 3, 'autotranslate-stream');
@@ -86,7 +99,14 @@ RocketChat.AutoTranslate = {
8699
RocketChat.callbacks.remove('streamMessage', 'autotranslate-stream');
87100
}
88101
});
102+
103+
Tracker.autorun(() => {
104+
if (RocketChat.settings.get('AutoTranslate_ServiceProvider') && RocketChat.authz.hasAtLeastOnePermission(['auto-translate'])) {
105+
Meteor.call('autoTranslate.refreshProviderSettings');
106+
}
107+
});
89108
}
109+
90110
};
91111

92112
Meteor.startup(function() {

packages/rocketchat-autotranslate/client/views/autoTranslateFlexTab.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* globals ChatSubscription */
1+
/* globals ChatSubscription, RocketChat */
22
import _ from 'underscore';
33
import toastr from 'toastr';
44

packages/rocketchat-autotranslate/package.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ Package.onUse(function(api) {
2525

2626
api.addFiles([
2727
'server/settings.js',
28-
'server/autotranslate.js',
2928
'server/permissions.js',
29+
'server/autotranslate.js',
30+
'server/googleTranslate.js',
3031
'server/models/Messages.js',
32+
'server/models/Settings.js',
3133
'server/models/Subscriptions.js',
3234
'server/methods/saveSettings.js',
3335
'server/methods/translateMessage.js',
34-
'server/methods/getSupportedLanguages.js'
36+
'server/methods/getSupportedLanguages.js',
37+
'server/methods/refreshProviderSettings.js'
3538
], 'server');
39+
api.mainModule('server/index.js', 'server');
3640
});

0 commit comments

Comments
 (0)