Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Integrate DEEPL translation service to RC core #12174

Merged
merged 25 commits into from
Aug 10, 2019
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a14b080
service extension for deepl
vickyokrm Apr 30, 2019
d0198cc
Merge remote-tracking branch 'RC/develop' into DEEPL-translation
vickyokrm May 2, 2019
a2632d2
Add translations
vickyokrm May 2, 2019
9a68dad
Merge remote-tracking branch 'RC/develop' into DEEPL-translation
vickyokrm May 4, 2019
7e5773d
Merge remote-tracking branch 'upstream/develop' into DEEPL-translation
mrsimpson Jul 11, 2019
eccb2a6
Add lint-fix-command
mrsimpson Jul 11, 2019
df73620
Adhere to new linting rules
mrsimpson Jul 11, 2019
48d08a5
Hard code service enpoint URLs of providers
mrsimpson Jul 11, 2019
6f7f8e1
Fix translation of attachment descriptions by DeepL
mrsimpson Jul 11, 2019
77f44fa
Fix getSupportedLanguages by Google
mrsimpson Jul 11, 2019
e954f87
Merge branch 'develop' into DEEPL-translation
mrsimpson Jul 11, 2019
f6bfa66
move renameSetting to Settings model
vickyokrm Jul 22, 2019
d3e2790
Add migrations for rename settings
vickyokrm Jul 22, 2019
cc36183
update package.json
vickyokrm Jul 22, 2019
a401edb
Fix formatting to make consistent with the other code
geekgonecrazy Jul 23, 2019
f58a8bc
Add additional comments
vickyokrm Jul 24, 2019
f6f9322
Use active provider for context menu translations
vickyokrm Jul 24, 2019
455e8f2
Add invidual key for each providers
vickyokrm Jul 24, 2019
c47c199
Nothing to migrate
vickyokrm Jul 24, 2019
fec4451
Fix spelling in comment
geekgonecrazy Jul 25, 2019
a904153
Merge branch 'develop' into DEEPL-translation
geekgonecrazy Jul 27, 2019
0499d05
Merge branch 'develop' into DEEPL-translation
mrsimpson Aug 9, 2019
54eb6c9
Merge branch 'develop' into DEEPL-translation
mrsimpson Aug 10, 2019
b2dd0a7
Replace TAPi18n library with rocketchat:tapi18n
mrsimpson Aug 10, 2019
d11b43c
Use correct setting
geekgonecrazy Aug 10, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/autotranslate/client/lib/autotranslate.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export const AutoTranslate = {
this.supportedLanguages = languages || [];
});

Meteor.call('autoTranslate.getProviderUiMetadata', (err, metadata) => {
vickyokrm marked this conversation as resolved.
Show resolved Hide resolved
this.providersMetadata = metadata;
});

Tracker.autorun(() => {
Subscriptions.find().observeChanges({
changed: (id, fields) => {
Expand Down
4 changes: 4 additions & 0 deletions app/autotranslate/client/stylesheets/autotranslate.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
left: 5px;

border-left: 0;

& .translation-provider {
vickyokrm marked this conversation as resolved.
Show resolved Hide resolved
display: none;
}
}
}
}
227 changes: 151 additions & 76 deletions app/autotranslate/server/autotranslate.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,88 @@
import { Meteor } from 'meteor/meteor';
import { HTTP } from 'meteor/http';
import _ from 'underscore';
import s from 'underscore.string';

import { settings } from '../../settings';
import { callbacks } from '../../callbacks';
import { Subscriptions, Messages } from '../../models';
import { Markdown } from '../../markdown/server';
import { Logger } from '../../logger';

/**
* This class allows translation providers to
* register,load and also returns the active provider.
*/
export class TranslationProviderRegistry {
geekgonecrazy marked this conversation as resolved.
Show resolved Hide resolved
/**
* Registers the translation provider into the registry.
* @param {*} provider
*/
static registerProvider(provider) {
// get provider information
const metadata = provider._getProviderMetadata();
if (!TranslationProviderRegistry._providers) {
TranslationProviderRegistry._providers = {};
}
TranslationProviderRegistry._providers[metadata.name] = provider;
}

class AutoTranslate {
/**
* Return the active Translation provider
*/
static getActiveProvider() {
return TranslationProviderRegistry._providers[TranslationProviderRegistry._activeProvider];
}

/**
* Make the activated provider by setting as the active.
*/
static loadActiveServiceProvider() {
settings.get('AutoTranslate_ServiceProvider', (key, value) => {
TranslationProviderRegistry._activeProvider = value;
});
}
}

/**
* Generic auto translate base implementation.
* This class provides generic parts of implementation for
* tokenization, detokenization, call back register and unregister.
* @abstract
* @class
*/
export class AutoTranslate {
geekgonecrazy marked this conversation as resolved.
Show resolved Hide resolved
/**
* Encapsulate the api key and provider settings.
* @constructor
*/
constructor() {
this.name = '';
this.languages = [];
this.enabled = settings.get('AutoTranslate_Enabled');
this.apiKey = settings.get('AutoTranslate_GoogleAPIKey');
this.supportedLanguages = {};
callbacks.add('afterSaveMessage', this.translateMessage.bind(this), callbacks.priority.MEDIUM, 'AutoTranslate');

// Get Auto Translate Active flag
geekgonecrazy marked this conversation as resolved.
Show resolved Hide resolved
settings.get('AutoTranslate_Enabled', (key, value) => {
this.enabled = value;
this.autoTranslateEnabled = value;
});
settings.get('AutoTranslate_GoogleAPIKey', (key, value) => {
this.apiKey = value;

/** Register the active service provider on the 'AfterSaveMessage' callback.
geekgonecrazy marked this conversation as resolved.
Show resolved Hide resolved
* So the registered provider will be invoked when a message is saved.
* All the other inactive service provider must be deactivated.
*/
settings.get('AutoTranslate_ServiceProvider', (key, value) => {
if (this.name === value) {
this.registerAfterSaveMsgCallBack(this.name);
} else {
this.unRegisterAfterSaveMsgCallBack(this.name);
}
});
}

/**
* Extracts non-translatable parts of a message
* @param {object} message
* @return {object} message
*/
tokenize(message) {
if (!message.tokens || !Array.isArray(message.tokens)) {
message.tokens = [];
Expand Down Expand Up @@ -93,10 +152,12 @@ class AutoTranslate {

tokenizeCode(message) {
let count = message.tokens.length;

message.html = message.msg;
message = Markdown.parseMessageNotEscaped(message);
message.msg = message.html;

// Some parsers (e. g. Marked) wrap the complete message in a <p> - this is unnecessary and should be ignored with respect to translations
geekgonecrazy marked this conversation as resolved.
Show resolved Hide resolved
const regexWrappedParagraph = new RegExp('^\s*<p>|<\/p>\s*$', 'gm');
message.msg = message.msg.replace(regexWrappedParagraph, '');

for (const tokenIndex in message.tokens) {
if (message.tokens.hasOwnProperty(tokenIndex)) {
Expand Down Expand Up @@ -153,8 +214,17 @@ class AutoTranslate {
return message.msg;
}

/**
* Triggers the translation of the prepared (tokenized) message
* and persists the result
* @public
* @param {object} message
* @param {object} room
* @param {object} targetLanguage
* @returns {object} unmodified message object.
*/
translateMessage(message, room, targetLanguage) {
if (this.enabled && this.apiKey) {
if (this.autoTranslateEnabled && this.apiKey) {
let targetLanguages;
if (targetLanguage) {
targetLanguages = [targetLanguage];
Expand All @@ -163,35 +233,13 @@ class AutoTranslate {
}
if (message.msg) {
Meteor.defer(() => {
const translations = {};
let targetMessage = Object.assign({}, message);

targetMessage.html = s.escapeHTML(String(targetMessage.msg));
targetMessage = this.tokenize(targetMessage);

let msgs = targetMessage.msg.split('\n');
msgs = msgs.map((msg) => encodeURIComponent(msg));
const query = `q=${ msgs.join('&q=') }`;

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('https://translation.googleapis.com/language/translate/v2', { params: { key: this.apiKey, target: language }, query });
} catch (e) {
console.log('Error translating message', e);
return message;
}
if (result.statusCode === 200 && result.data && result.data.data && result.data.data.translations && Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0) {
const txt = result.data.data.translations.map((translation) => translation.translatedText).join('\n');
translations[language] = this.deTokenize(Object.assign({}, targetMessage, { msg: txt }));
}
});
const translations = this._translateMessage(targetMessage, targetLanguages);
geekgonecrazy marked this conversation as resolved.
Show resolved Hide resolved
if (!_.isEmpty(translations)) {
Messages.addTranslations(message._id, translations);
Messages.addTranslations(message._id, translations, TranslationProviderRegistry._activeProvider);
}
});
}
Expand All @@ -201,20 +249,8 @@ class AutoTranslate {
for (const index in message.attachments) {
if (message.attachments.hasOwnProperty(index)) {
const attachment = message.attachments[index];
const translations = {};
if (attachment.description || attachment.text) {
const query = `q=${ 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);
}
const result = HTTP.get('https://translation.googleapis.com/language/translate/v2', { params: { key: this.apiKey, target: language }, query });
if (result.statusCode === 200 && result.data && result.data.data && result.data.data.translations && Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0) {
const txt = result.data.data.translations.map((translation) => translation.translatedText).join('\n');
translations[language] = txt;
}
});
const translations = this._translateAttachmentDescriptions(attachment, targetLanguages);
if (!_.isEmpty(translations)) {
Messages.addAttachmentTranslations(message._id, index, translations);
}
Expand All @@ -227,37 +263,76 @@ class AutoTranslate {
return message;
}

getSupportedLanguages(target) {
if (this.enabled && this.apiKey) {
if (this.supportedLanguages[target]) {
return this.supportedLanguages[target];
}
/**
* On changing the service provider, the callback in which the translation
* is being requested needs to be switched to the new provider
* @protected
* @param {string} provider
*/
registerAfterSaveMsgCallBack(provider) {
callbacks.add('afterSaveMessage', this.translateMessage.bind(this), callbacks.priority.MEDIUM, provider);
}

let result;
const params = { key: this.apiKey };
if (target) {
params.target = target;
}
/**
* On changing the service provider, the callback in which the translation
* is being requested needs to be deactivated for the all other translation providers
* @protected
* @param {string} provider
*/
unRegisterAfterSaveMsgCallBack(provider) {
callbacks.remove('afterSaveMessage', provider);
}

if (this.supportedLanguages[target]) {
return this.supportedLanguages[target];
}
/**
* Returns metadata information about the service provider which is used by
* the generic implementation
* @abstract
* @protected
* @returns { name, displayName, settings }
};
*/
_getProviderMetadata() {
Logger.warn('must be implemented by subclass!', '_getProviderMetadata');
}

try {
result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', { params });
} catch (e) {
if (e.response && e.response.statusCode === 400 && e.response.data && e.response.data.error && e.response.data.error.status === 'INVALID_ARGUMENT') {
params.target = 'en';
target = 'en';
if (!this.supportedLanguages[target]) {
result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', { params });
}
}
}
this.supportedLanguages[target || 'en'] = result && result.data && result.data.data && result.data.data.languages;
return this.supportedLanguages[target || 'en'];
}

/**
* Provides the possible languages _from_ which a message can be translated into a target language
* @abstract
* @protected
* @param {string} target - the language into which shall be translated
* @returns [{ language, name }]
*/
getSupportedLanguages(target) {
Logger.warn('must be implemented by subclass!', 'getSupportedLanguages', target);
}

/**
* Performs the actual translation of a message,
* usually by sending a REST API call to the service provider.
* @abstract
* @protected
* @param {object} message
* @param {object} targetLanguages
* @return {object}
*/
_translateMessage(message, targetLanguages) {
Logger.warn('must be implemented by subclass!', '_translateMessage', message, targetLanguages);
}

/**
* Performs the actual translation of an attachment (precisely its description),
* usually by sending a REST API call to the service provider.
* @abstract
* @param {object} attachment
* @param {object} targetLanguages
* @returns {object} translated messages for each target language
*/
_translateAttachmentDescriptions(attachment, targetLanguages) {
Logger.warn('must be implemented by subclass!', '_translateAttachmentDescriptions', attachment, targetLanguages);
}
}

export default new AutoTranslate();
Meteor.startup(() => {
TranslationProviderRegistry.loadActiveServiceProvider();
});
Loading