class LingotekContentTranslationService in Lingotek Translation 8
Same name and namespace in other branches
- 8.2 src/LingotekContentTranslationService.php \Drupal\lingotek\LingotekContentTranslationService
- 4.0.x src/LingotekContentTranslationService.php \Drupal\lingotek\LingotekContentTranslationService
- 3.0.x src/LingotekContentTranslationService.php \Drupal\lingotek\LingotekContentTranslationService
- 3.1.x src/LingotekContentTranslationService.php \Drupal\lingotek\LingotekContentTranslationService
- 3.2.x src/LingotekContentTranslationService.php \Drupal\lingotek\LingotekContentTranslationService
- 3.3.x src/LingotekContentTranslationService.php \Drupal\lingotek\LingotekContentTranslationService
- 3.4.x src/LingotekContentTranslationService.php \Drupal\lingotek\LingotekContentTranslationService
- 3.5.x src/LingotekContentTranslationService.php \Drupal\lingotek\LingotekContentTranslationService
- 3.6.x src/LingotekContentTranslationService.php \Drupal\lingotek\LingotekContentTranslationService
- 3.7.x src/LingotekContentTranslationService.php \Drupal\lingotek\LingotekContentTranslationService
- 3.8.x src/LingotekContentTranslationService.php \Drupal\lingotek\LingotekContentTranslationService
Service for managing Lingotek content translations.
Hierarchy
- class \Drupal\lingotek\LingotekContentTranslationService implements LingotekContentTranslationServiceInterface uses StringTranslationTrait
Expanded class hierarchy of LingotekContentTranslationService
1 string reference to 'LingotekContentTranslationService'
1 service uses LingotekContentTranslationService
File
- src/
LingotekContentTranslationService.php, line 30 - Contains \Drupal\lingotek\LingotekContentTranslationService.
Namespace
Drupal\lingotekView source
class LingotekContentTranslationService implements LingotekContentTranslationServiceInterface {
use StringTranslationTrait;
/**
* @var \Drupal\lingotek\LingotekInterface
*/
protected $lingotek;
/**
* The language-locale mapper.
*
* @var \Drupal\lingotek\LanguageLocaleMapperInterface
*/
protected $languageLocaleMapper;
/**
* The Lingotek configuration service.
*
* @var \Drupal\lingotek\LingotekConfigurationServiceInterface
*/
protected $lingotekConfiguration;
/**
* The Lingotek configuration translation service.
*
* @var \Drupal\lingotek\LingotekConfigTranslationServiceInterface
*/
protected $lingotekConfigTranslation;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface $language_manager
*/
protected $languageManager;
/**
* Constructs a new LingotekContentTranslationService object.
*
* @param \Drupal\lingotek\LingotekInterface $lingotek
* An lingotek object.
* @param \Drupal\lingotek\LanguageLocaleMapperInterface $language_locale_mapper
* The language-locale mapper.
* @param \Drupal\lingotek\LingotekConfigurationServiceInterface $lingotek_configuration
* The Lingotek configuration service.
* @param \Drupal\lingotek\LingotekConfigTranslationServiceInterface $translation_service
* The Lingotek config translation service.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* An entity manager object.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(LingotekInterface $lingotek, LanguageLocaleMapperInterface $language_locale_mapper, LingotekConfigurationServiceInterface $lingotek_configuration, LingotekConfigTranslationServiceInterface $lingotek_config_translation, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) {
$this->lingotek = $lingotek;
$this->languageLocaleMapper = $language_locale_mapper;
$this->lingotekConfiguration = $lingotek_configuration;
$this->lingotekConfigTranslation = $lingotek_config_translation;
$this->entityManager = $entity_manager;
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public function checkSourceStatus(ContentEntityInterface &$entity) {
$document_id = $this
->getDocumentId($entity);
// We set a max time of 1 hour for the import (in seconds)
$MAX_IMPORT_TIME = 3600;
$source_status = $this
->getSourceStatus($entity);
if ($document_id) {
// Document has successfully imported.
if ($this->lingotek
->getDocumentStatus($document_id)) {
$this
->setSourceStatus($entity, Lingotek::STATUS_CURRENT);
return TRUE;
}
else {
//TODO: change to actual last_uploaded timestamp rather than surrogate.
if ($entity
->getEntityType()
->isSubclassOf(EntityChangedInterface::class)) {
$last_uploaded_time = $entity
->getChangedTime();
// If document has not successfully imported after MAX_IMPORT_TIME then
// move to ERROR state.
if (REQUEST_TIME - $last_uploaded_time > $MAX_IMPORT_TIME) {
$this
->setSourceStatus($entity, Lingotek::STATUS_ERROR);
}
else {
// Document still may be importing and the MAX import time didn't
// complete yet, so we do nothing.
}
}
return FALSE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getSourceStatus(ContentEntityInterface &$entity) {
$source_language = LanguageInterface::LANGCODE_NOT_SPECIFIED;
if ($entity->lingotek_translation_source && $entity->lingotek_translation_source->value !== NULL) {
$source_language = $entity->lingotek_translation_source->value;
}
if ($source_language == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$source_language = $entity
->getUntranslated()
->language()
->getId();
}
return $this
->getTargetStatus($entity, $source_language);
}
/**
* {@inheritdoc}
*/
public function setSourceStatus(ContentEntityInterface &$entity, $status) {
$source_language = $entity->lingotek_translation_source->value;
if ($source_language == LanguageInterface::LANGCODE_NOT_SPECIFIED || $source_language == NULL) {
$source_language = $entity
->getUntranslated()
->language()
->getId();
}
return $this
->setTargetStatus($entity, $source_language, $status);
}
/**
* {@inheritdoc}
*/
public function checkTargetStatuses(ContentEntityInterface &$entity) {
$document_id = $this
->getDocumentId($entity);
$translation_statuses = $this->lingotek
->getDocumentTranslationStatuses($document_id);
foreach ($translation_statuses as $lingotek_locale => $progress) {
$drupal_language = $this->languageLocaleMapper
->getConfigurableLanguageForLocale($lingotek_locale);
if ($drupal_language == NULL) {
continue;
// languages existing in TMS, but not configured on Drupal
}
$langcode = $drupal_language
->id();
$current_target_status = $this
->getTargetStatus($entity, $langcode);
if (in_array($current_target_status, [
Lingotek::STATUS_UNTRACKED,
Lingotek::STATUS_EDITED,
Lingotek::STATUS_REQUEST,
Lingotek::STATUS_NONE,
Lingotek::STATUS_READY,
Lingotek::STATUS_PENDING,
NULL,
])) {
if ($progress === Lingotek::PROGRESS_COMPLETE) {
$this
->setTargetStatus($entity, $langcode, Lingotek::STATUS_READY);
}
else {
$this
->setTargetStatus($entity, $langcode, Lingotek::STATUS_PENDING);
}
}
}
}
/**
* {@inheritdoc}
*/
public function checkTargetStatus(ContentEntityInterface &$entity, $langcode) {
$current_status = $this
->getTargetStatus($entity, $langcode);
$locale = $this->languageLocaleMapper
->getLocaleForLangcode($langcode);
$source_status = $this
->getSourceStatus($entity);
$document_id = $this
->getDocumentId($entity);
if ($langcode !== $entity
->getUntranslated()
->language()
->getId()) {
if (($current_status == Lingotek::STATUS_PENDING || $current_status == Lingotek::STATUS_EDITED) && $source_status !== Lingotek::STATUS_EDITED) {
$translation_status = $this->lingotek
->getDocumentTranslationStatus($document_id, $locale);
if ($translation_status) {
$current_status = Lingotek::STATUS_READY;
$this
->setTargetStatus($entity, $langcode, $current_status);
}
elseif ($this->lingotek
->downloadDocument($document_id, $locale)) {
// TODO: Set Status to STATUS_READY_INTERIM when that status is
// available. See ticket: https://www.drupal.org/node/2850548
}
}
elseif ($current_status == Lingotek::STATUS_REQUEST || $current_status == Lingotek::STATUS_UNTRACKED) {
$translation_status = $this->lingotek
->getDocumentTranslationStatus($document_id, $locale);
if ($translation_status === TRUE) {
$current_status = Lingotek::STATUS_READY;
$this
->setTargetStatus($entity, $langcode, $current_status);
}
elseif ($translation_status !== FALSE) {
$current_status = Lingotek::STATUS_PENDING;
$this
->setTargetStatus($entity, $langcode, $current_status);
}
//elseif ($this->lingotek->downloadDocument($document_id, $locale)) {
// // TODO: Set Status to STATUS_READY_INTERIM when that status is
// // available. See ticket: https://www.drupal.org/node/2850548
// }
}
}
return $current_status;
}
/**
* {@inheritdoc}
*/
public function getTargetStatus(ContentEntityInterface &$entity, $langcode) {
$status = Lingotek::STATUS_UNTRACKED;
if (count($entity->lingotek_translation_status) > 0) {
foreach ($entity->lingotek_translation_status
->getIterator() as $delta => $value) {
if ($value->language == $langcode) {
$status = $value->value;
}
}
}
return $status;
}
/**
* {@inheritdoc}
*/
public function setTargetStatus(ContentEntityInterface &$entity, $langcode, $status, $save = TRUE) {
$set = FALSE;
if ($entity
->hasField('lingotek_translation_status') && count($entity->lingotek_translation_status) > 0) {
foreach ($entity->lingotek_translation_status
->getIterator() as $delta => $value) {
if ($value->language == $langcode) {
$value->value = $status;
$set = true;
}
}
}
if (!$set && $entity
->hasField('lingotek_translation_status')) {
$entity->lingotek_translation_status
->appendItem([
'language' => $langcode,
'value' => $status,
]);
$set = TRUE;
}
if ($set && $save) {
// If the entity supports revisions, ensure we don't create a new one.
if ($entity
->getEntityType()
->hasKey('revision')) {
$entity
->setNewRevision(FALSE);
}
$entity->lingotek_processed = TRUE;
$entity
->save();
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function setTargetStatuses(ContentEntityInterface &$entity, $status) {
$target_languages = $this->languageManager
->getLanguages();
$entity_langcode = $entity
->getUntranslated()
->language()
->getId();
foreach ($target_languages as $langcode => $language) {
if ($langcode != $entity_langcode && ($current_status = $this
->getTargetStatus($entity, $langcode))) {
if ($current_status === Lingotek::STATUS_PENDING && $status === Lingotek::STATUS_REQUEST) {
// Don't allow to pass from pending to request. We have been already
// requested this one.
continue;
}
if (in_array($current_status, [
Lingotek::STATUS_UNTRACKED,
Lingotek::STATUS_REQUEST,
Lingotek::STATUS_DISABLED,
NULL,
]) && $status === Lingotek::STATUS_PENDING) {
continue;
}
if ($current_status == $status) {
continue;
}
if ($current_status != Lingotek::STATUS_EDITED && $current_status !== Lingotek::STATUS_CURRENT) {
$this
->setTargetStatus($entity, $langcode, $status);
}
elseif ($current_status == Lingotek::STATUS_EDITED && in_array($status, [
Lingotek::STATUS_CURRENT,
Lingotek::STATUS_PENDING,
])) {
$this
->setTargetStatus($entity, $langcode, $status);
}
}
}
}
/**
* {@inheritdoc}
*/
public function markTranslationsAsDirty(ContentEntityInterface &$entity) {
$target_languages = $this->languageManager
->getLanguages();
$entity_langcode = $entity
->getUntranslated()
->language()
->getId();
// These statuses indicate that content has been uploaded to the API, so
// we need to flag them as out of date.
$to_change = [
Lingotek::STATUS_CURRENT,
Lingotek::STATUS_PENDING,
Lingotek::STATUS_INTERMEDIATE,
Lingotek::STATUS_READY,
];
foreach ($target_languages as $langcode => $language) {
if ($langcode != $entity_langcode && ($current_status = $this
->getTargetStatus($entity, $langcode))) {
if (in_array($current_status, $to_change)) {
$this
->setTargetStatus($entity, $langcode, Lingotek::STATUS_EDITED);
}
}
}
}
/**
* {@inheritdoc}
*/
public function getDocumentId(ContentEntityInterface &$entity) {
$doc_id = NULL;
if ($entity->lingotek_document_id) {
$doc_id = $entity->lingotek_document_id->value;
}
return $doc_id;
}
/**
* {@inheritdoc}
*/
public function setDocumentId(ContentEntityInterface &$entity, $doc_id) {
$entity->lingotek_document_id = $doc_id;
// If the entity supports revisions, ensure we don't create a new one.
if ($entity
->getEntityType()
->hasKey('revision')) {
$entity
->setNewRevision(FALSE);
}
$entity->lingotek_processed = TRUE;
$entity
->save();
\Drupal::database()
->insert('lingotek_content_metadata')
->fields([
'document_id',
'entity_type',
'entity_id',
])
->values([
'document_id' => $doc_id,
'entity_type' => $entity
->getEntityTypeId(),
'entity_id' => $entity
->id(),
])
->execute();
return $entity;
}
/**
* {@inheritdoc}
*/
public function getSourceLocale(ContentEntityInterface &$entity) {
$source_language = $entity
->getUntranslated()
->language()
->getId();
return $this->languageLocaleMapper
->getLocaleForLangcode($source_language);
}
/**
* {@inheritdoc}
*/
public function getSourceData(ContentEntityInterface &$entity, &$visited = []) {
// Logic adapted from Content Translation core module and TMGMT contrib
// module for pulling translatable field info from content entities.
$visited[$entity
->bundle()][] = $entity
->id();
$entity_type = $entity
->getEntityType();
$field_definitions = $this->entityManager
->getFieldDefinitions($entity
->getEntityTypeId(), $entity
->bundle());
$storage_definitions = $entity_type instanceof ContentEntityTypeInterface ? $this->entityManager
->getFieldStorageDefinitions($entity_type
->id()) : array();
$translatable_fields = array();
// We need to include computed fields, as we may have a URL alias.
foreach ($entity
->getFields(TRUE) as $field_name => $definition) {
if ($this->lingotekConfiguration
->isFieldLingotekEnabled($entity
->getEntityTypeId(), $entity
->bundle(), $field_name) && $field_name != $entity_type
->getKey('langcode') && $field_name != $entity_type
->getKey('default_langcode')) {
$translatable_fields[$field_name] = $definition;
}
}
$default_display = entity_get_display($entity
->getEntityTypeId(), $entity
->bundle(), 'default');
if ($default_display !== NULL) {
uksort($translatable_fields, function ($a, $b) use ($default_display) {
return SortArray::sortByKeyString($default_display
->getComponent($a), $default_display
->getComponent($b), 'weight');
});
}
$data = array();
$source_entity = $entity
->getUntranslated();
foreach ($translatable_fields as $k => $definition) {
// If there is only one relevant attribute, upload it.
// Get the column translatability configuration.
module_load_include('inc', 'content_translation', 'content_translation.admin');
$column_element = content_translation_field_sync_widget($field_definitions[$k]);
$field = $source_entity
->get($k);
$field_type = $field_definitions[$k]
->getType();
foreach ($field as $fkey => $fval) {
// If we have only one relevant column, upload that. If not, check our
// settings.
if (!$column_element) {
$properties = $fval
->getProperties();
foreach ($properties as $property_name => $property_value) {
if (isset($storage_definitions[$k])) {
$property_definition = $storage_definitions[$k]
->getPropertyDefinition($property_name);
$data_type = $property_definition
->getDataType();
if (($data_type === 'string' || $data_type === 'uri') && !$property_definition
->isComputed()) {
if (isset($fval->{$property_name}) && !empty($fval->{$property_name})) {
$data[$k][$fkey][$property_name] = $fval
->get($property_name)
->getValue();
}
// If there is a path item, we need to handle that the pid is a
// string but we don't want to upload it. See
// https://www.drupal.org/node/2689253.
if ($field_type === 'path') {
unset($data[$k][$fkey]['pid']);
}
}
}
}
}
else {
$configured_properties = $this->lingotekConfiguration
->getFieldPropertiesLingotekEnabled($entity
->getEntityTypeId(), $entity
->bundle(), $k);
$properties = $fval
->getProperties();
foreach ($properties as $pkey => $pval) {
if (isset($configured_properties[$pkey]) && $configured_properties[$pkey]) {
$data[$k][$fkey][$pkey] = $pval
->getValue();
}
}
}
}
// If we have an entity reference, we may want to embed it.
if ($field_type === 'entity_reference' || $field_type === 'er_viewmode') {
$target_entity_type_id = $field_definitions[$k]
->getFieldStorageDefinition()
->getSetting('target_type');
foreach ($entity->{$k} as $field_item) {
$embedded_entity_id = $field_item
->get('target_id')
->getValue();
$embedded_entity = $this->entityManager
->getStorage($target_entity_type_id)
->load($embedded_entity_id);
// We may have orphan references, so ensure that they exist before
// continuing.
if ($embedded_entity !== NULL) {
if ($embedded_entity instanceof ContentEntityInterface) {
// We need to avoid cycles if we have several entity references
// referencing each other.
if (!isset($visited[$embedded_entity
->bundle()]) || !in_array($embedded_entity
->id(), $visited[$embedded_entity
->bundle()])) {
$embedded_data = $this
->getSourceData($embedded_entity, $visited);
$data[$k][$field_item
->getName()] = $embedded_data;
}
}
else {
if ($embedded_entity instanceof ConfigEntityInterface) {
$embedded_data = $this->lingotekConfigTranslation
->getSourceData($embedded_entity);
$data[$k][$field_item
->getName()] = $embedded_data;
}
}
}
else {
// If the referenced entity doesn't exist, remove the target_id
// that may be already set.
unset($data[$k]);
}
}
}
else {
if ($field_type === 'entity_reference_revisions') {
$target_entity_type_id = $field_definitions[$k]
->getFieldStorageDefinition()
->getSetting('target_type');
foreach ($entity->{$k} as $field_item) {
$embedded_entity_id = $field_item
->get('target_id')
->getValue();
$embedded_entity_revision_id = $field_item
->get('target_revision_id')
->getValue();
$embedded_entity = $this->entityManager
->getStorage($target_entity_type_id)
->loadRevision($embedded_entity_revision_id);
$embedded_data = $this
->getSourceData($embedded_entity);
$data[$k][$field_item
->getName()] = $embedded_data;
}
}
else {
if ($field_type === 'metatag') {
foreach ($entity->{$k} as $field_item) {
$metatag_serialized = $field_item
->get('value')
->getValue();
$metatags = unserialize($metatag_serialized);
if ($metatags) {
$data[$k][$field_item
->getName()] = $metatags;
}
}
}
else {
if ($field_type === 'path') {
if ($entity
->id()) {
$source = '/' . $entity
->toUrl()
->getInternalPath();
$path = \Drupal::service('path.alias_storage')
->load([
'source' => $source,
'langcode' => $entity
->language()
->getId(),
]);
$alias = $path['alias'];
if ($alias !== NULL) {
$data[$k][0]['alias'] = $alias;
}
}
}
}
}
}
}
// Embed entity metadata if there is any.
if ($entity
->id()) {
$data['_lingotek_metadata'] = [
'_entity_type_id' => $entity
->getEntityTypeId(),
'_entity_id' => $entity
->id(),
'_entity_revision' => $entity
->getRevisionId(),
];
}
return $data;
}
/**
* {@inheritdoc}
*/
public function updateEntityHash(ContentEntityInterface $entity) {
$source_data = json_encode($this
->getSourceData($entity));
$entity->lingotek_hash->value = md5($source_data);
}
/**
* {@inheritdoc}
*/
public function hasEntityChanged(ContentEntityInterface &$entity) {
// Perform the cheapest checks first.
if (isset($entity->original)) {
return $entity->lingotek_hash->value !== $entity->original->lingotek_hash->value;
}
// The following code should not be called very often, if at all.
$old_hash = $entity->lingotek_hash->value;
if (!$old_hash) {
return TRUE;
}
$source_data = json_encode($this
->getSourceData($entity));
$hash = md5($source_data);
return (bool) strcmp($hash, $old_hash);
}
/**
* {@inheritdoc}
*/
public function addTarget(ContentEntityInterface &$entity, $locale) {
$source_langcode = $entity
->getUntranslated()
->language()
->getId();
$source_locale = $this->languageLocaleMapper
->getLocaleForLangcode($source_langcode);
if ($locale == $source_locale) {
// We don't want to translate from one language to itself.
return FALSE;
}
if ($document_id = $this
->getDocumentId($entity)) {
$drupal_language = $this->languageLocaleMapper
->getConfigurableLanguageForLocale($locale);
$source_status = $this
->getSourceStatus($entity);
$current_status = $this
->getTargetStatus($entity, $drupal_language
->id());
// When a translation is in one of these states, we know that it hasn't yet been sent up to the Lingotek API,
// which means that we'll have to call addTarget() on it.
//
// TODO: should we consider STATUS_NONE as a "pristine" status?
$pristine_statuses = [
Lingotek::STATUS_REQUEST,
Lingotek::STATUS_UNTRACKED,
Lingotek::STATUS_EDITED,
];
if (in_array($current_status, $pristine_statuses)) {
if ($this->lingotek
->addTarget($document_id, $locale, $this->lingotekConfiguration
->getEntityProfile($entity))) {
$this
->setTargetStatus($entity, $drupal_language
->id(), Lingotek::STATUS_PENDING);
// If the status was "Importing", and the target was added
// successfully, we can ensure that the content is current now.
if ($source_status == Lingotek::STATUS_IMPORTING) {
$this
->setSourceStatus($entity, Lingotek::STATUS_CURRENT);
}
return TRUE;
}
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function requestTranslations(ContentEntityInterface &$entity) {
$languages = [];
if ($document_id = $this
->getDocumentId($entity)) {
$target_languages = $this->languageManager
->getLanguages();
$target_languages = array_filter($target_languages, function (LanguageInterface $language) {
$configLanguage = ConfigurableLanguage::load($language
->getId());
return $this->lingotekConfiguration
->isLanguageEnabled($configLanguage);
});
$entity_langcode = $entity
->getUntranslated()
->language()
->getId();
foreach ($target_languages as $langcode => $language) {
$locale = $this->languageLocaleMapper
->getLocaleForLangcode($langcode);
if ($langcode !== $entity_langcode) {
$source_status = $this
->getSourceStatus($entity);
$current_status = $this
->getTargetStatus($entity, $langcode);
if ($current_status !== Lingotek::STATUS_PENDING && $current_status !== Lingotek::STATUS_CURRENT && $current_status !== Lingotek::STATUS_EDITED && $current_status !== Lingotek::STATUS_READY) {
if ($this->lingotek
->addTarget($document_id, $locale, $this->lingotekConfiguration
->getEntityProfile($entity))) {
$languages[] = $langcode;
$this
->setTargetStatus($entity, $langcode, Lingotek::STATUS_PENDING);
// If the status was "Importing", and the target was added
// successfully, we can ensure that the content is current now.
if ($source_status == Lingotek::STATUS_IMPORTING) {
$this
->setSourceStatus($entity, Lingotek::STATUS_CURRENT);
}
}
}
}
}
}
return $languages;
}
/**
* {@inheritdoc}
*/
public function uploadDocument(ContentEntityInterface $entity) {
if (!empty($entity->lingotek_document_id->value)) {
return $this
->updateDocument($entity);
}
$source_data = $this
->getSourceData($entity);
$document_name = $entity
->bundle() . ' (' . $entity
->getEntityTypeId() . '): ' . $entity
->label();
$url = $entity
->hasLinkTemplate('canonical') ? $entity
->toUrl()
->setAbsolute(TRUE)
->toString() : NULL;
$profile = $this->lingotekConfiguration
->getEntityProfile($entity);
// Allow other modules to alter the data before is uploaded.
\Drupal::moduleHandler()
->invokeAll('lingotek_content_entity_document_upload', [
&$source_data,
&$entity,
&$url,
]);
$encoded_data = json_encode($source_data);
$document_id = $this->lingotek
->uploadDocument($document_name, $encoded_data, $this
->getSourceLocale($entity), $url, $profile);
if ($document_id) {
$this
->setDocumentId($entity, $document_id);
$this
->setSourceStatus($entity, Lingotek::STATUS_IMPORTING);
$this
->setTargetStatuses($entity, Lingotek::STATUS_REQUEST);
return $document_id;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function downloadDocument(ContentEntityInterface &$entity, $locale) {
if ($document_id = $this
->getDocumentId($entity)) {
$source_status = $this
->getSourceStatus($entity);
try {
$data = $this->lingotek
->downloadDocument($document_id, $locale);
} catch (LingotekApiException $exception) {
// TODO: log issue
return FALSE;
}
if ($data) {
// Check the real status, because it may still need review or anything.
$status = $this->lingotek
->getDocumentTranslationStatus($document_id, $locale);
$transaction = db_transaction();
try {
$drupal_language = $this->languageLocaleMapper
->getConfigurableLanguageForLocale($locale);
$langcode = $drupal_language
->id();
$saved = $this
->saveTargetData($entity, $langcode, $data);
if ($saved) {
// If the status was "Importing", and the target was added
// successfully, we can ensure that the content is current now.
if ($source_status == Lingotek::STATUS_IMPORTING || $source_status == Lingotek::STATUS_EDITED) {
$this
->setSourceStatus($entity, Lingotek::STATUS_CURRENT);
}
if ($status) {
$this
->setTargetStatus($entity, $langcode, Lingotek::STATUS_CURRENT);
}
else {
$this
->setTargetStatus($entity, $langcode, Lingotek::STATUS_INTERMEDIATE);
}
}
} catch (LingotekContentEntityStorageException $storageException) {
throw $storageException;
} catch (\Exception $exception) {
$transaction
->rollback();
return FALSE;
}
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function updateDocument(ContentEntityInterface &$entity) {
$source_data = $this
->getSourceData($entity);
$document_id = $this
->getDocumentId($entity);
$url = $entity
->hasLinkTemplate('canonical') ? $entity
->toUrl()
->setAbsolute(TRUE)
->toString() : NULL;
$document_name = $entity
->bundle() . ' (' . $entity
->getEntityTypeId() . '): ' . $entity
->label();
// Allow other modules to alter the data before is uploaded.
\Drupal::moduleHandler()
->invokeAll('lingotek_content_entity_document_upload', [
&$source_data,
&$entity,
&$url,
]);
$encoded_data = json_encode($source_data);
if ($this->lingotek
->updateDocument($document_id, $encoded_data, $url, $document_name)) {
$this
->setSourceStatus($entity, Lingotek::STATUS_IMPORTING);
$this
->setTargetStatuses($entity, Lingotek::STATUS_PENDING);
return $document_id;
}
return FALSE;
}
public function downloadDocuments() {
try {
$data = $this->lingotek
->downloadDocuments();
} catch (LingotekApiException $exception) {
return FALSE;
}
return $data;
}
public function downloadDocumentContent($document_id) {
try {
$data = $this->lingotek
->downloadDocumentContent($document_id);
} catch (LingotekApiException $exception) {
// TODO: log issue
return FALSE;
}
return $data;
}
/**
* {@inheritdoc}
*/
public function deleteDocument(ContentEntityInterface &$entity) {
return $this->lingotek
->deleteDocument($this
->getDocumentId($entity));
}
/**
* {@inheritdoc}
*/
public function deleteMetadata(ContentEntityInterface &$entity) {
if ($this->lingotekConfiguration
->mustDeleteRemoteAfterDisassociation()) {
$this
->deleteDocument($entity);
}
$doc_id = $this
->getDocumentId($entity);
$entity->lingotek_translation_status = NULL;
$entity->lingotek_document_id = NULL;
\Drupal::database()
->delete('lingotek_content_metadata')
->condition('document_id', $doc_id)
->condition('entity_type', $entity
->getEntityTypeId())
->condition('entity_id', $entity
->id())
->execute();
// If the entity supports revisions, ensure we don't create a new one.
if ($entity
->getEntityType()
->hasKey('revision')) {
$entity
->setNewRevision(FALSE);
}
$entity->lingotek_processed = TRUE;
$entity
->save();
}
/**
* {@inheritdoc}
*/
public function loadByDocumentId($document_id) {
$entity = NULL;
$metadata = \Drupal::database()
->select('lingotek_content_metadata', 'lcm')
->fields('lcm', [
'document_id',
'entity_type',
'entity_id',
])
->condition('document_id', $document_id)
->execute()
->fetchAssoc();
if ($metadata) {
$entity = $this->entityManager
->getStorage($metadata['entity_type'])
->load($metadata['entity_id']);
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function getAllLocalDocumentIds() {
return $metadata = \Drupal::database()
->select('lingotek_content_metadata', 'lcm')
->fields('lcm', [
'document_id',
])
->execute()
->fetchCol(0);
}
/**
* Loads the correct revision is loaded from the database, bypassing caches.
*
* @param ContentEntityInterface $entity
* The entity we want to load a revision from.
* @param int|NULL $revision
* The revision id. NULL if we don't know it.
*
* @return ContentEntityInterface
* The wanted revision of the entity.
*/
protected function loadUploadedRevision(ContentEntityInterface $entity, $revision = NULL) {
$the_revision = NULL;
$entity_type = $entity
->getEntityType();
$type = $entity
->getEntityTypeId();
if ($entity_type
->isRevisionable()) {
// If the entity type is revisionable, we need to check the proper revision.
// This may come from the uploaded data, but in case we didn't have it, we
// have to infer using the timestamp.
if ($revision !== NULL) {
$the_revision = entity_revision_load($type, $revision);
}
elseif ($revision === NULL && $entity
->hasField('revision_timestamp')) {
// Let's find the better revision based on the timestamp.
$timestamp = $this->lingotek
->getUploadedTimestamp($this
->getDocumentId($entity));
$revision = $this
->getClosestRevisionToTimestamp($entity, $timestamp);
$the_revision = entity_revision_load($type, $revision);
}
else {
// We didn't find a better option, but let's reload this one so it's not
// cached.
$the_revision = entity_revision_load($type, $entity
->getRevisionId());
}
}
else {
$the_revision = entity_load($type, $entity
->id(), TRUE);
}
return $the_revision;
}
protected function getClosestRevisionToTimestamp(ContentEntityInterface &$entity, $timestamp) {
$entity_id = $entity
->id();
$query = \Drupal::database()
->select($entity
->getEntityType()
->getRevisionDataTable(), 'nfr');
$query
->fields('nfr', [
$entity
->getEntityType()
->getKey('revision'),
]);
$query
->addJoin('INNER', $entity
->getEntityType()
->getRevisionTable(), 'nr', 'nfr.vid = nr.vid and nfr.nid = nr.nid and nfr.langcode = nr.langcode');
$query
->condition('nfr.' . $entity
->getEntityType()
->getKey('id'), $entity_id);
$query
->condition('nfr.' . $entity
->getEntityType()
->getKey('langcode'), $entity
->language()
->getId());
$query
->condition('nr.revision_timestamp', $timestamp, '<');
$query
->orderBy('nfr.changed', 'DESC');
$query
->range(0, 1);
$value = $query
->execute();
$vids = $value
->fetchAssoc();
return count($vids) === 1 ? $vids['vid'] : NULL;
}
/**
* {@inheritdoc}
*/
public function saveTargetData(ContentEntityInterface &$entity, $langcode, $data) {
// Without a defined langcode, we can't proceed
if (!$langcode) {
// TODO: log warning that downloaded translation's langcode is not enabled.
return FALSE;
}
$storage_definitions = $this->entityManager
->getFieldStorageDefinitions($entity
->getEntityTypeId());
$lock = \Drupal::lock();
$lock_name = __FUNCTION__ . ':' . $entity
->getEntityTypeId() . ':' . $entity
->id();
$held = $lock
->acquire($lock_name);
// It is critical that we acquire a lock on this entity since we want to ensure
// that our translation is saved.
if (!$held) {
if ($lock
->wait($lock_name) === FALSE) {
$held = $lock
->acquire($lock_name);
}
}
if (!$held) {
// We were unable to acquire the lock even after waiting, so we have to bail.
// (We don't have to call release() here since we never succeeded at acquiring it)
throw new \Exception(new FormattableMarkup('Unable to acquire lock for entity @id of type @type.', [
'@id' => $entity
->id(),
'@type' => $entity
->getEntityTypeId(),
]));
}
try {
// We need to load the revision that was uploaded for consistency. For that,
// we check if we have a valid revision in the response, and if not, we
// check the date of the uploaded document.
/** @var ContentEntityInterface $entity */
$revision = isset($data['_lingotek_metadata']) ? $data['_lingotek_metadata']['_entity_revision'] : NULL;
$revision = $this
->loadUploadedRevision($entity, $revision);
// Initialize the translation on the Drupal side, if necessary.
/** @var ContentEntityInterface $translation */
if (!$entity
->hasTranslation($langcode)) {
$entity
->addTranslation($langcode, $revision
->toArray());
}
$translation = $entity
->getTranslation($langcode);
foreach ($data as $name => $field_data) {
if (strpos($name, '_') === 0) {
// Skip special fields underscored.
break;
}
$field_definition = $entity
->getFieldDefinition($name);
if (($field_definition
->isTranslatable() || $field_definition
->getType() === 'entity_reference_revisions') && $this->lingotekConfiguration
->isFieldLingotekEnabled($entity
->getEntityTypeId(), $entity
->bundle(), $name)) {
// First we check if this is a entity reference, and save the translated entity.
$field_type = $field_definition
->getType();
if ($field_type === 'entity_reference' || $field_type === 'er_viewmode') {
$target_entity_type_id = $field_definition
->getFieldStorageDefinition()
->getSetting('target_type');
$index = 0;
foreach ($field_data as $field_item) {
if (isset($field_item['_lingotek_metadata'])) {
$target_entity_type_id = $field_item['_lingotek_metadata']['_entity_type_id'];
$embedded_entity_id = $field_item['_lingotek_metadata']['_entity_id'];
$embedded_entity_revision_id = $field_item['_lingotek_metadata']['_entity_revision'];
}
else {
// Try to get it from the revision itself. It may have been
// modified, so this can be a source of errors, but we need this
// because we didn't have metadata before.
$embedded_entity_id = $revision->{$name}
->get($index)
->get('target_id')
->getValue();
}
$embedded_entity = $this->entityManager
->getStorage($target_entity_type_id)
->load($embedded_entity_id);
// We may have orphan references, so ensure that they exist before
// continuing.
if ($embedded_entity !== NULL) {
// ToDo: It can be a content entity, or a config entity.
if ($embedded_entity instanceof ContentEntityInterface) {
if ($this->lingotekConfiguration
->isEnabled($embedded_entity
->getEntityTypeId(), $embedded_entity
->bundle())) {
$this
->saveTargetData($embedded_entity, $langcode, $field_item);
}
else {
\Drupal::logger('lingotek')
->warning($this
->t('Field %field not saved as it\'s referenced entity is not translatable by Lingotek', [
'%field' => $name,
]));
}
}
elseif ($embedded_entity instanceof ConfigEntityInterface) {
$this->lingotekConfigTranslation
->saveTargetData($embedded_entity, $langcode, $field_item);
}
// Now the embedded entity is saved, but we need to ensure
// the reference will be saved too.
$translation->{$name}
->set($index, $embedded_entity_id);
}
++$index;
}
}
else {
if ($field_type === 'entity_reference_revisions') {
$target_entity_type_id = $field_definition
->getFieldStorageDefinition()
->getSetting('target_type');
$index = 0;
foreach ($field_data as $field_item) {
$embedded_entity_id = $revision->{$name}
->get($index)
->get('target_id')
->getValue();
/** @var \Drupal\Core\Entity\RevisionableInterface $embedded_entity */
$embedded_entity = $this->entityManager
->getStorage($target_entity_type_id)
->load($embedded_entity_id);
$this
->saveTargetData($embedded_entity, $langcode, $field_item);
// Now the embedded entity is saved, but we need to ensure
// the reference will be saved too. Ensure it's the same revision.
$translation->{$name}
->set($index, [
'target_id' => $embedded_entity_id,
'target_revision_id' => $embedded_entity
->getRevisionId(),
]);
++$index;
}
}
else {
if ($field_type === 'path') {
$pid = NULL;
$source = '/' . $entity
->toUrl()
->getInternalPath();
/** @var \Drupal\Core\Path\AliasStorageInterface $aliasStorage */
$alias_storage = \Drupal::service('path.alias_storage');
$path = $alias_storage
->load([
'source' => $source,
'langcode' => $langcode,
]);
$original_path = $alias_storage
->load([
'source' => $source,
'langcode' => $entity
->getUntranslated()
->language()
->getId(),
]);
if ($path) {
$pid = $path['pid'];
}
$alias = $field_data[0]['alias'];
// Validate the alias before saving.
if (!\Drupal::pathValidator()
->isValid($alias)) {
\Drupal::logger('lingotek')
->warning($this
->t('Alias for %type %label in language %langcode not saved, invalid uri "%uri"', [
'%type' => $entity
->getEntityTypeId(),
'%label' => $entity
->label(),
'%langcode' => $langcode,
'%uri' => $alias,
]));
// Default to the original path.
$alias = $original_path ? $original_path['alias'] : $source;
}
if ($alias !== NULL) {
\Drupal::service('path.alias_storage')
->save($source, $alias, $langcode, $pid);
}
}
else {
if ($field_type === 'metatag') {
$index = 0;
foreach ($field_data as $field_item) {
$metatag_value = serialize($field_item);
$translation->{$name}
->set($index, $metatag_value);
++$index;
}
}
else {
// Initialize delta in case there are no items in $field_data.
$delta = -1;
// Save regular fields.
foreach ($field_data as $delta => $delta_data) {
foreach ($delta_data as $property => $property_data) {
$property_definition = $storage_definitions[$name]
->getPropertyDefinition($property);
$data_type = $property_definition
->getDataType();
if ($data_type === 'uri') {
// Validate an uri.
if (!\Drupal::pathValidator()
->isValid($property_data)) {
\Drupal::logger('lingotek')
->warning($this
->t('Field %field for %type %label in language %langcode not saved, invalid uri "%uri"', [
'%field' => $name,
'%type' => $entity
->getEntityTypeId(),
'%label' => $entity
->label(),
'%langcode' => $langcode,
'%uri' => $property_data,
]));
// Let's default to the original value given that there was a problem.
$property_data = $revision
->get($name)
->offsetGet($delta)->{$property};
}
}
if (method_exists($translation
->get($name)
->offsetGet($delta), "set")) {
$translation
->get($name)
->offsetGet($delta)
->set($property, $property_data);
}
elseif ($translation
->get($name)) {
$translation
->get($name)
->appendItem()
->set($property, $property_data);
}
}
}
// Remove the rest of deltas that were no longer found in the document downloaded from lingotek.
$continue = TRUE;
while ($continue) {
if ($translation
->get($name)
->offsetExists($delta + 1)) {
$translation
->get($name)
->removeItem($delta + 1);
}
else {
$continue = FALSE;
}
}
}
}
}
}
}
}
// We need to set the content_translation source so the files are synced
// properly. See https://www.drupal.org/node/2544696 for more information.
$translation
->set('content_translation_source', $entity
->getUntranslated()
->language()
->getId());
$entity->lingotek_processed = TRUE;
// Allow other modules to alter the translation before is saved.
\Drupal::moduleHandler()
->invokeAll('lingotek_content_entity_translation_presave', [
&$translation,
$langcode,
$data,
]);
$status_field = $entity
->getEntityType()
->getKey('status');
$status_field_definition = $entity
->getFieldDefinition($status_field);
if ($status_field_definition !== NULL && $status_field_definition
->isTranslatable()) {
$status_setting = $this->lingotekConfiguration
->getPreference('target_download_status');
if ($status_setting !== "same-as-source") {
$status_value = $status_setting === 'published' ? NODE_PUBLISHED : NODE_NOT_PUBLISHED;
$translation
->set($status_field, $status_value);
}
}
$translation
->save();
return $entity;
} catch (EntityStorageException $storage_exception) {
throw new LingotekContentEntityStorageException($entity, $storage_exception);
} finally {
// Ensure the lock is released, even if we crash.
$lock
->release($lock_name);
}
}
}