Code development platform for open source projects from the European Union institutions :large_blue_circle: EU Login authentication by SMS has been phased out. To see alternatives please check here

Skip to content
Snippets Groups Projects
Commit 7a251b8b authored by Davis Ragels's avatar Davis Ragels
Browse files

EMW Drupal plugin Beta version

parent 8edc7be9
Branches
Tags
No related merge requests found
Showing
with 2007 additions and 34 deletions
...@@ -16,13 +16,13 @@ For machine-translation this module uses either eTranslation or any other MT int ...@@ -16,13 +16,13 @@ For machine-translation this module uses either eTranslation or any other MT int
![Module architecture](emw-drupal-architecture.png) ![Module architecture](emw-drupal-architecture.png)
## Setup ## Setup
1. Copy `emw_translator` folder to Drupal website's `modules/custom` folder or create ZIP archive and install it via `Extend->Add new module` admin page; 1. Copy `emw_translator` folder to Drupal website's `modules/custom` folder or create ZIP archive and install it via `Extend->Add new module` admin page.
2. Clear cache by using `Configuration->Performance->Clear all caches` button or by running `drush cr` command 2. Clear cache by using `Configuration->Performance->Clear all caches` button or by running `drush cr` command.
3. Go to `Extend->List` section and under `Multilingual` section select 'EMW translator' module, click `Install`. If asked, confirm installation of module dependencies. 3. Go to `Extend->List` section and under `Multilingual` section select 'EMW translator' module, click `Install`. If asked, confirm installation of module dependencies.
4. Go to `Configuration->Languages` and add translation languages; 4. Go to `Configuration->Languages` and add translation languages.
5. Go to `Configuration->Content language and translation` and enable content translation for all website content elements you want to translate automatically (e.g. Comment, Content, Taxonomy term) by marking them as 'Translatable' and saving settings. 5. Go to `Configuration->Content language and translation` and enable content translation for all website content elements you want to translate automatically (e.g. Comment, Content, Taxonomy term) by marking them as 'Translatable' and saving settings.
6. Go to `Configuration->EMW translator settings` and paste MT API key into `WTP access key` text field, click `Save` 6. Go to `Configuration->EMW translator settings` and choose MT engine. For eTranslation enter eTranslation API credentials; for Other provider - specify provider's generic MT API URL and API key. Click `Save`.
7. Translate all exisiting and translatable elements by clicking `Translate` 7. Translate all exisiting and translatable elements by clicking `Translate`.
## Screenshots ## Screenshots
......
# Package plugin as Zip archive
variables:
- name: PackageName
value: emw_translator
trigger:
- master
pool:
name: default
steps:
- task: CopyFiles@2
inputs:
SourceFolder: '$(Build.SourcesDirectory)/emw_translator'
Contents: |
**
!.git/**
!azure-pipelines.yml
!readme.md
!emw-translator.png
!emw-drupal-architecture.png
TargetFolder: '$(Build.BinariesDirectory)/$(PackageName)'
CleanTargetFolder: true
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: '$(Build.BinariesDirectory)/$(PackageName)'
includeRootFolder: true
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(PackageName).zip'
replaceExistingArchive: true
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'plugin'
publishLocation: 'FilePath'
TargetPath: '\\tilde.lv\ad\Builds\MT\$(Build.DefinitionName)\$(Build.BuildNumber)'
.ajax-progress-bar {
width: auto!important;
}
.ajax-progress {
display: block!important;
}
pretranslation_tab.style:
version: 1.x
css:
theme:
css/pretranslation_tab.css: {}
pretranslation_tab.js:
version: 1.x
js:
js/ajax_handler.js: {}
dependencies:
- core/jquery
\ No newline at end of file
emw_translator.settings.tab:
route_name: emw_translator.settings
base_route: emw_translator.settings
title: 'Settings'
weight: 10
emw_translator.translation.tab:
route_name: emw_translator.settings.translation
base_route: emw_translator.settings
title: 'Translation'
weight: 20
\ No newline at end of file
<<<<<<< HEAD
<?php <?php
/*** /***
...@@ -29,4 +30,40 @@ function emw_translator_entity_load($e) { ...@@ -29,4 +30,40 @@ function emw_translator_entity_load($e) {
$translation_manager = \Drupal::service('emw_translator.translation_manager'); $translation_manager = \Drupal::service('emw_translator.translation_manager');
$translation_manager->entity_update_all_translations($entity); $translation_manager->entity_update_all_translations($entity);
} }
=======
<?php
/***
* Hooks for updating translations on entity insert/update.
*/
function emw_translator_entity_insert($entity) {
mark_entity_for_translation($entity);
}
/**
* Schedules entity translation update if any translatable value in original language has changed.
*
* @param [type] $entity Entity that was updated
* @return void
*/
function emw_translator_entity_update($entity) {
mark_entity_for_translation($entity);
}
function emw_translator_entity_load($e) {
$entity = \Drupal::state()->get("emw_translator_update_entity");
if ($entity) {
\Drupal::state()->delete("emw_translator_update_entity");
$translation_manager = \Drupal::service('emw_translator.translation_manager');
$translation_manager->entity_update_all_translations($entity);
}
}
function mark_entity_for_translation($entity) {
if ( method_exists($entity, 'isTranslatable') && $entity->isTranslatable() && array_key_exists( 'default_langcode', $entity->toArray()) && $entity->default_langcode &&
$entity->default_langcode[0]->value && \Drupal::service('emw_translator.translation_manager')->entity_translatable_content_changed( $entity ) ) {
\Drupal::state()->set("emw_translator_update_entity", $entity);
}
>>>>>>> main
} }
\ No newline at end of file
...@@ -2,6 +2,49 @@ emw_translator.settings: ...@@ -2,6 +2,49 @@ emw_translator.settings:
path: '/admin/config/regional/emw-translator' path: '/admin/config/regional/emw-translator'
defaults: defaults:
_form: '\Drupal\emw_translator\Form\EmwTranslatorSettingsForm' _form: '\Drupal\emw_translator\Form\EmwTranslatorSettingsForm'
<<<<<<< HEAD
_title: 'EMW Translator Settings' _title: 'EMW Translator Settings'
requirements: requirements:
_permission: 'administer site configuration' _permission: 'administer site configuration'
=======
_title: 'EMW Translator: Settings'
requirements:
_permission: 'administer site configuration'
emw_translator.settings.translation:
path: '/admin/config/regional/emw-translator/translate'
defaults:
_form: '\Drupal\emw_translator\Form\PretranslationForm'
_title: 'EMW Translator: Translation'
requirements:
_permission: 'administer site configuration'
emw_translator.etranslation.ok:
path: /etranslation/translation/{id}
defaults:
_controller: Drupal\emw_translator\Controller\EtranslationCallbackController::onResponse
requirements:
_permission: 'access content'
emw_translator.etranslation.err:
path: /etranslation/error/{id}
defaults:
_controller: Drupal\emw_translator\Controller\EtranslationCallbackController::onError
requirements:
_permission: 'access content'
emw_translator.pretranslation.progress:
path: /emw_pretranslation_progress
defaults:
_controller: Drupal\emw_translator\Controller\PretranslationProgressController::getTranslationProgress
requirements:
_permission: 'administer site configuration'
emw_translator.pretranslation.ajax_error:
path: /emw_pretranslation_set_ajax_error
defaults:
_controller: Drupal\emw_translator\Controller\PretranslationProgressController::ajaxErrorHandler
requirements:
_permission: 'administer site configuration'
emw_translator.pretranslation.cancel:
path: /emw_pretranslation_cancel
defaults:
_controller: Drupal\emw_translator\Controller\PretranslationProgressController::cancelTranslation
requirements:
_permission: 'administer site configuration'
>>>>>>> main
<<<<<<< HEAD
services: services:
emw_translator.translation_service: emw_translator.translation_service:
class: Drupal\emw_translator\TranslationService class: Drupal\emw_translator\TranslationService
arguments: ['@http_client_factory'] arguments: ['@http_client_factory']
emw_translator.translation_manager: emw_translator.translation_manager:
class: Drupal\emw_translator\TranslationManager class: Drupal\emw_translator\TranslationManager
=======
services:
emw_translator.translation_service.etranslation:
class: Drupal\emw_translator\translation_engines\etranslation\EtranslationService
arguments: ['@http_client_factory']
emw_translator.translation_service.generic:
class: Drupal\emw_translator\translation_engines\generic\GenericMTService
arguments: ['@http_client_factory']
emw_translator.translation_manager:
class: Drupal\emw_translator\TranslationManager
>>>>>>> main
arguments: ['@config.factory'] arguments: ['@config.factory']
\ No newline at end of file
(function ($, Drupal) {
'use strict';
var cancelled = false;
var ajaxError = false;
var success = false;
$(window).on('beforeunload', function () {
if ($.active && !ajaxError && !success) {
cancelled = true;
$.get(Drupal.url('emw_pretranslation_cancel'), function (data, textStatus, jqXHR) {});
}
});
$(document).ajaxError(function (event, xhr, settings, thrownError) {
if (settings.extraData && settings.extraData._triggering_element_name && !cancelled && !success) {
ajaxError = true;
$.get(Drupal.url('emw_pretranslation_set_ajax_error'), function (data, textStatus, jqXHR) {
location.reload();
});
}
});
$(document).ajaxComplete(function (event, xhr, settings) {
if (settings.extraData && settings.extraData._drupal_ajax && settings.extraData._triggering_element_value ) {
success = true;
// reload page on translation completion to show notifications & update table.
location.reload();
}
});
})(jQuery, Drupal);
\ No newline at end of file
<?php
namespace Drupal\emw_translator\Controller;
use Drupal\emw_translator\translation_engines\etranslation\EtranslationUtils;
use Drupal\Component\Utility\Html;
use Symfony\Component\HttpFoundation\Response;
class EtranslationCallbackController {
public function onResponse() {
$request_id = Html::escape( $_REQUEST['request-id'] );
$translated_content = base64_decode( file_get_contents( 'php://input' ) );
$key = EtranslationUtils::get_cache_key( $request_id );
\Drupal::logger( 'emw_translator' )->info( "Received response from eTranslation ($request_id)" );
if ( \Drupal::cache()->get( $key ) && \Drupal::cache()->get( $key )->data === EtranslationUtils::$timeout_value ) {
\Drupal::logger( 'emw_translator' )->info( "Received late translation ($request_id)" );
$entity_id = Html::escape( $_REQUEST['external-reference'] );
if ( $entity_id ) {
$entity = $this->loadEntityById( $entity_id );
if ( $entity ) {
$this->processEntityTranslation( $entity, $translated_content, Html::escape( $_REQUEST['target-language'] ) );
}
}
} else {
\Drupal::cache()->set( $key, $translated_content );
}
return new Response();
}
public function onError() {
$request_id = Html::escape( $_REQUEST['request-id'] );
\Drupal::logger( 'emw_translator' )->error( "eTranslation response error ($request_id)" );
\Drupal::cache()->set( EtranslationUtils::get_cache_key( $request_id ), EtranslationUtils::$error_value );
return new Response();
}
private function processEntityTranslation( $entity, $translated_content, $target_language ) {
$source_langcode = $entity->language()->getId();
$target_langcode = $this->getLangcodeByLanguage( $target_language );
$translation_manager = \Drupal::service( 'emw_translator.translation_manager' );
$translation_service = \Drupal::service( 'emw_translator.translation_service.etranslation' );
$xml_translations = $translation_service->etranslation_response_to_translation_array( $translated_content );
$translatable_fields = $translation_manager->get_translatable_fields( $entity );
$original_values = $translation_manager->extract_translatable_values( $entity, $translatable_fields );
if ( count( $original_values ) === count( $xml_translations ) ) {
$translations = $translation_service->decode_xml_translations( $original_values, $xml_translations );
$translation_manager->translate_entity( $entity, $source_langcode, $target_langcode, $translations );
}
}
private function loadEntityById( $id ) {
$entity_type_manager = \Drupal::service( 'entity_type.manager' );
foreach ( $entity_type_manager->getDefinitions() as $entity_type_id => $entity_type ) {
if ( $entity_type->entityClassImplements( '\Drupal\Core\Entity\EntityInterface' ) ) {
$storage = $entity_type_manager->getStorage( $entity_type_id );
$entity = $storage->load( $id );
if ( $entity ) {
return $entity;
}
}
}
\Drupal::logger( 'emw_translator' )->error( "Could not load entity by ID: $id" );
return null;
}
private function getLangcodeByLanguage( $language ) {
$languages = \Drupal::languageManager()->getLanguages();
$language_lowercase = strtolower( $language );
foreach ( $languages as $langcode => $value ) {
if ( str_starts_with( $langcode, $language_lowercase ) ) {
return $langcode;
}
}
\Drupal::logger( 'emw_translator' )->error( "Could not find langcode for language $language_lowercase" );
return null;
}
}
<?php
namespace Drupal\emw_translator\Controller;
use Symfony\Component\HttpFoundation\Response;
class PretranslationProgressController {
public static $progress_key = 'emw_translation_progress';
public function getTranslationProgress() {
$response = null;
$value = \Drupal::cache()->get( self::$progress_key );
if ( $value && $value->data ) {
$response = $value->data;
} else {
$empty_response = new \stdClass();
$empty_response->message = '';
$empty_response->percentage = 0;
$response = $empty_response;
}
return new Response( json_encode( $response ) );
}
public function cancelTranslation() {
\Drupal::logger( 'emw_translator' )->error( 'Client page was closed, cancelling pre-translation...' );
self::updateProgress( null, null, true );
return new Response();
}
public function ajaxErrorHandler() {
\Drupal::messenger()->addError( t( 'Backend translation process was aborted! Check logs for errors or try increasing website\'s PHP max_execution_time.' ) );
return new Response();
}
public static function updateTranslationMessage( $message ) {
self::updateProgress( null, $message );
}
public static function updateTranslationPercentage( $percentage ) {
self::updateProgress( $percentage );
}
public static function clearTranslationProgress() {
\Drupal::cache()->delete( self::$progress_key );
}
public static function getValue() {
return \Drupal::cache()->get( self::$progress_key );
}
private static function updateProgress( $percentage = null, $message = null, $aborted = false ) {
$value = \Drupal::cache()->get( self::$progress_key );
$update = null;
if ( $value && $value->data ) {
$update = $value->data;
} else {
$update = new \stdClass();
$update->message = '';
$update->percentage = 0;
$update->aborted = false;
$update->request_id = uniqid();
}
if ( $percentage !== null ) {
$update->percentage = round( $percentage );
}
if ( $message ) {
$update->message = $message;
}
if ( $aborted ) {
$update->aborted = $aborted;
}
\Drupal::cache()->set( self::$progress_key, $update );
}
}
<<<<<<< HEAD
<?php <?php
namespace Drupal\emw_translator\Form; namespace Drupal\emw_translator\Form;
...@@ -261,3 +262,221 @@ class EmwTranslatorSettingsForm extends FormBase { ...@@ -261,3 +262,221 @@ class EmwTranslatorSettingsForm extends FormBase {
return false; return false;
} }
} }
=======
<?php
namespace Drupal\emw_translator\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\emw_translator\translation_engines\etranslation\EtranslationService;
use Drupal\emw_translator\translation_engines\etranslation\EtranslationUtils;
use Drupal\emw_translator\translation_engines\generic\GenericMTService;
class EmwTranslatorSettingsForm extends FormBase {
private $etranslation_engine_id = 'etranslation';
private $generic_mt_engine_id = 'generic';
public function getFormId() {
return 'EmwTranslatorSettingsForm';
}
public function buildForm( array $form, FormStateInterface $form_state ) {
$config = $this->config( 'emw_translator.settings' );
$selected_mt_engine = $config->get( 'mt_engine' );
$form['mt_engine'] = array(
'#type' => 'radios',
'#title' => $this->t( 'Machine-translation engine' ),
'#options' => array(
$this->etranslation_engine_id => $this->t( 'eTranslation' ),
$this->generic_mt_engine_id => $this->t( 'Other' ),
),
'#default_value' => $selected_mt_engine ? $selected_mt_engine : $this->etranslation_engine_id,
);
$condition_etranslation = array(
':input[name="mt_engine"]' => array( 'value' => $this->etranslation_engine_id ),
);
$condition_generic_mt = array(
':input[name="mt_engine"]' => array( 'value' => $this->generic_mt_engine_id ),
);
$form['etranslation_appname'] = array(
'#type' => 'textfield',
'#title' => $this->t( 'eTranslation Application Name' ),
'#default_value' => $config->get( 'etranslation_appname' ),
'#states' => array(
'visible' => $condition_etranslation,
'required' => $condition_etranslation,
'disabled' => $condition_generic_mt,
),
);
$form['etranslation_password'] = array(
'#type' => 'password',
'#title' => $this->t( 'eTranslation Password' ),
'#attributes' => array( 'value' => $config->get( 'etranslation_password' ) ),
'#states' => array(
'visible' => $condition_etranslation,
'required' => $condition_etranslation,
'disabled' => $condition_generic_mt,
),
);
$form['etranslation_advanced'] = array(
'#type' => 'details',
'#title' => $this->t( 'Advanced' ),
'#open' => false,
'#tree' => true,
'#states' => array(
'visible' => $condition_etranslation,
'required' => $condition_etranslation,
'disabled' => $condition_generic_mt,
),
'etranslation_timeout' => array(
'#type' => 'number',
'#title' => $this->t( 'eTranslation Timeout' ),
'#description' => $this->t( 'Max time in seconds to wait for eTranslation to translate new/updated entities on save (per language). Infinite if zero. Does not affect pre-translation.' ),
'#step' => '0.1',
'#default_value' => $config->get( 'etranslation_timeout' ) ? $config->get( 'etranslation_timeout' ) : EtranslationUtils::$etranslation_timeout_default,
),
'etranslation_chars_per_request' => array(
'#type' => 'number',
'#title' => $this->t( 'Max characters per single request' ),
'#description' => $this->t( 'Affects how many/big requests are sent to MT provider, which can influence translation speed.' ),
'#step' => '100',
'#default_value' => $config->get( 'etranslation_chars_per_request' ) ? $config->get( 'etranslation_chars_per_request' ) : EtranslationService::$default_chars_per_request,
),
);
$form['mt_api_url'] = array(
'#type' => 'textfield',
'#title' => $this->t( 'MT API URL' ),
'#default_value' => $config->get( 'mt_api_url' ),
'#states' => array(
'visible' => $condition_generic_mt,
'required' => $condition_generic_mt,
'disabled' => $condition_etranslation,
),
);
$form['mt_api_key'] = array(
'#type' => 'textfield',
'#title' => $this->t( 'MT API key' ),
'#default_value' => $config->get( 'mt_api_key' ),
'#states' => array(
'visible' => $condition_generic_mt,
'required' => $condition_generic_mt,
'disabled' => $condition_etranslation,
),
);
$form['mt_advanced'] = array(
'#type' => 'details',
'#title' => $this->t( 'Advanced' ),
'#open' => false,
'#tree' => true,
'#states' => array(
'visible' => $condition_generic_mt,
'required' => $condition_generic_mt,
'disabled' => $condition_etranslation,
),
'mt_chars_per_request' => array(
'#type' => 'number',
'#title' => $this->t( 'Max characters per single request' ),
'#description' => $this->t( 'Affects how many/big requests are sent to MT provider, which can influence translation speed.' ),
'#step' => '100',
'#default_value' => $config->get( 'mt_chars_per_request' ) ? $config->get( 'mt_chars_per_request' ) : GenericMTService::$default_chars_per_request,
),
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => $this->t( 'Save' ),
);
if ( $this->machineTranslationConfigured() ) {
$form['test'] = array(
'#type' => 'submit',
'#value' => t( 'Test connection' ),
'#submit' => array( '::checkMachineTranslatonConnection' ),
);
}
return $form;
}
public function submitForm( array &$form, FormStateInterface $form_state ) {
$config = \Drupal::getContainer()->get( 'config.factory' )->getEditable( 'emw_translator.settings' );
$mt_engine = $form_state->getValue( 'mt_engine' );
$config->set( 'mt_engine', $mt_engine );
if ( $this->etranslation_engine_id === $mt_engine ) {
$config->set( 'etranslation_appname', $form_state->getValue( 'etranslation_appname' ) );
if ( $config->get( 'etranslation_password' ) !== $form_state->getValue( 'etranslation_password' ) ) {
$encrypted_pwd = EtranslationUtils::encrypt_password( $form_state->getValue( 'etranslation_password' ) );
$config->set( 'etranslation_password', $encrypted_pwd );
}
$config->set( 'etranslation_timeout', $form_state->getValue( 'etranslation_advanced' )['etranslation_timeout'] );
$config->set( 'etranslation_chars_per_request', $form_state->getValue( 'etranslation_advanced' )['etranslation_chars_per_request'] );
} elseif ( $this->generic_mt_engine_id === $mt_engine ) {
$config->set( 'mt_api_url', $form_state->getValue( 'mt_api_url' ) );
$config->set( 'mt_api_key', $form_state->getValue( 'mt_api_key' ) );
$config->set( 'mt_chars_per_request', $form_state->getValue( 'mt_advanced' )['mt_chars_per_request'] );
}
$config->save();
}
public function validateForm( array &$form, FormStateInterface $form_state ) {
$engine = $form_state->getValue( 'mt_engine' );
switch ( $engine ) {
case $this->etranslation_engine_id:
return;
case $this->generic_mt_engine_id:
$url_key = 'mt_api_url';
$mt_api_url = $form_state->getValue( $url_key );
if ( ! filter_var( $mt_api_url, FILTER_VALIDATE_URL ) ) {
$form_state->setErrorByName( $url_key, $this->t( 'Please enter a valid URL' ) );
}
break;
default:
\Drupal::logger( 'emw_translator' )->error( "Invalid translation engine: '$engine'" );
return;
}
}
private function machineTranslationConfigured() {
$config = $this->config( 'emw_translator.settings' );
$engine = $config->get( 'mt_engine' );
switch ( $engine ) {
case $this->etranslation_engine_id:
return $config->get( 'etranslation_appname' ) && $config->get( 'etranslation_password' );
case $this->generic_mt_engine_id:
return $config->get( 'mt_api_url' ) && $config->get( 'mt_api_key' );
default:
return false;
}
}
function checkMachineTranslatonConnection() {
\Drupal::messenger()->deleteAll();
$config = $this->config( 'emw_translator.settings' );
$engine = $config->get( 'mt_engine' );
$translation_service = \Drupal::service( 'emw_translator.translation_service.' . $engine );
$connection_successful = $translation_service->test_connection();
if ( $connection_successful ) {
\Drupal::messenger()->addStatus( t( 'Machine-translation connection successful' ) );
return true;
} else {
\Drupal::messenger()->addError( t( 'There was a problem connecting to machine-translation provider' ) );
return false;
}
}
}
>>>>>>> main
<?php
namespace Drupal\emw_translator\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Url;
use Drupal\emw_translator\Controller\PretranslationProgressController;
use Drupal\emw_translator\Model\ProcessingResponse;
class PretranslationForm extends FormBase {
public function getFormId() {
return 'PretranslationForm';
}
public function buildForm( array $form, FormStateInterface $form_state ) {
$language_name_list = array_map(
function( $l ) {
return t( $l->getName() );
},
\Drupal::languageManager()->getLanguages()
);
$form['description'] = array(
'#type' => 'markup',
'#markup' => '<div class="form-item__description"><strong>' . t( 'Selected languages' ) . ': </strong>' . implode( ', ', $language_name_list ) . '</div>',
);
$form['nodes'] = array(
'#type' => 'checkbox',
'#title' => t( 'Nodes' ),
'#default_value' => $this->checkContentTypeTranslationEnabled( 'node', array( 'article', 'page' ) ),
'#description' => t( 'Translate untranslated node type elements (articles, pages)' ),
);
$form['comments'] = array(
'#type' => 'checkbox',
'#title' => t( 'Comments' ),
'#default_value' => $this->checkContentTypeTranslationEnabled( 'comment', array( 'comment' ) ),
'#description' => t( 'Translate untranslated comments' ),
);
$form['tags'] = array(
'#type' => 'checkbox',
'#title' => t( 'Tags' ),
'#default_value' => $this->checkContentTypeTranslationEnabled( 'taxonomy_term', array( 'tags' ) ),
'#description' => t( 'Translate untranslated taxonomy terms (tags)' ),
);
$form['configuration'] = array(
'#type' => 'checkbox',
'#title' => t( 'Configuration' ),
'#default_value' => true,
'#description' => t( 'Translate untranslated configuration strings (site name, role names, blocks, etc.)' ),
);
$form['ui'] = array(
'#type' => 'checkbox',
'#title' => t( 'UI elements' ),
'#default_value' => true,
'#description' => t( 'Translate untranslated UI elements' ),
);
$form['translate'] = array(
'#type' => 'submit',
'#value' => t( 'Translate' ),
'#ajax' => array(
'callback' => '::translateItems',
'event' => 'click',
'progress' => array(
'type' => 'bar',
'message' => 'Translating...',
'url' => Url::fromRoute( 'emw_translator.pretranslation.progress' ),
'interval' => '500',
),
),
);
$form['advanced'] = array(
'#type' => 'details',
'#title' => t( 'Advanced options' ),
'#open' => false,
'#tree' => true,
'progress' => array(
'#type' => 'markup',
'#markup' => '<strong>' . t( 'Translation progress' ) . '</strong><br/>' . $this->getTranslationProgressMarkup(),
),
'delete' => array(
'#type' => 'submit',
'#value' => t( 'Delete translations' ),
'#submit' => array( '::deleteItems' ),
'#attributes' => array( 'title' => t( 'Delete translations of selected types' ) ),
),
);
$form['#attached']['library'][] = 'core/drupal.ajax';
$form['#attached']['library'][] = 'emw_translator/pretranslation_tab.style';
$form['#attached']['library'][] = 'emw_translator/pretranslation_tab.js';
return $form;
}
public function submitForm( array &$form, FormStateInterface $form_state ) {}
public function checkContentTypeTranslationEnabled( $entity_type_id, $bundle_names ) {
$content_tranlation_manager = \Drupal::service( 'content_translation.manager' );
foreach ( $bundle_names as $bundle_name ) {
if ( $content_tranlation_manager->isEnabled( $entity_type_id, $bundle_name ) ) {
return true;
}
}
return false;
}
public function translateItems( array &$form, FormStateInterface $form_state ) {
$response = new AjaxResponse();
\Drupal::messenger()->deleteAll();
PretranslationProgressController::clearTranslationProgress();
PretranslationProgressController::updateTranslationMessage( 'Translation started' );
PretranslationProgressController::updateTranslationPercentage( 0 );
if ( $form_state->getValue( array( 'tags' ) ) ) {
$this->translateEntities( 'taxonomy_term' );
}
if ( $form_state->getValue( array( 'nodes' ) ) ) {
$this->translateEntities( 'node' );
}
if ( $form_state->getValue( array( 'comments' ) ) ) {
$this->translateEntities( 'comment' );
}
if ( $form_state->getValue( array( 'configuration' ) ) ) {
$this->translateConfigurationStrings();
}
if ( $form_state->getValue( array( 'ui' ) ) ) {
$this->translateUserInterfaceElements();
}
drupal_flush_all_caches();
\Drupal::logger( 'emw_translator' )->info( 'Website translation finished' );
return $response;
}
public function translateEntities( $entity_type ) {
\Drupal::logger( 'emw_translator' )->debug( "Translating entities of type: '$entity_type'" );
$entities = \Drupal::entityTypeManager()
->getStorage( $entity_type )
->loadMultiple();
$translation_manager = \Drupal::service( 'emw_translator.translation_manager' );
$response = $translation_manager->translate_entities( $entities, $entity_type );
$this->register_translation_results( $response, $entity_type );
}
public function translateUserInterfaceElements() {
\Drupal::logger( 'emw_translator' )->debug( 'Translating UI elements' );
$translation_manager = \Drupal::service( 'emw_translator.translation_manager' );
$source_language = $translation_manager->get_default_language_code();
$langcodes = \Drupal::languageManager()->getLanguages();
$langcodes_list = array_keys( $langcodes );
$results = array();
foreach ( $langcodes_list as $langcode ) {
if ( $langcode === $source_language ) {
continue;
}
$untranslated_strings = $translation_manager->get_untranslated_ui_strings( $langcode );
$results[] = $translation_manager->translate_ui_strings( $untranslated_strings, $source_language, $langcode );
}
$response = min( $results );
$this->register_translation_results( $response, 'UI' );
}
public function translateConfigurationStrings() {
\Drupal::logger( 'emw_translator' )->debug( 'Translating configurations strings' );
PretranslationProgressController::updateTranslationPercentage( 0 );
$translation_manager = \Drupal::service( 'emw_translator.translation_manager' );
$source_language = $translation_manager->get_default_language_code();
$langcodes = \Drupal::languageManager()->getLanguages();
$langcodes_list = array_keys( $langcodes );
$results = array();
foreach ( $langcodes_list as $langcode ) {
if ( $langcode === $source_language ) {
continue;
}
$strings = $translation_manager->get_untranslated_config_strings( $langcode );
$results[] = $translation_manager->translate_config_strings( $strings, $langcode );
}
$response = min( $results );
$this->register_translation_results( $response, 'configuration' );
}
public function deleteItems( array &$form, FormStateInterface $form_state ) {
$translation_manager = \Drupal::service( 'emw_translator.translation_manager' );
if ( $form_state->getValue( array( 'tags' ) ) ) {
$this->deleteEntityTranslations( 'taxonomy_term' );
}
if ( $form_state->getValue( array( 'nodes' ) ) ) {
$this->deleteEntityTranslations( 'node' );
}
if ( $form_state->getValue( array( 'comments' ) ) ) {
$this->deleteEntityTranslations( 'comment' );
}
if ( $form_state->getValue( array( 'configuration' ) ) ) {
\Drupal::logger( 'emw_translator' )->debug( 'Deleting configuration translations' );
$response = $translation_manager->delete_config_translations();
$this->register_deletion_results( $response, 'configuration' );
}
if ( $form_state->getValue( array( 'ui' ) ) ) {
\Drupal::logger( 'emw_translator' )->debug( 'Deleting UI string translations' );
$response = $translation_manager->delete_ui_translations();
$this->register_deletion_results( $response, 'UI' );
}
}
public function deleteEntityTranslations( $entity_type ) {
\Drupal::logger( 'emw_translator' )->debug( "Deleting entity translations of type: '$entity_type'" );
$entities = \Drupal::entityTypeManager()
->getStorage( $entity_type )
->loadMultiple();
$translation_manager = \Drupal::service( 'emw_translator.translation_manager' );
$response = $translation_manager->delete_translations_of_entities( $entities );
$this->register_deletion_results( $response, $entity_type );
}
public function getTranslationProgressMarkup() {
$types = array(
'node' => t( 'Nodes' ),
'comment' => t( 'Comments' ),
'taxonomy_term' => t( 'Tags' ),
'configuration' => t( 'Configuration' ),
'ui' => t( 'UI elements' ),
);
$translation_manager = \Drupal::service( 'emw_translator.translation_manager' );
$langcodes = \Drupal::languageManager()->getLanguages();
$langcodes_list = array_keys( $langcodes );
$translation_progress = array();
foreach ( $langcodes_list as $langcode ) {
$translation_progress[ $langcode ] = array();
foreach ( array_keys( $types ) as $type ) {
$translation_progress[ $langcode ][ $type ] = $translation_manager->get_translation_progress_percents( $type, $langcode );
}
}
$markup = '<table><tr><th>' . t( 'Language' ) . '</th><th>' . implode( '</th><th>', $types ) . '</th></tr>';
foreach ( $translation_progress as $langcode => $value ) {
$markup .= '<tr><td>' . $langcodes[ $langcode ]->getName() . '</td>';
foreach ( $value as $progress ) {
$markup .= '<td>' . round( $progress, 2 ) . '%</td>';
}
$markup .= '</tr>';
}
$markup .= '</table>';
return $markup;
}
public function register_translation_results( $response, $display_type ) {
switch ( $response ) {
case ProcessingResponse::$fully_processed:
\Drupal::messenger()->addStatus( t( "Type '$display_type' strings translated successfully!" ) );
return;
case ProcessingResponse::$partially_processed:
\Drupal::messenger()->addWarning( t( "Type '$display_type' strings translated partially. Try decreasing MT engine request size (Settings->Advanced->Max characters per single request) or check logs for errors." ) );
return;
case ProcessingResponse::$processing_error:
\Drupal::messenger()->addError( t( "Type '$display_type' strings were not translated. Check your machine-translation configuration or logs." ) );
return;
case ProcessingResponse::$already_processed:
return;
default:
\Drupal::logger( 'emw_translator' )->error( "Invalid translation response '$response'" );
return;
}
}
public function register_deletion_results( $response, $display_type ) {
switch ( $response ) {
case ProcessingResponse::$fully_processed:
\Drupal::messenger()->addStatus( t( "Type '$display_type' strings deleted successfully!" ) );
return;
default:
\Drupal::logger( 'emw_translator' )->error( "Invalid deletion response '$response'" );
return;
}
}
}
<?php
namespace Drupal\emw_translator\Model;
class ProcessingResponse {
public static $processing_error = 0;
public static $partially_processed = 1;
public static $fully_processed = 2;
public static $already_processed = 3;
}
<?php
namespace Drupal\emw_translator\Model;
class TranslationResponse {
public $status;
public $translations;
public function __construct( $translations, $status = TranslationStatus::FullResult ) {
$this->translations = $translations;
$this->status = $status;
}
}
<?php
namespace Drupal\emw_translator\Model;
enum TranslationStatus {
case NoResult;
case PartialResult;
case FullResult;
}
<<<<<<< HEAD
<?php <?php
namespace Drupal\emw_translator; namespace Drupal\emw_translator;
...@@ -419,3 +420,488 @@ class TranslationManager { ...@@ -419,3 +420,488 @@ class TranslationManager {
} }
} }
} }
=======
<?php
namespace Drupal\emw_translator;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\emw_translator\Model\ProcessingResponse;
use Drupal\emw_translator\Model\TranslationStatus;
class TranslationManager {
private $translation_service;
protected $config_factory;
public function __construct( ConfigFactoryInterface $config_factory ) {
$this->config_factory = $config_factory;
$engine = \Drupal::config( 'emw_translator.settings' )->get( 'mt_engine' );
if ( ! $engine ) {
$engine = 'etranslation';
}
$this->translation_service = \Drupal::service( 'emw_translator.translation_service.' . $engine );
}
public function translate_entities( $entities, $type, $skip_if_translated = true ) {
$translatable_values = array();
$translatable_enitites = array();
$langcodes = \Drupal::languageManager()->getLanguages();
$langcodes_list = array_keys( $langcodes );
$translated_versions_to_create = 0;
$translated_versions_created = 0;
// group entities by src-trg language.
foreach ( $entities as $entity ) {
$source_langcode = $entity->language()->getId();
foreach ( $langcodes_list as $langcode ) {
if ( $langcode === $source_langcode ) {
continue;
}
if ( $skip_if_translated && $entity->hasTranslation( $langcode ) ) {
continue;
}
$key = "$source_langcode->$langcode";
if ( ! isset( $translatable_values[ $key ] ) ) {
$translatable_values[ $key ] = array();
$translatable_enitites[ $key ] = array();
}
$translatable_fields = $this->get_translatable_fields( $entity );
if ( ! empty( $translatable_fields ) ) {
$translated_versions_to_create++;
$entity_values = $this->extract_translatable_values( $entity, $translatable_fields );
$translatable_values[ $key ] = array_merge( $translatable_values[ $key ], $entity_values );
$translatable_enitites[ $key ][] = $entity;
}
}
}
// translate entities for each src-trg language pair.
foreach ( $translatable_values as $key => $strings_to_translate ) {
$key_parts = explode( '->', $key );
$src_lang = $key_parts[0];
$trg_lang = $key_parts[1];
$translation_response = $this->translation_service->translate( $src_lang, $trg_lang, $strings_to_translate, $type );
if ( empty( $translation_response->translations ) || TranslationStatus::FullResult !== $translation_response->status ) {
continue;
}
$offset = 0;
foreach ( $translatable_enitites[ $key ] as $entity ) {
$entity_translations = array();
$entity_translatable_fields = $this->get_translatable_fields( $entity );
$translatable_value_count = count( $this->extract_translatable_values( $entity, $translatable_fields ) );
for ( $i = 0; $i < $translatable_value_count; $i++, $offset++ ) {
$entity_translations[] = $translation_response->translations[ $offset ];
}
if ( ! empty( $entity_translations ) ) {
$this->entity_create_translation( $entity, $trg_lang, $entity_translatable_fields, $entity_translations );
$translated_versions_created++;
}
}
}
return $this->get_processing_response( $translated_versions_created, $translated_versions_to_create );
}
public function entity_update_all_translations( $entity ) {
\Drupal::logger( 'emw_translator' )->info( 'Updating all translations for entity' );
$source_langcode = $entity->language()->getId();
$langcodes = \Drupal::languageManager()->getLanguages();
$langcodes_list = array_keys( $langcodes );
foreach ( $langcodes_list as $langcode ) {
if ( $langcode === $source_langcode ) {
continue;
}
$this->translate_entity( $entity, $source_langcode, $langcode );
}
}
public function translate_entity( $entity, $src_lang, $trg_lang, $translations = null ) {
\Drupal::logger( 'emw_translator' )->info( "Translating entity ($src_lang-$trg_lang)" );
$translatable_fields = $this->get_translatable_fields( $entity );
if ( count( $translatable_fields ) > 0 && $entity->isTranslatable() ) {
$strings_to_translate = $this->extract_translatable_values( $entity, $translatable_fields );
if ( ! $translations ) {
$translation_response = $this->translation_service->translate( $src_lang, $trg_lang, $strings_to_translate, 'entity', false, $entity->id() );
$translations = $translation_response->translations;
if ( TranslationStatus::FullResult !== $translation_response->status ) {
return;
}
}
$this->entity_create_translation( $entity, $trg_lang, $translatable_fields, $translations );
}
}
public function delete_translations_of_entities( $entities ) {
$langcodes = \Drupal::languageManager()->getLanguages();
$langcodes_list = array_keys( $langcodes );
foreach ( $entities as $entity ) {
$source_langcode = $entity->language()->getId();
foreach ( $langcodes_list as $langcode ) {
if ( $langcode === $source_langcode ) {
continue;
}
if ( $entity->hasTranslation( $langcode ) ) {
$entity->removeTranslation( $langcode );
$entity->save();
}
}
}
return ProcessingResponse::$fully_processed;
}
/**
* Translate given UI strings (TranslationString[]) and save translations
*
* @return void
*/
public function translate_ui_strings( $translatable_strings, $src_lang, $trg_lang ) {
$locale_storage = \Drupal::service( 'locale.storage' );
$values = array_map(
function( $s ) {
return $s->source;
},
$translatable_strings
);
$translation_response = $this->translation_service->translate( $src_lang, $trg_lang, $values, 'UI', true );
$translations = $translation_response->translations;
$count = count( $translations );
for ( $i = 0; $i < $count; $i++ ) {
$translated_string = $translations[ $i ];
$translation = $locale_storage->createTranslation(
array(
'lid' => $translatable_strings[ $i ]->lid,
'language' => $trg_lang,
'translation' => $translated_string,
)
);
$translation->save();
}
return $this->get_processing_response( $count, count( $values ) );
}
/**
* Get list of all untranslated UI strings for a language
*
* @return TranslationString[]
*/
public function get_untranslated_ui_strings( $langcode ) {
$locale_storage = \Drupal::service( 'locale.storage' );
$existing_translations = $locale_storage->getTranslations( array( 'language' => $langcode ) );
$untranslated_strings = array_filter(
$existing_translations,
function( $s ) {
$has_source = isset( $s->source ) && ! empty( $s->source );
$has_translation = isset( $s->translation );
return $has_source && ! $has_translation;
}
);
return array_values( $untranslated_strings );
}
public function delete_ui_translations() {
$source_langcode = $this->get_default_language_code();
$langcodes = \Drupal::languageManager()->getLanguages();
$langcodes_list = array_keys( $langcodes );
$storage = \Drupal::service( 'locale.storage' );
foreach ( $langcodes_list as $langcode ) {
if ( $langcode === $source_langcode ) {
continue;
}
$storage->deleteTranslations( array( 'language' => $langcode ) );
}
return ProcessingResponse::$fully_processed;
}
public function translate_config_strings( $config_strings, $langcode ) {
// prepare source value list for translation.
$source_strings = array();
foreach ( $config_strings as $item ) {
array_walk_recursive(
$item,
function( $p ) use ( &$source_strings ) {
if ( is_string( $p ) ) {
$source_strings[] = $p;
}
}
);
}
$offset = 0;
if ( ! empty( $source_strings ) ) {
// translate.
$source_langcode = $this->get_default_language_code();
$translation_response = $this->translation_service->translate( $source_langcode, $langcode, $source_strings, 'configuration', true );
$translations = $translation_response->translations;
// replace source values with translations & save.
if ( ! empty( $translations ) && TranslationStatus::FullResult === $translation_response->status ) {
$collection = \Drupal::service( 'config.storage' )->createCollection( "language.$langcode" );
foreach ( $config_strings as $key => $value ) {
$translated_data = $value;
array_walk_recursive(
$translated_data,
function( &$v ) use ( $translations, &$offset ) {
if ( is_string( $v ) ) {
$v = $translations[ $offset ];
$offset++;
}
}
);
$collection->write( $key, $translated_data );
}
}
}
return $this->get_processing_response( $offset, count( $source_strings ) );
}
public function get_untranslated_config_strings( $langcode ) {
$translatable_items = $this->get_translatable_config_strings();
$translated_item_names = \Drupal::service( 'config.storage' )->createCollection( "language.$langcode" )->listAll();
// get untranslated config item names.
$missing_items = array_filter(
$translatable_items,
function( $name ) use ( $translated_item_names ) {
return ! in_array( $name, $translated_item_names, true );
},
ARRAY_FILTER_USE_KEY
);
return $missing_items;
}
private function get_translatable_config_strings() {
$source_item_names = $this->config_factory->listAll();
$config_items = $this->config_factory->loadMultiple( $source_item_names );
$translatable_items = array();
// identify translatable configuration items and their fields.
foreach ( $config_items as $item ) {
$original = $item->getOriginal();
if ( isset( $original['langcode'] ) ) {
$translatable_fields = $this->get_translatable_config_fields( $original );
if ( ! empty( $translatable_fields ) ) {
$translatable_items[ $item->getName() ] = $translatable_fields;
}
}
}
return $translatable_items;
}
private function get_translatable_config_fields( $config_item ) {
$translatable_fields = array();
// keys that usually hold translatable values.
$search_keys = array( 'label', 'name', 'description', 'message' );
// arrays that can contain $search_keys.
$container_keys = array( 'settings' );
foreach ( $search_keys as $key ) {
// check 1st level values.
if ( isset( $config_item[ $key ] ) && strlen( $config_item[ $key ] ) > 0 ) {
if ( 'name' === $key && in_array( 'label', array_keys( $translatable_fields ), true ) ) {
// do not translate 'name' if 'label' already present. Workaround for drupal/admin/config/media/image-styles translation.
continue;
}
$translatable_fields[ $key ] = $config_item[ $key ];
}
// check 2nd level values.
foreach ( $container_keys as $container_key ) {
if ( isset( $config_item[ $container_key ] ) && isset( $config_item[ $container_key ][ $key ] ) &&
strlen( $config_item[ $container_key ][ $key ] ) > 0 ) {
if ( ! isset( $translatable_fields[ $container_key ] ) ) {
$translatable_fields[ $container_key ] = array();
}
$translatable_fields[ $container_key ][ $key ] = $config_item[ $container_key ][ $key ];
}
}
}
return $translatable_fields;
}
public function delete_config_translations() {
$source_langcode = $this->get_default_language_code();
$langcodes = \Drupal::languageManager()->getLanguages();
$langcodes_list = array_keys( $langcodes );
foreach ( $langcodes_list as $langcode ) {
if ( $langcode === $source_langcode ) {
continue;
}
\Drupal::service( 'config.storage' )->createCollection( "language.$langcode" )->deleteAll();
}
return ProcessingResponse::$fully_processed;
}
public function get_translatable_fields( $entity ) {
switch ( $entity->getEntityTypeId() ) {
case 'node':
return array(
'title' => array( 'value' ),
'body' => array( 'value', 'summary' ),
);
case 'comment':
return array(
'subject' => array( 'value' ),
'comment_body' => array( 'value' ),
);
case 'taxonomy_term':
return array( 'name' => array( 'value' ) );
default:
// type not implemented.
return array();
}
}
/***
* Checks whether translatable content has changed and entity needs retranslation
*
* @param [type] $entity
* @return void
*/
public function entity_translatable_content_changed( $entity ) {
$translatable_fields = $this->get_translatable_fields( $entity );
if ( count( $translatable_fields ) > 0 ) {
foreach ( $translatable_fields as $parent => $fields ) {
foreach ( $fields as $field ) {
if ( ! $entity->original || $entity->$parent->$field !== $entity->original->$parent->$field ) {
return true;
}
}
}
}
if ( $entity->hasField( 'field_tags' ) ) {
return $entity->field_tags->referencedEntities() !== $entity->original->field_tags->referencedEntities();
}
return false;
}
public function get_translation_progress_percents( $type, $langcode ) {
$source_language = $this->get_default_language_code();
$source_count = 0;
$translated_count = 0;
switch ( $type ) {
case 'ui':
if ( $source_language === $langcode ) {
return 100;
}
$locale_storage = \Drupal::service( 'locale.storage' );
$source_count = count( $locale_storage->getStrings() );
$translated_count = $source_count - count( $this->get_untranslated_ui_strings( $langcode ) );
break;
case 'configuration':
if ( $source_language === $langcode ) {
return 100;
} else {
$source_strings = $this->get_translatable_config_strings();
$source_count = count( $source_strings );
$translated_count = $source_count - count( $this->get_untranslated_config_strings( $langcode ) );
break;
}
default:
$source_strings = \Drupal::entityTypeManager()
->getStorage( $type )
->loadMultiple();
$translated_strings = array_filter(
$source_strings,
function( $e ) use ( $langcode ) {
return $e->language()->getId() === $langcode || $e->hasTranslation( $langcode );
}
);
$source_count = count( $source_strings );
$translated_count = count( $translated_strings );
break;
}
return $source_count > 0 ? (float) 100 * $translated_count / $source_count : 0;
}
public function get_default_language_code() {
return \Drupal::service( 'language.default' )->get()->getId();
}
private function entity_create_translation( $entity, $trg_lang, $translation_fields, $translation_values ) {
$translation_count = count( $translation_values );
$source_value_count = count( $this->extract_translatable_values( $entity, $translation_fields ) );
if ( count( $translation_fields ) > 0 && $source_value_count === $translation_count && $entity->isTranslatable() ) {
if ( $entity->hasTranslation( $trg_lang ) ) {
$entity->removeTranslation( $trg_lang );
}
$field_translations = array();
$offset = 0;
foreach ( $translation_fields as $parent => $fields ) {
$field_translations[ $parent ] = array();
foreach ( $fields as $field ) {
$field_translations[ $parent ][ $field ] = $translation_values[ $offset ];
$offset++;
}
}
$translation = $entity->addTranslation( $trg_lang, $field_translations );
$body_key = null;
switch ( $entity->getEntityTypeId() ) {
case 'node':
$body_key = 'body';
break;
case 'comment':
$body_key = 'comment_body';
break;
}
if ( $body_key ) {
$translation->$body_key->format = $entity->$body_key->format;
}
if ( $entity->hasField( 'field_tags' ) ) {
$translation->field_tags = $entity->field_tags;
}
if ( $entity->hasField( 'status' ) ) {
$translation->status->value = $entity->status->value;
}
$translation->save();
}
}
private function get_processing_response( $translation_count, $reference_value_count ) {
if ( 0 === $reference_value_count ) {
return ProcessingResponse::$already_processed;
} elseif ( $translation_count === $reference_value_count ) {
return ProcessingResponse::$fully_processed;
} elseif ( 0 === $translation_count && 0 < $reference_value_count ) {
return ProcessingResponse::$processing_error;
} else {
return ProcessingResponse::$partially_processed;
}
}
public function extract_translatable_values( $entity, $translatable_fields ) {
$translatable_values = array();
foreach ( $translatable_fields as $parent => $fields ) {
foreach ( $fields as $field ) {
$translatable_values[] = $entity->$parent->$field;
}
}
return $translatable_values;
}
}
>>>>>>> main
<?php
namespace Drupal\emw_translator\translation_engines;
use Drupal\Core\Http\ClientFactory;
use Drupal\Component\Utility\Html;
use Drupal\emw_translator\Controller\PretranslationProgressController;
use Drupal\emw_translator\Model\TranslationResponse;
use Drupal\emw_translator\Model\TranslationStatus;
abstract class AbstractTranslationService {
private $drupal_variable_patterns = array(
// match @.. and %.. and !..
'(([^(@|%|!)^\sa-zA-Z\d+<>]*)(@|%|!)([^\s<>(@|%|!)]+))',
// match curly bracket variables like {var}.
'({([^\s]*?)})',
// match double curly bracket variables like {{ var }}.
'({{\s*(.*?)\s*}})',
// match square bracket variables like [site:name].
'(\[(.*?)\:(.*?)\])',
// match PHP variables like $var['abc'].
'(\$(([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(->)*)*(\[[^\]]*\])*))',
);
private $html_wrapper_prefix = '<div>';
private $html_wrapper_suffix = '</div>';
private $special_char_dict = array( "\x03" => '<etx></etx>' );
protected $http_client;
protected $retry_on_entity_translation_error = true;
protected $max_chars_per_request = 60000;
public function __construct( ClientFactory $http_client_factory ) {
$this->http_client = $http_client_factory->fromOptions( array( 'timeout' => 120 ) );
}
abstract protected function send_translation_request( $from, $to, $values, $entity_id );
public function translate( $from, $to, $values, $object_type = '', $encode_placeholders = false, $entity_id = null ) {
$current_char_len = 0;
$request_value_lists = array();
$current_list = array();
$lang_from = substr( $from, 0, 2 );
$lang_to = substr( $to, 0, 2 );
// split into multiple arrays not exceeding max char limit.
foreach ( $values as $value ) {
$chars = strlen( $value );
if ( $current_char_len + $chars > $this->max_chars_per_request ) {
$request_value_lists[] = $current_list;
$current_list = array( $value );
$current_char_len = $chars;
} else {
$current_list[] = $value;
$current_char_len += $chars;
}
}
if ( ! empty( $current_list ) ) {
$request_value_lists[] = $current_list;
}
// send requests and merge results together.
$full_result = array();
$request_count = count( $request_value_lists );
$percentage_completed = 0;
$reference_id = $this->update_progress_info( 0, $object_type, $lang_from, $lang_to, $entity_id );
for ( $i = 0; $i < $request_count; $i++ ) {
$this->check_client_disconnected( $reference_id, $entity_id );
// replace necessary special characters removed on HTML tree creation; currently needed only for UI strings containing multiple values in same string, separated by ETX char.
$request_value_lists[ $i ] = str_replace( array_keys( $this->special_char_dict ), $this->special_char_dict, $request_value_lists[ $i ] );
$contains_only_empty_strings = empty(
array_filter(
$request_value_lists[ $i ],
function ( $a ) {
return ! empty( $a );
}
)
);
if ( $contains_only_empty_strings ) {
$full_result = array_merge( $full_result, $request_value_lists[ $i ] );
} else {
// convert string[] to XML[].
$xml_values = $this->translatable_values_to_xml( $request_value_lists[ $i ] );
$translatable_values = $xml_values;
$encode_response = null;
if ( $encode_placeholders ) {
// replace non-translatable string placeholders with XML tags (for UI strings).
$encode_response = $this->encode_placeholders( $xml_values );
$translatable_values = $encode_response->values;
}
// translate XML[].
\Drupal::logger( 'emw_translator' )->debug( "Sending $lang_from-$lang_to translation request " . $i + 1 . '/' . $request_count . ' (' . count( $translatable_values ) . ' strings)' );
$xml_translations = $this->send_translation_request( $lang_from, $lang_to, $translatable_values, $entity_id );
if ( ! $xml_translations || empty( $xml_translations ) ) {
$retries_left = 2;
while ( $retries_left > 0 && ( ! $entity_id || $this->retry_on_entity_translation_error ) ) {
$this->check_client_disconnected( $reference_id, $entity_id );
\Drupal::logger( 'emw_translator' )->debug( "Translation request failed, retrying... (retries left: $retries_left)" );
$xml_translations = $this->send_translation_request( $lang_from, $lang_to, $translatable_values, $entity_id );
$retries_left--;
}
// do not continue if one of requests fail.
if ( ! $xml_translations || empty( $xml_translations ) ) {
if ( $request_count > 1 ) {
\Drupal::logger( 'emw_translator' )->error( 'One of translation requests failed' );
}
return new TranslationResponse( $full_result, empty( $full_result ) ? TranslationStatus::NoResult : TranslationStatus::PartialResult );
}
}
if ( $encode_response ) {
// restore non-translatable string placeholders.
$xml_translations = $this->restore_spaces_around_placeholders( $translatable_values, $xml_translations, $encode_response->placeholders );
$xml_translations = $this->decode_placeholders( $xml_translations, $encode_response->placeholders );
}
// convert XML[] back to string[].
$decoded_translations = $this->decode_xml_translations( $request_value_lists[ $i ], $xml_translations );
// restore special characters (for some UI strings).
$decoded_translations = str_replace( $this->special_char_dict, array_keys( $this->special_char_dict ), $decoded_translations );
// merge with previous translations.
$full_result = array_merge( $full_result, $decoded_translations );
}
// update percentage for pre-translation progress bar.
$percentage_completed += 100 / $request_count;
$this->update_progress_info( $percentage_completed, $object_type, $lang_from, $lang_to, $entity_id );
}
return new TranslationResponse( $full_result, TranslationStatus::FullResult );
}
public function test_connection() {
$response = $this->translate( 'en', 'de', array( 'test request' ) );
return TranslationStatus::FullResult === $response->status;
}
protected function translatable_values_to_xml( $values ) {
$xmls = array();
$count = 1;
foreach ( $values as $v ) {
$html_string = $this->wrap_as_html( $v );
$dom = Html::load( $html_string );
$body = $dom->getElementsByTagName( 'body' )[0];
$nodes = array( $body->firstChild );
while ( $node = array_shift( $nodes ) ) {
if ( $node instanceof \DOMElement ) {
// replace each non-text node with <g> node (or x tag if self-closing), also removing all attributes.
$newTag = $dom->createElement( $node->hasChildNodes() ? 'g' : 'x' );
// set id for each <g> or <x> node.
$newTag->setAttribute( 'id', $count++ );
// replace node.
$node->parentNode->replaceChild( $newTag, $node );
// move all children nodes to new node.
while ( $node->hasChildNodes() ) {
$newTag->appendChild( $node->firstChild );
}
// process children nodes next.
$nodes = array_merge( iterator_to_array( $newTag->childNodes ), $nodes );
}
}
$xmls[] = $dom->saveXML( $body->firstChild );
}
return $xmls;
}
public function decode_xml_translations( $source_values, $translated_values ) {
$translation_count = count( $translated_values );
$decoded_translations = array();
for ( $i = 0; $i < $translation_count; $i++ ) {
// retrieve source and translated values.
$source = $source_values[ $i ];
$translation = $translated_values[ $i ];
// load source as html document.
$html_source = $this->wrap_as_html( $source );
$html_dom = Html::load( $html_source );
$html_wrapper = $html_dom->getElementsByTagName( 'body' )[0]->firstChild;
// load translation as xml document.
$xml = simplexml_load_string( $translation );
$xml_dom = dom_import_simplexml( $xml )->ownerDocument;
$xml_wrapper = $xml_dom->firstChild;
// replace text nodes in source html with matching text nodes from translation xml.
$new_node = $this->replace_text_nodes_bottom_up( $html_wrapper, $xml_wrapper );
// save as XML instead of HTML to avoid url encoding.
$text_xml = $html_dom->saveXML( $new_node );
// remove XML tag.
$text_value = preg_replace( '/\<\?xml(.*?)\?\>/', '', $text_xml );
// remove wrapper.
$html_prefix_len = strlen( $this->html_wrapper_prefix );
$decoded_translation = substr( $text_value, $html_prefix_len, strlen( $text_value ) - $html_prefix_len - strlen( $this->html_wrapper_suffix ) );
$decoded_translations[] = $decoded_translation;
}
return $decoded_translations;
}
private function replace_text_nodes_bottom_up( $original_node, $encoded_node ) {
// Recursively call this function on each child node.
$original_child_node = $original_node->lastChild;
$encoded_child_node = $encoded_node->lastChild;
while ( $original_child_node && $encoded_child_node ) {
$this->replace_text_nodes_bottom_up( $original_child_node, $encoded_child_node );
$original_child_node = $original_child_node->previousSibling;
$encoded_child_node = $encoded_child_node->previousSibling;
// workaround for extra spaces inserted by MT between <g> tags like: '</g> <g>'.
while ( $original_child_node && $encoded_child_node && XML_TEXT_NODE === $encoded_child_node->nodeType && empty( trim( $encoded_child_node->nodeValue ) ) && XML_TEXT_NODE !== $original_child_node->nodeType ) {
$textNode = $original_node->ownerDocument->createTextNode( $encoded_child_node->nodeValue );
$original_node->insertBefore( $textNode, $original_child_node->nextSibling );
$encoded_child_node = $encoded_child_node->previousSibling;
}
}
// Process the current node.
if ( XML_TEXT_NODE === $original_node->nodeType ) {
$original_node->nodeValue = $encoded_node->nodeValue;
}
return $original_node;
}
private function encode_placeholders( $values ) {
$regex = '/' . implode( '|', $this->drupal_variable_patterns ) . '/';
$response = new \stdClass();
$response->placeholders = array();
$response->values = array();
$index = 1;
foreach ( $values as $v ) {
$response->values[] = preg_replace_callback(
$regex,
function( $matches ) use ( &$index, $response ) {
$key = $this->get_placeholder( $index++ );
$response->placeholders[ $key ] = $matches[0];
return $key;
},
$v
);
}
return $response;
}
// replaces placeholders with original values.
private function decode_placeholders( $translated_values, $placeholders ) {
$translated_values = str_replace( array_keys( $placeholders ), $placeholders, $translated_values );
return $translated_values;
}
private function restore_spaces_around_placeholders( $original_xml_values, $translated_xml_values, $placeholders ) {
$delimiter = '__EMW__;';
$original_xml = implode( $delimiter, $original_xml_values );
$translated_xml = implode( $delimiter, $translated_xml_values );
foreach ( array_keys( $placeholders ) as $key ) {
$original_pos = strpos( $original_xml, $key );
$translation_pos = strpos( $translated_xml, $key );
if ( $original_pos > -1 && $translation_pos > -1 ) {
$original_char_before = substr( $original_xml, $original_pos - 1, 1 );
$translation_char_before = substr( $translated_xml, $translation_pos - 1, 1 );
if ( ctype_space( $original_char_before ) && ! ctype_space( $translation_char_before ) ) {
$translated_xml = str_replace( $key, $original_char_before . $key, $translated_xml );
}
$original_char_after = substr( $original_xml, $original_pos + strlen( $key ), 1 );
$translation_char_after = substr( $translated_xml, $translation_pos + strlen( $key ), 1 );
if ( ctype_space( $original_char_after ) && ! ctype_space( $translation_char_after ) ) {
$translated_xml = str_replace( $key, $key . $original_char_after, $translated_xml );
}
}
}
return explode( $delimiter, $translated_xml );
}
protected function get_placeholder( $index ) {
return "<x id=\"#$index\"/>";
}
private function wrap_as_html( $string ) {
return $this->html_wrapper_prefix . $string . $this->html_wrapper_suffix;
}
private function update_progress_info( $percentage, $type, $from, $to, $entity_id ) {
if ( ! $entity_id && PretranslationProgressController::getValue() ) {
PretranslationProgressController::updateTranslationMessage( "Translating strings of type '$type' ($from -> $to)" );
PretranslationProgressController::updateTranslationPercentage( $percentage );
return PretranslationProgressController::getValue()->data->request_id;
}
return null;
}
private function check_client_disconnected( $reference_id, $entity_id ) {
$value = PretranslationProgressController::getValue();
if ( ! $entity_id && $value && $value->data ) {
if ( $reference_id && $reference_id !== $value->data->request_id ) {
\Drupal::logger( 'emw_translator' )->debug( 'Stopping execution, parallel translation detected!' );
die();
}
if ( $value->data->aborted ) {
\Drupal::logger( 'emw_translator' )->debug( 'Stopping execution, client disconnected!' );
die();
}
}
}
}
<?php
namespace Drupal\emw_translator\translation_engines\etranslation;
use Drupal\emw_translator\translation_engines\AbstractTranslationService;
use Drupal\Core\Http\ClientFactory;
class EtranslationService extends AbstractTranslationService {
public static $default_chars_per_request = 60000;
private static $error_map = array(
-20000 => 'Source language not specified',
-20001 => 'Invalid source language',
-20002 => 'Target language(s) not specified',
-20003 => 'Invalid target language(s)',
-20004 => 'DEPRECATED',
-20005 => 'Caller information not specified',
-20006 => 'Missing application name',
-20007 => 'Application not authorized to access the service',
-20008 => 'Bad format for ftp address',
-20009 => 'Bad format for sftp address',
-20010 => 'Bad format for http address',
-20011 => 'Bad format for email address',
-20012 => 'Translation request must be text type, document path type or document base64 type and not several at a time',
-20013 => 'Language pair not supported by the domain',
-20014 => 'Username parameter not specified',
-20015 => 'Extension invalid compared to the MIME type',
-20016 => 'DEPRECATED',
-20017 => 'Username parameter too long',
-20018 => 'Invalid output format',
-20019 => 'Institution parameter too long',
-20020 => 'Department number too long',
-20021 => 'Text to translate too long',
-20022 => 'Too many FTP destinations',
-20023 => 'Too many SFTP destinations',
-20024 => 'Too many HTTP destinations',
-20025 => 'Missing destination',
-20026 => 'Bad requester callback protocol',
-20027 => 'Bad error callback protocol',
-20028 => 'Concurrency quota exceeded',
-20029 => 'Document format not supported',
-20030 => 'Text to translate is empty',
-20031 => 'Missing text or document to translate',
-20032 => 'Email address too long',
-20033 => 'Cannot read stream',
-20034 => 'Output format not supported',
-20035 => 'Email destination tag is missing or empty',
-20036 => 'HTTP destination tag is missing or empty',
-20037 => 'FTP destination tag is missing or empty',
-20038 => 'SFTP destination tag is missing or empty',
-20039 => 'Document to translate tag is missing or empty',
-20040 => 'Format tag is missing or empty',
-20041 => 'The content is missing or empty',
-20042 => 'Source language defined in TMX file differs from request',
-20043 => 'Source language defined in XLIFF file differs from request',
-20044 => 'Output format is not available when quality estimate is requested. It should be blank or \'xslx\'',
-20045 => 'Quality estimate is not available for text snippet',
-20046 => 'Document too big (>20Mb)',
-20047 => 'Quality estimation not available',
-40010 => 'Too many segments to translate',
-80004 => 'Cannot store notification file at specified FTP address',
-80005 => 'Cannot store notification file at specified SFTP address',
-80006 => 'Cannot store translated file at specified FTP address',
-80007 => 'Cannot store translated file at specified SFTP address',
-90000 => 'Cannot connect to FTP',
-90001 => 'Cannot retrieve file at specified FTP address',
-90002 => 'File not found at specified address on FTP',
-90007 => 'Malformed FTP address',
-90012 => 'Cannot retrieve file content on SFTP',
-90013 => 'Cannot connect to SFTP',
-90014 => 'Cannot store file at specified FTP address',
-90015 => 'Cannot retrieve file content on SFTP',
-90016 => 'Cannot retrieve file at specified SFTP address',
);
// do not retry if response has not been received within specified $etranslation_timeout on entity insert/update.
protected $retry_on_entity_translation_error = false;
private $request_timeout = 600;
private $application_name;
private $password;
private $etranslation_timeout;
// XML variables.
private $segment_opening = '<segment>';
private $segment_closing = '</segment>';
private $delimiter;
public function __construct( ClientFactory $http_client_factory ) {
parent::__construct( $http_client_factory );
$config = \Drupal::config( 'emw_translator.settings' );
$this->http_client = $http_client_factory->fromOptions( array( 'timeout' => $this->request_timeout ) );
$this->application_name = $config->get( 'etranslation_appname' );
$this->password = EtranslationUtils::decrypt_password( $config->get( 'etranslation_password' ) );
$this->etranslation_timeout = $config->get( 'etranslation_timeout' ) ? $config->get( 'etranslation_timeout' ) : EtranslationUtils::$etranslation_timeout_default;
$this->max_chars_per_request = $config->get( 'etranslation_chars_per_request' ) ? $config->get( 'etranslation_chars_per_request' ) : self::$default_chars_per_request;
$this->delimiter = $this->segment_closing . $this->segment_opening;
}
protected function send_translation_request( $from, $to, $xml_values, $entity_id = null ) {
if ( ! $this->application_name || ! $this->password ) {
\Drupal::logger( 'emw_translator' )->error( 'eTranslation credentials not configured' );
return array();
}
// prepare translation request.
$id = uniqid();
$xml_prefix = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?><root>' . $this->segment_opening;
$xml_suffix = $this->segment_closing . '</root>';
$content = $xml_prefix . implode( $this->delimiter, $xml_values ) . $xml_suffix;
if ( strlen( $content ) === 0 ) {
return array();
}
$post = $this->get_post_body( $id, $from, $to, $content, $entity_id );
$client = $this->get_curl_client( $post );
// send translation request.
$response = curl_exec( $client );
$http_status = curl_getinfo( $client, CURLINFO_RESPONSE_CODE );
curl_close( $client );
// check request response.
$body = json_decode( $response );
$request_id = is_numeric( $body ) ? (int) $body : null;
if ( 200 !== $http_status || $request_id < 0 ) {
$message = self::$error_map[ $request_id ] ?? $body;
$err = curl_error( $client );
\Drupal::logger( 'emw_translator' )->error( "Invalid request response from eTranslation: $response [status: $http_status, message: $message, error: $err]" );
return array();
}
\Drupal::logger( 'emw_translator' )->info( "eTranslation request successful ($request_id)" );
// wait for translation callback.
$response = $this->await_etranslation_response( $request_id, null === $entity_id );
if ( $response ) {
return $this->etranslation_response_to_translation_array( $response->data );
} else {
return array();
}
}
public function etranslation_response_to_translation_array( $response ) {
// extract translations.
$segment_tag_length = strlen( $this->segment_opening );
$first_segment_pos = strpos( $response, $this->segment_opening );
$data_start_pos = $first_segment_pos + $segment_tag_length;
$data_end_pos = strrpos( $response, $this->segment_closing );
$segment_string = substr( $response, $data_start_pos, $data_end_pos - $data_start_pos );
$translations = explode( $this->delimiter, $segment_string );
return $translations;
}
private function get_curl_client( $body ) {
$config = \Drupal::config( 'emw_translator.settings' );
$application_name = $config->get( 'etranslation_appname' );
$password = EtranslationUtils::decrypt_password( $config->get( 'etranslation_password' ) );
$client = curl_init( 'https://webgate.ec.europa.eu/etranslation/si/translate' );
curl_setopt( $client, CURLOPT_POST, 1 );
curl_setopt( $client, CURLOPT_POSTFIELDS, $body );
curl_setopt( $client, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $client, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST );
curl_setopt( $client, CURLOPT_USERPWD, $application_name . ':' . $password );
curl_setopt( $client, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $client, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $client, CURLOPT_TIMEOUT, $this->request_timeout );
curl_setopt(
$client,
CURLOPT_HTTPHEADER,
array(
'Content-Type: application/json',
'Content-Length: ' . strlen( $body ),
)
);
return $client;
}
private function get_post_body( $id, $lang_from, $lang_to, $translatable_string, $entity_id = null ) {
$site_url = \Drupal::request()->getSchemeAndHttpHost();
$document = array(
'content' => base64_encode( $translatable_string ),
'format' => 'xml',
'filename' => 'translateMe',
);
$translation_request_body = array(
'documentToTranslateBase64' => $document,
'sourceLanguage' => strtoupper( $lang_from ),
'targetLanguages' => array(
strtoupper( $lang_to ),
),
'errorCallback' => $site_url . '/etranslation/error/' . $id,
'callerInformation' => array(
'application' => $this->application_name,
),
'destinations' => array(
'httpDestinations' => array(
$site_url . '/etranslation/translation/' . $id,
),
),
'domain' => 'GEN',
);
if ( $entity_id ) {
$translation_request_body['externalReference'] = $entity_id;
}
return json_encode( $translation_request_body );
}
private function await_etranslation_response( $request_id, $is_pretranslation = true ) {
$response = null;
$key = EtranslationUtils::get_cache_key( $request_id );
$start_time = microtime( true );
$check_interval_ms = 250;
$timeout = $is_pretranslation ? $this->request_timeout : $this->etranslation_timeout;
while ( ! $response && $timeout > 0 && microtime( true ) - $start_time < $timeout ) {
$response = \Drupal::cache()->get( $key );
usleep( $check_interval_ms * 1000 );
}
if ( ! $response ) {
\Drupal::logger( 'emw_translator' )->info( "eTranslation response timeout ($request_id, $timeout s)" );
\Drupal::cache()->set( $key, EtranslationUtils::$timeout_value );
return null;
} else {
\Drupal::cache()->delete( $key );
if ( EtranslationUtils::$error_value === $response->data ) {
return null;
}
return $response;
}
}
}
<?php
namespace Drupal\emw_translator\translation_engines\etranslation;
class EtranslationUtils {
public static $timeout_value = '_timeout';
public static $error_value = '_error';
public static $etranslation_timeout_default = 10;
public static function encrypt_password( $plaintext ) {
$host = \Drupal::request()->getHost();
$method = 'AES-256-CBC';
$key = hash( 'sha256', $host, true );
$iv = openssl_random_pseudo_bytes( 16 );
$ciphertext = openssl_encrypt( $plaintext, $method, $key, OPENSSL_RAW_DATA, $iv );
$hash = hash_hmac( 'sha256', $ciphertext . $iv, $key, true );
return base64_encode( $iv . $hash . $ciphertext );
}
public static function decrypt_password( $base64cipher ) {
$host = \Drupal::request()->getHost();
$method = 'AES-256-CBC';
$ivHashCiphertext = base64_decode( $base64cipher );
$iv = substr( $ivHashCiphertext, 0, 16 );
$hash = substr( $ivHashCiphertext, 16, 32 );
$ciphertext = substr( $ivHashCiphertext, 48 );
$key = hash( 'sha256', $host, true );
if ( ! hash_equals( hash_hmac( 'sha256', $ciphertext . $iv, $key, true ), $hash ) ) {
return null;
}
return openssl_decrypt( $ciphertext, $method, $key, OPENSSL_RAW_DATA, $iv );
}
public static function get_cache_key( $request_id ) {
return "etranslation/$request_id";
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment