Skip to content

Commit 6914d11

Browse files
authored
[NEW] Translation via MS translate (#16363)
* Translation: Microsoft translate as new provider * Translation: Display translation provider * Linting * Add missing translation * Don't expose translation API keys to client * Refactor MS translate: remove redundant code * More precision in MS translate API key setting * Apply suggestions from code review Co-Authored-By: Rodrigo Nascimento <[email protected]> * reset package-lock to upstream Co-authored-by: Rodrigo Nascimento <[email protected]>
2 parents 4ec72d5 + b9bd2f6 commit 6914d11

File tree

8 files changed

+204
-4
lines changed

8 files changed

+204
-4
lines changed

app/autotranslate/server/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import './methods/saveSettings';
1313
import './methods/translateMessage';
1414
import './googleTranslate.js';
1515
import './deeplTranslate.js';
16+
import './msTranslate.js';
1617
import './methods/getProviderUiMetadata.js';
1718

1819
export {

app/autotranslate/server/logger.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Logger } from '../../logger';
2+
3+
export const logger = new Logger('AutoTranslate', {
4+
sections: {
5+
google: 'Google',
6+
deepl: 'DeepL',
7+
microsoft: 'Microsoft',
8+
},
9+
});
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* @author Vigneshwaran Odayappan <[email protected]>
3+
*/
4+
5+
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
6+
import { HTTP } from 'meteor/http';
7+
import _ from 'underscore';
8+
9+
import { TranslationProviderRegistry, AutoTranslate } from './autotranslate';
10+
import { logger } from './logger';
11+
import { settings } from '../../settings';
12+
13+
/**
14+
* Microsoft translation service provider class representation.
15+
* Encapsulates the service provider settings and information.
16+
* Provides languages supported by the service provider.
17+
* Resolves API call to service provider to resolve the translation request.
18+
* @class
19+
* @augments AutoTranslate
20+
*/
21+
class MsAutoTranslate extends AutoTranslate {
22+
/**
23+
* setup api reference to Microsoft translate to be used as message translation provider.
24+
* @constructor
25+
*/
26+
constructor() {
27+
super();
28+
this.name = 'microsoft-translate';
29+
this.apiEndPointUrl = 'https://api.cognitive.microsofttranslator.com/translate?api-version=3.0';
30+
this.apiDetectText = 'https://api.cognitive.microsofttranslator.com/detect?api-version=3.0';
31+
this.apiGetLanguages = 'https://api.cognitive.microsofttranslator.com/languages?api-version=3.0';
32+
this.breakSentence = 'https://api.cognitive.microsofttranslator.com/breaksentence?api-version=3.0';
33+
// Get the service provide API key.
34+
settings.get('AutoTranslate_MicrosoftAPIKey', (key, value) => {
35+
this.apiKey = value;
36+
});
37+
}
38+
39+
/**
40+
* Returns metadata information about the service provide
41+
* @private implements super abstract method.
42+
* @return {object}
43+
*/
44+
_getProviderMetadata() {
45+
return {
46+
name: this.name,
47+
displayName: TAPi18n.__('AutoTranslate_Microsoft'),
48+
settings: this._getSettings(),
49+
};
50+
}
51+
52+
/**
53+
* Returns necessary settings information about the translation service provider.
54+
* @private implements super abstract method.
55+
* @return {object}
56+
*/
57+
_getSettings() {
58+
return {
59+
apiKey: this.apiKey,
60+
apiEndPointUrl: this.apiEndPointUrl,
61+
};
62+
}
63+
64+
/**
65+
* Returns supported languages for translation by the active service provider.
66+
* Microsoft does not provide an endpoint yet to retrieve the supported languages.
67+
* So each supported languages are explicitly maintained.
68+
* @private implements super abstract method.
69+
* @param {string} target
70+
* @returns {object} code : value pair
71+
*/
72+
getSupportedLanguages(target) {
73+
if (this.autoTranslateEnabled && this.apiKey) {
74+
if (this.supportedLanguages[target]) {
75+
return this.supportedLanguages[target];
76+
}
77+
const languages = HTTP.get(this.apiGetLanguages);
78+
this.supportedLanguages[target] = Object.keys(languages.data.translation).map((language) => ({
79+
language,
80+
name: languages.data.translation[language].name,
81+
}));
82+
return this.supportedLanguages[target || 'en'];
83+
}
84+
}
85+
86+
/**
87+
* Re-use method for REST API consumption of MS translate.
88+
* @private
89+
* @param {object} message
90+
* @param {object} targetLanguages
91+
* @throws Communication Errors
92+
* @returns {object} translations: Translated messages for each language
93+
*/
94+
_translate(data, targetLanguages) {
95+
let translations = {};
96+
const supportedLanguages = this.getSupportedLanguages('en');
97+
targetLanguages = targetLanguages.map((language) => {
98+
if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) {
99+
language = language.substr(0, 2);
100+
}
101+
return language;
102+
});
103+
const url = `${ this.apiEndPointUrl }&to=${ targetLanguages.join('&to=') }`;
104+
const result = HTTP.post(url, {
105+
headers: {
106+
'Ocp-Apim-Subscription-Key': this.apiKey,
107+
'Content-Type': 'application/json; charset=UTF-8',
108+
},
109+
data,
110+
});
111+
112+
if (result.statusCode === 200 && result.data && result.data.length > 0) {
113+
// store translation only when the source and target language are different.
114+
translations = Object.assign({}, ...targetLanguages.map((language) =>
115+
({
116+
[language]: result.data.map((line) => line.translations.find((translation) => translation.to === language).text).join('\n'),
117+
}),
118+
));
119+
}
120+
121+
return translations;
122+
}
123+
124+
/**
125+
* Returns translated message for each target language.
126+
* @private
127+
* @param {object} message
128+
* @param {object} targetLanguages
129+
* @returns {object} translations: Translated messages for each language
130+
*/
131+
_translateMessage(message, targetLanguages) {
132+
// There are multi-sentence-messages where multiple sentences come from different languages
133+
// This is a problem for translation services since the language detection fails.
134+
// Thus, we'll split the message in sentences, get them translated, and join them again after translation
135+
const msgs = message.msg.split('\n').map((msg) => ({ Text: msg }));
136+
try {
137+
return this._translate(msgs, targetLanguages);
138+
} catch (e) {
139+
logger.microsoft.error('Error translating message', e);
140+
}
141+
return {};
142+
}
143+
144+
/**
145+
* Returns translated message attachment description in target languages.
146+
* @private
147+
* @param {object} attachment
148+
* @param {object} targetLanguages
149+
* @returns {object} translated messages for each target language
150+
*/
151+
_translateAttachmentDescriptions(attachment, targetLanguages) {
152+
try {
153+
return this._translate([{
154+
Text: attachment.description || attachment.text,
155+
}], targetLanguages);
156+
} catch (e) {
157+
logger.microsoft.error('Error translating message attachment', e);
158+
}
159+
return {};
160+
}
161+
}
162+
163+
// Register Microsoft translation provider to the registry.
164+
TranslationProviderRegistry.registerProvider(new MsAutoTranslate());

app/autotranslate/server/settings.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ Meteor.startup(function() {
2020
}, {
2121
key: 'deepl-translate',
2222
i18nLabel: 'AutoTranslate_DeepL',
23+
}, {
24+
key: 'microsoft-translate',
25+
i18nLabel: 'AutoTranslate_Microsoft',
2326
}],
2427
enableQuery: [{ _id: 'AutoTranslate_Enabled', value: true }],
2528
i18nLabel: 'AutoTranslate_ServiceProvider',
@@ -30,7 +33,7 @@ Meteor.startup(function() {
3033
type: 'string',
3134
group: 'Message',
3235
section: 'AutoTranslate_Google',
33-
public: true,
36+
public: false,
3437
i18nLabel: 'AutoTranslate_APIKey',
3538
enableQuery: [
3639
{
@@ -45,7 +48,7 @@ Meteor.startup(function() {
4548
type: 'string',
4649
group: 'Message',
4750
section: 'AutoTranslate_DeepL',
48-
public: true,
51+
public: false,
4952
i18nLabel: 'AutoTranslate_APIKey',
5053
enableQuery: [
5154
{
@@ -54,4 +57,18 @@ Meteor.startup(function() {
5457
_id: 'AutoTranslate_ServiceProvider', value: 'deepl-translate',
5558
}],
5659
});
60+
61+
settings.add('AutoTranslate_MicrosoftAPIKey', '', {
62+
type: 'string',
63+
group: 'Message',
64+
section: 'AutoTranslate_Microsoft',
65+
public: false,
66+
i18nLabel: 'AutoTranslate_Microsoft_API_Key',
67+
enableQuery: [
68+
{
69+
_id: 'AutoTranslate_Enabled', value: true,
70+
}, {
71+
_id: 'AutoTranslate_ServiceProvider', value: 'microsoft-translate',
72+
}],
73+
});
5774
});

app/models/server/models/Messages.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ export class Messages extends Base {
108108
return this.createWithTypeRoomIdMessageAndUser('r', roomId, roomName, user, extraData);
109109
}
110110

111-
addTranslations(messageId, translations) {
112-
const updateObj = {};
111+
addTranslations(messageId, translations, providerName) {
112+
const updateObj = { translationProvider: providerName };
113113
Object.keys(translations).forEach((key) => {
114114
const translation = translations[key];
115115
updateObj[`translations.${ key }`] = translation;

app/ui-message/client/message.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
{{#if showTranslated}}
4545
<span class="translated">
4646
<i class="icon-language {{#if msg.autoTranslateFetching}}loading{{/if}}" aria-label="{{_ "Translated"}}"></i>
47+
<span class="translation-provider">{{ translationProvider }}</span>
4748
</span>
4849
{{/if}}
4950
{{#if msg.sentByEmail}}

app/ui-message/client/message.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { t, roomTypes, getURL } from '../../utils';
1515
import { upsertMessage } from '../../ui-utils/client/lib/RoomHistoryManager';
1616
import './message.html';
1717
import './messageThread.html';
18+
import { AutoTranslate } from '../../autotranslate/client';
1819

1920
async function renderPdfToCanvas(canvasId, pdfLink) {
2021
const isSafari = /constructor/i.test(window.HTMLElement)
@@ -250,6 +251,11 @@ Template.message.helpers({
250251
return msg.autoTranslateFetching || (!!autoTranslate !== !!msg.autoTranslateShowInverse && msg.translations && msg.translations[settings.translateLanguage]);
251252
}
252253
},
254+
translationProvider() {
255+
const instance = Template.instance();
256+
const { translationProvider } = instance.data.msg;
257+
return translationProvider && AutoTranslate.providersMetadata[translationProvider].displayName;
258+
},
253259
edited() {
254260
const { msg } = this;
255261
return msg.editedAt && !MessageTypes.isSystemMessage(msg);

packages/rocketchat-i18n/i18n/en.i18n.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,8 @@
450450
"AutoTranslate_Enabled": "Enable Auto-Translate",
451451
"AutoTranslate_Enabled_Description": "Enabling auto-translation will allow people with the <code class=\"inline\">auto-translate</code> permission to have all messages automatically translated into their selected language. Fees may apply.",
452452
"AutoTranslate_Google": "Google",
453+
"AutoTranslate_Microsoft": "Microsoft",
454+
"AutoTranslate_Microsoft_API_Key": "Ocp-Apim-Subscription-Key",
453455
"AutoTranslate_ServiceProvider": "Service Provider",
454456
"Available": "Available",
455457
"Available_agents": "Available agents",

0 commit comments

Comments
 (0)