-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #255 from openeuropa/EWPP-3263
EWPP-3263: Translation multivalue submodule
- Loading branch information
Showing
53 changed files
with
2,588 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
modules/oe_translation_local/modules/oe_translation_multivalue/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# OpenEuropa Translation Multivalue | ||
|
||
This submodule can be used to solve a very particular, but annoying, issue with the way the local translation system works. | ||
|
||
## The problem (scenario) | ||
|
||
* You have a node with a translatable multivalue field, with 2 values. The key here is that it needs to be multivalue, but not an entity reference. A regular one like Textfield or Link. | ||
* You translate the node and its multivalue field values. | ||
* You edit the node and you reorder the values in the multivalue field. | ||
* You translate again using local translation and the pre-filled translation values no longer matches the delta for the multivalue field. Because in the previous version the deltas were reversed. So if you save the translation without paying | ||
attention, you'll end up with mixed up translation values. | ||
|
||
This problem is caused by the fact on the local translation form, the system does a best effort to pre-fill with translation values from the previous content version. Most of the time, it manages. Even with values that are within | ||
multiple referenced entities. It cannot, however, do so on simple multivalue fields because it cannot guess that they were reversed or any added. All it can check is the delta. | ||
|
||
## The solution | ||
|
||
Installing the current module gives the possibility to add a new column to multivalue fields called `translation_id`. So whenever a value is saved, a unique ID is generated for it. And based on this ID, the | ||
system can then track which value is at which delta to prefill. | ||
|
||
## How it works | ||
|
||
It works by overriding the field item class for a given field and adding a new table column and property to track this translation ID. Moreover, it does the handling for saving this ID when synchronising the translations | ||
as well. | ||
|
||
## How to use | ||
|
||
Go to the storage settings of a multivalue translatable field and check the box `Translation multivalue`. This will create the column and add the property. | ||
|
||
Note that this only works for fields which don't have any data inside. | ||
|
||
If you already have data in the field, or you have an existing site where you need to turn this feature one, you need to do an update path. For this, there is a helper method: `TranslationMultivalueColumnInstaller::installColumn`. | ||
|
||
To this helper you need to pass the field name in the format `entity_type.field_name`. If you do this on multiple fields with lots of data, make sure you batch this per field to avoid problems. | ||
|
||
**Note that for the update, the process creates a backup table, truncates the field tables, adds the column and then sets | ||
back the values. So make sure you thoroughly test your process before deploying to production to avoid any issues**. | ||
|
||
## Support | ||
|
||
Currently, only certain field types are supported. You can check `oe_translation_multivalue_field_types()` for which field types are altered for this. | ||
|
||
If you need support for other field types, open an issue or a ticket on the EWPP board. | ||
|
23 changes: 23 additions & 0 deletions
23
...ocal/modules/oe_translation_multivalue/config/schema/oe_translation_multivalue.schema.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
field.storage_settings.description_list_field: | ||
type: mapping | ||
label: 'Description list storage settings' | ||
mapping: | ||
translation_multivalue: | ||
type: boolean | ||
label: Translation multivalue | ||
|
||
field.storage_settings.address: | ||
type: mapping | ||
label: 'Address storage settings' | ||
mapping: | ||
translation_multivalue: | ||
type: boolean | ||
label: Translation multivalue | ||
|
||
field.storage_settings.timeline_field: | ||
type: mapping | ||
label: 'Timeline storage settings' | ||
mapping: | ||
translation_multivalue: | ||
type: boolean | ||
label: Translation multivalue |
8 changes: 8 additions & 0 deletions
8
...oe_translation_local/modules/oe_translation_multivalue/oe_translation_multivalue.info.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
name: OpenEuropa Translation Local Multivalue | ||
description: Provides IDs for the values of multivalue fields to aid in the pre-filling of translation values in the local translation form | ||
package: OpenEuropa | ||
|
||
type: module | ||
core_version_requirement: ^9.4 || ^10 | ||
dependencies: | ||
- oe_translation:oe_translation_local |
260 changes: 260 additions & 0 deletions
260
...s/oe_translation_local/modules/oe_translation_multivalue/oe_translation_multivalue.module
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
<?php | ||
|
||
/** | ||
* @file | ||
* Contains hook implementations for oe_translation_multivalue. | ||
*/ | ||
|
||
declare(strict_types = 1); | ||
|
||
use Drupal\Component\Uuid\Uuid; | ||
use Drupal\Core\Entity\ContentEntityInterface; | ||
use Drupal\Core\Entity\EntityInterface; | ||
use Drupal\Core\Form\FormStateInterface; | ||
use Drupal\oe_translation_multivalue\AddressTranslationMultivalueSourceFieldProcessor; | ||
use Drupal\oe_translation_multivalue\DescriptionListTranslationMultivalueSourceFieldProcessor; | ||
use Drupal\oe_translation_multivalue\FieldItemOverrides\AddressItemMultiple; | ||
use Drupal\oe_translation_multivalue\FieldItemOverrides\DescriptionListItemMultiple; | ||
use Drupal\oe_translation_multivalue\FieldItemOverrides\LinkDescriptionItemMultiple; | ||
use Drupal\oe_translation_multivalue\FieldItemOverrides\LinkItemMultiple; | ||
use Drupal\oe_translation_multivalue\FieldItemOverrides\StringItemMultiple; | ||
use Drupal\oe_translation_multivalue\FieldItemOverrides\TimelineItemMultiple; | ||
use Drupal\oe_translation_multivalue\FieldItemOverrides\TypedLinkItemMultiple; | ||
use Drupal\oe_translation_multivalue\Form\LocalTranslationRequestForm; | ||
use Drupal\oe_translation_multivalue\MultivalueTranslationSourceFieldProcessor; | ||
use Drupal\oe_translation_multivalue\TimelineTranslationMultivalueSourceFieldProcessor; | ||
|
||
/** | ||
* Implements hook_entity_type_alter(). | ||
*/ | ||
function oe_translation_multivalue_entity_type_alter(array &$entity_types) { | ||
if (!isset($entity_types['oe_translation_request'])) { | ||
return; | ||
} | ||
|
||
$entity_type = $entity_types['oe_translation_request']; | ||
$entity_type->setFormClass('local_translation', LocalTranslationRequestForm::class); | ||
} | ||
|
||
/** | ||
* Implements hook_element_info_alter(). | ||
*/ | ||
function oe_translation_multivalue_element_info_alter(array &$info) { | ||
if (isset($info['address'])) { | ||
$info['address']['#process'][] = 'oe_translation_multivalue_address_process'; | ||
} | ||
} | ||
|
||
/** | ||
* Processor for the Address element. | ||
* | ||
* We need to add a hidden form value on the element with a default value | ||
* we set in the widget alter. | ||
* | ||
* @param array $element | ||
* The element. | ||
* @param \Drupal\Core\Form\FormStateInterface $form_state | ||
* The form state. | ||
* @param array $complete_form | ||
* The form. | ||
* | ||
* @return array | ||
* The element. | ||
*/ | ||
function oe_translation_multivalue_address_process(array &$element, FormStateInterface $form_state, array &$complete_form) { | ||
if (!isset($element['#default_value']['translation_id'])) { | ||
return $element; | ||
} | ||
$element['translation_id'] = [ | ||
'#type' => 'hidden', | ||
'#value' => $element['#default_value']['translation_id'], | ||
]; | ||
return $element; | ||
} | ||
|
||
/** | ||
* Get a list of the form widgets that should be altered. | ||
* | ||
* These are the widgets we alter the form of in order to put a hidden | ||
* translation_id form element with a random ID. | ||
* | ||
* @return string[] | ||
* The widget IDs. | ||
*/ | ||
function oe_translation_multivalue_widgets() { | ||
$field_types = oe_translation_multivalue_field_types(); | ||
$options = []; | ||
$plugin_manager = \Drupal::service('plugin.manager.field.widget'); | ||
foreach ($field_types as $field_type) { | ||
$options += $plugin_manager->getOptions($field_type); | ||
} | ||
|
||
return array_keys($options); | ||
} | ||
|
||
/** | ||
* Get a list of the field types we are altering. | ||
* | ||
* @todo make this extendable. | ||
* | ||
* @return string[] | ||
* The field IDs. | ||
*/ | ||
function oe_translation_multivalue_field_types() { | ||
return [ | ||
'link', | ||
'address', | ||
'description_list_field', | ||
'link_description', | ||
'string', | ||
'timeline_field', | ||
'typed_link', | ||
]; | ||
} | ||
|
||
/** | ||
* Implements hook_field_widget_complete_form_alter(). | ||
*/ | ||
function oe_translation_multivalue_field_widget_complete_form_alter(&$field_widget_complete_form, FormStateInterface $form_state, $context) { | ||
$widgets = oe_translation_multivalue_widgets(); | ||
$widget = $context['widget']; | ||
if (!in_array($widget->getPluginId(), $widgets)) { | ||
return; | ||
} | ||
$items = $context['items']; | ||
$property_definitions = $items->getFieldDefinition()->getItemDefinition()->getPropertyDefinitions(); | ||
if (!isset($property_definitions['translation_id'])) { | ||
return; | ||
} | ||
|
||
$id_generator = \Drupal::service('oe_translation_multivalue.translation_id_generator'); | ||
$field_id = $items->getFieldDefinition()->getFieldStorageDefinition()->id(); | ||
foreach ($items as $delta => $item) { | ||
// If the field delta doesn't yet have a translation ID, generate the next | ||
// one and set that. However, if we do generate a new one, we need to also | ||
// increment it here with each iteration because each delta needs to | ||
// receive a new one. | ||
$translation_id = $item->translation_id && Uuid::isValid($item->translation_id) ? $item->translation_id : NULL; | ||
if (!$translation_id) { | ||
$translation_id = $id_generator->generateTranslationUuid($field_id); | ||
} | ||
|
||
// For address, we have an exception because it uses a form element. | ||
if ($widget->getPluginId() === 'address_default') { | ||
$field_widget_complete_form['widget'][$delta]['address']['#default_value']['translation_id'] = $translation_id; | ||
continue; | ||
} | ||
$field_widget_complete_form['widget'][$delta]['translation_id'] = [ | ||
'#type' => 'hidden', | ||
'#value' => $translation_id, | ||
]; | ||
} | ||
} | ||
|
||
/** | ||
* Implements hook_entity_presave(). | ||
* | ||
* When we save a content entity that may have fields that contain the | ||
* translation_id column, check if by any chance an attempt is being made to | ||
* save them without one. If so, set a value. This can happen when entities | ||
* are created programmatically. | ||
*/ | ||
function oe_translation_multivalue_entity_presave(EntityInterface $entity) { | ||
if (!$entity instanceof ContentEntityInterface) { | ||
return; | ||
} | ||
|
||
$field_types = oe_translation_multivalue_field_types(); | ||
|
||
$field_definitions = $entity->getFieldDefinitions(); | ||
$fields = []; | ||
foreach ($field_definitions as $field_name => $field_definition) { | ||
if (in_array($field_definition->getType(), $field_types)) { | ||
$fields[] = $field_name; | ||
} | ||
} | ||
|
||
$id_generator = \Drupal::service('oe_translation_multivalue.translation_id_generator'); | ||
|
||
foreach ($fields as $field) { | ||
if ($entity->get($field)->isEmpty()) { | ||
continue; | ||
} | ||
|
||
if (!in_array('translation_id', $entity->get($field)->getFieldDefinition()->getFieldStorageDefinition()->getPropertyNames())) { | ||
continue; | ||
} | ||
|
||
$field_id = $entity->getEntityTypeId() . '.' . $field; | ||
foreach ($entity->get($field) as $item) { | ||
if (!isset($item->getProperties()['translation_id'])) { | ||
continue; | ||
} | ||
|
||
if ($item->translation_id) { | ||
continue; | ||
} | ||
|
||
$item->translation_id = $id_generator->generateTranslationUuid($field_id); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Implements hook_field_info_alter(). | ||
*/ | ||
function oe_translation_multivalue_field_info_alter(&$info) { | ||
if (isset($info['link'])) { | ||
$info['link']['class'] = LinkItemMultiple::class; | ||
$info['link']['oe_translation_source_field_processor'] = MultivalueTranslationSourceFieldProcessor::class; | ||
} | ||
|
||
if (isset($info['description_list_field'])) { | ||
$info['description_list_field']['class'] = DescriptionListItemMultiple::class; | ||
$info['description_list_field']['oe_translation_source_field_processor'] = DescriptionListTranslationMultivalueSourceFieldProcessor::class; | ||
} | ||
|
||
if (isset($info['timeline_field'])) { | ||
$info['timeline_field']['class'] = TimelineItemMultiple::class; | ||
$info['timeline_field']['oe_translation_source_field_processor'] = TimelineTranslationMultivalueSourceFieldProcessor::class; | ||
} | ||
|
||
if (isset($info['address'])) { | ||
$info['address']['class'] = AddressItemMultiple::class; | ||
$info['address']['oe_translation_source_field_processor'] = AddressTranslationMultivalueSourceFieldProcessor::class; | ||
} | ||
|
||
if (isset($info['typed_link'])) { | ||
$info['typed_link']['class'] = TypedLinkItemMultiple::class; | ||
$info['typed_link']['oe_translation_source_field_processor'] = MultivalueTranslationSourceFieldProcessor::class; | ||
} | ||
|
||
if (isset($info['string'])) { | ||
$info['string']['class'] = StringItemMultiple::class; | ||
$info['string']['oe_translation_source_field_processor'] = MultivalueTranslationSourceFieldProcessor::class; | ||
} | ||
|
||
if (isset($info['link_description'])) { | ||
$info['link_description']['class'] = LinkDescriptionItemMultiple::class; | ||
$info['link_description']['oe_translation_source_field_processor'] = MultivalueTranslationSourceFieldProcessor::class; | ||
} | ||
} | ||
|
||
/** | ||
* Implements hook_config_schema_info_alter(). | ||
*/ | ||
function oe_translation_multivalue_config_schema_info_alter(&$definitions) { | ||
$field_types = oe_translation_multivalue_field_types(); | ||
foreach ($field_types as $plugin_id) { | ||
$schema_id = 'field.storage_settings.' . $plugin_id; | ||
if (!isset($definitions[$schema_id])) { | ||
// If the schema doesn't exist already, we cannot add it as we get an | ||
// exception. For those, we need to define them in the schema yml. | ||
continue; | ||
} | ||
$definitions[$schema_id]['mapping']['translation_multivalue'] = [ | ||
'type' => 'boolean', | ||
'label' => 'Translation multivalue', | ||
]; | ||
} | ||
} |
4 changes: 4 additions & 0 deletions
4
...ranslation_local/modules/oe_translation_multivalue/oe_translation_multivalue.services.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
services: | ||
oe_translation_multivalue.translation_id_generator: | ||
class: Drupal\oe_translation_multivalue\MultivalueTranslationIdGenerator | ||
arguments: ['@entity_type.manager', '@uuid', '@database'] |
24 changes: 24 additions & 0 deletions
24
...odules/oe_translation_multivalue/src/AddressTranslationMultivalueSourceFieldProcessor.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace Drupal\oe_translation_multivalue; | ||
|
||
use Drupal\Core\Field\FieldItemListInterface; | ||
use Drupal\oe_translation\TranslationSourceFieldProcessor\AddressFieldProcessor; | ||
|
||
/** | ||
* Translation source manager field processor for the Address field. | ||
*/ | ||
class AddressTranslationMultivalueSourceFieldProcessor extends AddressFieldProcessor { | ||
|
||
use MultivalueFieldProcessorTrait; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function setTranslations($field_data, FieldItemListInterface $field): void { | ||
$this->setMultivalueTranslations($field_data, $field); | ||
} | ||
|
||
} |
Oops, something went wrong.