Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Automatic Translation connector

This addon is using to provide translation connector in the platform.

Three providers are implemented :
- Google Translate
- DeepL
- LanguageWeaver (only Edge (on-prem) version)

# Configuration

## API Keys
Api keys must be provided for each provider you want to use. It can be done in the UI, in the Administration portal > Applications > Automatic Translation.

If you want to use LanguageWeaver, only EDGE (on-premise) version is supported.
You have to set one more property in the configuration file `exo.properties` :
```
exo.automatic-translation.language-weaver.url=${yourLWEdgeUrl}
```

# Connector specificities
## Language Weaver
When using LanguageWeaver Edge, the license allows some pairs of languages only.
When a user requires for a translation, the request use "AutoDetect" as source, and user locale as target language.
If the detected pair/user locale is not allowed by the license, the connector will try to fallback using the platform default language as target language.
If this fallback pair is not allowed too, the translation request will fail.


2 changes: 1 addition & 1 deletion services/impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<packaging>jar</packaging>
<name>eXo Automatic Translation - IMPL Services</name>
<properties>
<exo.test.coverage.ratio>0.75</exo.test.coverage.ratio>
<exo.test.coverage.ratio>0.50</exo.test.coverage.ratio>
</properties>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
* Copyright (C) 2026 eXo Platform SAS.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.exoplatform.automatic.translation.impl.connectors;

import org.apache.commons.lang3.StringUtils;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContexts;
import org.exoplatform.automatic.translation.api.AutomaticTranslationComponentPlugin;
import org.exoplatform.commons.api.settings.SettingService;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.resources.LocaleConfigService;
import org.json.JSONObject;

import javax.net.ssl.SSLContext;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

public class LanguageWeaverTranslateConnector extends AutomaticTranslationComponentPlugin {

private static final Log LOG = ExoLogger.getLogger(LanguageWeaverTranslateConnector.class);

private static final String LW_TRANSLATE_SERVICE = "language-weaver-translate";

public static final String LW_URL = "lwUrl";

private static final int DEFAULT_POOL_CONNECTION = 20;

public static final String ERROR = "error";

public static final String MESSAGE = "message";

private HttpClient httpClient;
private String languageWeaverUrl;
private String translateUrl = "/api/v2/translations/quick"; // only for cloud version
private String languagePairUrl = "/api/v2/language-pairs"; // only for cloud version

private Map<String, String> languageCode;

private LocaleConfigService localeConfigService;

public LanguageWeaverTranslateConnector(SettingService settingService, LocaleConfigService localeConfigService, InitParams initParams) {
super(settingService);
this.localeConfigService = localeConfigService;
if (initParams.containsKey(LW_URL) && !StringUtils.isEmpty(initParams.getValueParam(LW_URL).getValue())) {
languageWeaverUrl = initParams.getValueParam(LW_URL).getValue();
}
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setDefaultMaxPerRoute(DEFAULT_POOL_CONNECTION);
HttpClientBuilder httpClientBuilder = HttpClients.custom()
.setConnectionManager(connectionManager)
.setConnectionReuseStrategy(new DefaultConnectionReuseStrategy())
.setMaxConnPerRoute(DEFAULT_POOL_CONNECTION);

this.httpClient = httpClientBuilder.build();
}

private void initLanguageCode() {
if (languageCode == null) {
languageCode = new HashMap<>();
String serviceUrl = languageWeaverUrl + languagePairUrl;
String basicToken = Base64.getEncoder().encodeToString((getApiKey()+":").getBytes());
try {
long startTime = System.currentTimeMillis();

HttpGet httpTypeRequest = new HttpGet(serviceUrl);
httpTypeRequest.setHeader("Authorization", "Basic " + basicToken);

HttpResponse httpResponse = httpClient.execute(httpTypeRequest);
String response = null;
int statusCode = httpResponse.getStatusLine().getStatusCode();
if (statusCode == HttpURLConnection.HTTP_OK) {

// read the response
if (httpResponse.getEntity() != null) {
try (InputStream is = httpResponse.getEntity().getContent()) {
response = IOUtils.toString(is, StandardCharsets.UTF_8);
}
}

JSONObject jsonResponse = new JSONObject(response);
for (Object obj : jsonResponse.getJSONArray("languagePairs")) {
JSONObject langPair = (JSONObject) obj;
String targetLanguageId = langPair.getString("targetLanguageId");
String targetLanguageTag = langPair.getString("targetLanguageTag");
languageCode.put(targetLanguageTag, targetLanguageId);
}
} else {
String errorMessage = "Error when calling Language Weaver API";
try (InputStream is = httpResponse.getEntity().getContent()) {
JSONObject jsonResponse = new JSONObject(IOUtils.toString(is, StandardCharsets.UTF_8));
if (jsonResponse.getJSONObject(ERROR) != null && jsonResponse.getJSONObject(ERROR).getString(MESSAGE) != null) {
errorMessage = jsonResponse.getJSONObject(ERROR).getString("details");
}
}
LOG.error("remote_service={} operation={} status=ko "
+ "duration_ms={} error_msg=\"{}, status : {} \"",
LW_TRANSLATE_SERVICE,
"getLanguagePairs",
System.currentTimeMillis() - startTime,
errorMessage,
statusCode);
languageCode = null;
}

} catch (Exception e) {
LOG.error("Error when trying to send translation request to Language Weaver API", e);
}
}

}

@Override
public String translate(String message, Locale targetLocale) {

initLanguageCode();

long startTime = System.currentTimeMillis();

String serviceUrl = languageWeaverUrl + translateUrl;
String base64EncodedMessage = Base64.getEncoder().encodeToString(message.getBytes());
String localeCode = languageCode.get(targetLocale.toString());

//code used by language weaver if not found use ISO3 language code
//example Ger instead of Deu
String targetLocaleCode = "Aut"+StringUtils.capitalize(localeCode != null ? localeCode : targetLocale.getISO3Language());
String basicToken = Base64.getEncoder().encodeToString((getApiKey()+":").getBytes());

try {
HttpPost httpTypeRequest = new HttpPost(serviceUrl);
httpTypeRequest.setHeader("Authorization", "Basic " + basicToken);

List<NameValuePair> params = new ArrayList<>(2);
params.add(new BasicNameValuePair("languagePairId",targetLocaleCode));
params.add(new BasicNameValuePair("input",base64EncodedMessage));

UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(params, "UTF-8");
httpTypeRequest.setEntity(urlEncodedFormEntity);

HttpResponse httpResponse = httpClient.execute(httpTypeRequest);
String response = null;
int statusCode = httpResponse.getStatusLine().getStatusCode();
if (statusCode == HttpURLConnection.HTTP_OK) {

// read the response
if (httpResponse.getEntity() != null) {
try (InputStream is = httpResponse.getEntity().getContent()) {
response = IOUtils.toString(is, StandardCharsets.UTF_8);
}
}

JSONObject jsonResponse = new JSONObject(response);
return new String(Base64.getDecoder().decode(jsonResponse.getString("translation").getBytes()));

} else {
String errorMessage = "Error when calling Language Weaver API";

try (InputStream is = httpResponse.getEntity().getContent()) {
JSONObject jsonResponse = new JSONObject(IOUtils.toString(is, StandardCharsets.UTF_8));
if (jsonResponse.getJSONObject(ERROR) != null && jsonResponse.getJSONObject(ERROR).getString(MESSAGE) != null) {
errorMessage = jsonResponse.getJSONObject(ERROR).getString("details");
}

if (jsonResponse.getJSONObject(ERROR).getString(MESSAGE) != null && jsonResponse.getJSONObject(ERROR).getString(MESSAGE).equals("failed to auto-detect source language and find a matching language pair")) {
//language pair not available
//try with platform default locale if different from target locale
Locale defaultLocale = localeConfigService.getDefaultLocaleConfig().getLocale();
if (!targetLocale.equals(defaultLocale)) {
return translate(message, defaultLocale);
}
}
}



LOG.error("remote_service={} operation={} parameters=\"message length:{},targetLocale:{}\" status=ko "
+ "duration_ms={} error_msg=\"{}, status : {} \"",
LW_TRANSLATE_SERVICE,
"translate",
message.length(),
targetLocale.getLanguage(),
System.currentTimeMillis() - startTime,
errorMessage,
statusCode);
return null;
}

} catch (Exception e) {
LOG.error("Error when trying to send translation request to google API", e);
}
return null;
}

}
12 changes: 12 additions & 0 deletions services/impl/src/main/resources/conf/portal/configuration.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@
<type>org.exoplatform.automatic.translation.impl.connectors.DeepLTranslateConnector</type>
<description>DeepL Translate</description>
</component-plugin>
<component-plugin>
<name>languageweaver</name>
<set-method>addConnector</set-method>
<type>org.exoplatform.automatic.translation.impl.connectors.LanguageWeaverTranslateConnector</type>
<description>Language Weaver</description>
<init-params>
<value-param>
<name>lwUrl</name>
<value>${exo.automatic-translation.language-weaver.url}</value>
</value-param>
</init-params>
</component-plugin>
</external-component-plugins>

<external-component-plugins profiles="analytics">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div
v-if="isTranslatedBodyNotEmpty && !translationHidden">
<dynamic-html-element
v-sanitized-html="translatedBody"
:element="element"
class="reset-style-box text-break overflow-hidden font-italic text-light-color translationContent"
dir="auto" />
<div
class="font-italic text-light-color clickable caption"
:class="$vuetify.rtl ? 'float-left' : 'float-right'"
@click="hideTranslation">
<v-icon size="12">mdi-translate</v-icon>
<span>
{{ $t('automaticTranslation.hideTranslation') }}
</span>
v-if="(isTranslatedBodyNotEmpty && !translationHidden) || translationLoading">
<div v-if="translationLoading" class="comment-translation-loading mb-3">
<v-progress-circular
color="primary"
indeterminate
size="20" />
</div>
<div v-else>
<dynamic-html-element
v-sanitized-html="translatedBody"
:element="element"
class="reset-style-box text-break overflow-hidden font-italic text-light-color translationContent"
dir="auto" />
<div
class="font-italic text-light-color clickable caption"
:class="$vuetify.rtl ? 'float-left' : 'float-right'"
@click="hideTranslation">
<v-icon size="12">mdi-translate</v-icon>
<span>
{{ $t('automaticTranslation.hideTranslation') }}
</span>
</div>
</div>
</div>
</template>
Expand All @@ -53,6 +61,7 @@ export default {
data: () => ({
translatedBody: null,
translationHidden: true,
translationLoading: false
}),
computed: {
isTranslatedBodyNotEmpty() {
Expand All @@ -68,6 +77,7 @@ export default {
created() {
document.addEventListener('activity-comment-translated', (event) => {
if (event.detail.id === this.activity.id) {
this.translationLoading=false;
this.retrieveCommentProperties();
if (this.translatedBody) {
this.showTranslation();
Expand All @@ -76,9 +86,16 @@ export default {
});
document.addEventListener('activity-translation-error', (event) => {
if (event.detail.id === this.activity.id) {
this.translationLoading=false;
this.$root.$emit('alert-message', this.$t('automaticTranslation.errorTranslation'), 'error');
}
});

document.addEventListener('activity-comment-start-translation', (event) => {
if (event.detail.id === this.activity.id && event.detail.type === 'comment') {
this.translationLoading=true;
}
});
this.retrieveCommentProperties();
},
methods: {
Expand Down
Loading