diff --git a/Classes/ContentRepository/NodeTranslationService.php b/Classes/ContentRepository/NodeTranslationService.php index ca0658b..f77ba00 100644 --- a/Classes/ContentRepository/NodeTranslationService.php +++ b/Classes/ContentRepository/NodeTranslationService.php @@ -288,8 +288,25 @@ public function translateNode(NodeInterface $sourceNode, NodeInterface $targetNo /** @phpstan-ignore arguments.count */ $properties = (array)$sourceNode->getProperties(true); $propertiesToTranslate = []; + $repeatablePropertiesToTranslate = []; foreach ($properties as $propertyName => $propertyValue) { + // Check if this is a translatable repeatable property + $repeatableProperty = $translatableProperties->isTranslatableRepeatable($propertyName); + if ($repeatableProperty !== null) { + // Repeatable properties can be: JSON string, array, or object with toArray/jsonSerialize + $repeatableData = $this->normalizeRepeatableValue($propertyValue); + if ($repeatableData !== null) { + $repeatablePropertiesToTranslate[$propertyName] = [ + 'value' => $repeatableData, + 'subProperties' => $repeatableProperty->getTranslatableSubProperties() + ]; + unset($properties[$propertyName]); + continue; + } + } + + // Handle regular string properties if (empty($propertyValue)) { continue; } @@ -318,6 +335,17 @@ public function translateNode(NodeInterface $sourceNode, NodeInterface $targetNo $properties = array_merge($translatedProperties, $properties); } + // Translate repeatable properties + foreach ($repeatablePropertiesToTranslate as $propertyName => $config) { + $translatedValue = $this->translateRepeatableProperty( + $config['value'], + $config['subProperties'], + $targetLanguage, + $sourceLanguage + ); + $properties[$propertyName] = $translatedValue; + } + foreach ($properties as $propertyName => $propertyValue) { // Make sure the uriPathSegment is valid if ($propertyName === 'uriPathSegment' && !preg_match('/^[a-z0-9\-]+$/i', $propertyValue)) { @@ -329,6 +357,10 @@ public function translateNode(NodeInterface $sourceNode, NodeInterface $targetNo if (is_object($targetValue)) { $targetValue = $connector->applyTranslations($targetValue, $propertyValue); } + } else { + // For repeatable fields or other array properties without a connector, + // use the translated array value directly + $targetValue = $propertyValue; } } else { $targetValue = $propertyValue; @@ -455,4 +487,124 @@ public function resetContextCache(): void { $this->contextFirstLevelCache = []; } + + /** + * Normalize a repeatable property value to an array format + * + * @param mixed $propertyValue + * @return array|null Returns array if successfully normalized, null otherwise + */ + protected function normalizeRepeatableValue($propertyValue): ?array + { + // Already an array + if (is_array($propertyValue)) { + return $propertyValue; + } + + // JSON string + if (is_string($propertyValue)) { + $decoded = json_decode($propertyValue, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + return $decoded; + } + return null; + } + + // Object with toArray method (like Mireo\RepeatableFields\Model\Repeatable) + if (is_object($propertyValue)) { + if (method_exists($propertyValue, 'toArray')) { + $array = $propertyValue->toArray(); + if (is_array($array)) { + return $array; + } + } + // Try jsonSerialize + if ($propertyValue instanceof \JsonSerializable) { + $serialized = $propertyValue->jsonSerialize(); + if (is_array($serialized)) { + return $serialized; + } + } + } + + return null; + } + + /** + * Translate translatable sub-properties within a repeatable property + * + * @param array $repeatableValue + * @param array $translatableSubProperties + * @param string $targetLanguage + * @param string $sourceLanguage + * @return array + */ + protected function translateRepeatableProperty( + array $repeatableValue, + array $translatableSubProperties, + string $targetLanguage, + string $sourceLanguage + ): array { + // Handle repeatable structure: can be array of items or byGroup structure + $isByGroupStructure = isset($repeatableValue['byGroup']); + $items = $isByGroupStructure ? $repeatableValue['byGroup'] : $repeatableValue; + + if (!is_array($items)) { + return $repeatableValue; + } + + // Collect all strings to translate (for batch API call) + $stringsToTranslate = []; + $itemKeys = []; // Store the original keys for proper reassignment + + foreach ($items as $itemIndex => $item) { + if (!is_array($item)) { + continue; + } + $itemKeys[] = $itemIndex; + foreach ($translatableSubProperties as $subPropertyName) { + if (isset($item[$subPropertyName]) && is_string($item[$subPropertyName])) { + $value = $item[$subPropertyName]; + $trimmedValue = trim(strip_tags($value)); + if (!empty($trimmedValue)) { + // Use a separator that won't appear in property names + $mappingKey = $itemIndex . '::' . $subPropertyName; + $stringsToTranslate[$mappingKey] = $value; + } + } + } + } + + if (empty($stringsToTranslate)) { + return $repeatableValue; + } + + // Batch translate all strings + $translatedStrings = $this->translationService->translate( + $stringsToTranslate, + $targetLanguage, + $sourceLanguage + ); + + // Apply translations back to the structure + foreach ($translatedStrings as $mappingKey => $translatedValue) { + $parts = explode('::', $mappingKey, 2); + if (count($parts) !== 2) { + continue; + } + [$itemIndex, $subPropertyName] = $parts; + // Handle both numeric and string keys + $key = is_numeric($itemIndex) ? (int)$itemIndex : $itemIndex; + if (isset($items[$key]) && is_array($items[$key])) { + $items[$key][$subPropertyName] = $translatedValue; + } + } + + // Restore byGroup structure if needed + if ($isByGroupStructure) { + return ['byGroup' => $items]; + } + + return $items; + } } diff --git a/Classes/Domain/TranslatableProperty/TranslatablePropertyName.php b/Classes/Domain/TranslatableProperty/TranslatablePropertyName.php index 3da51e8..9642669 100644 --- a/Classes/Domain/TranslatableProperty/TranslatablePropertyName.php +++ b/Classes/Domain/TranslatableProperty/TranslatablePropertyName.php @@ -40,4 +40,9 @@ public function getTranslationConnector(): ?TranslationConnectorInterface { return $this->translationConnector; } + + public function isRepeatable(): bool + { + return false; + } } diff --git a/Classes/Domain/TranslatableProperty/TranslatablePropertyNames.php b/Classes/Domain/TranslatableProperty/TranslatablePropertyNames.php index 969ce1f..c8d9ae3 100644 --- a/Classes/Domain/TranslatableProperty/TranslatablePropertyNames.php +++ b/Classes/Domain/TranslatableProperty/TranslatablePropertyNames.php @@ -44,6 +44,38 @@ public function getTranslationObjectConnector(string $propertyName): ?Translatio return null; } + /** + * Check if a property is a repeatable property with translatable sub-properties + * + * @param string $propertyName + * @return TranslatableRepeatablePropertyName|null + */ + public function isTranslatableRepeatable(string $propertyName): ?TranslatableRepeatablePropertyName + { + foreach ($this->translatableProperties as $property) { + if ($property->getName() === $propertyName && $property->isRepeatable()) { + /** @var TranslatableRepeatablePropertyName $property */ + return $property; + } + } + return null; + } + + /** + * Get all repeatable properties + * + * @return array + */ + public function getRepeatableProperties(): array + { + /** @var array $result */ + $result = array_values(array_filter( + $this->translatableProperties, + fn($prop) => $prop->isRepeatable() + )); + return $result; + } + /** * @return \ArrayIterator */ diff --git a/Classes/Domain/TranslatableProperty/TranslatablePropertyNamesFactory.php b/Classes/Domain/TranslatableProperty/TranslatablePropertyNamesFactory.php index 1c27a60..7dfc75b 100644 --- a/Classes/Domain/TranslatableProperty/TranslatablePropertyNamesFactory.php +++ b/Classes/Domain/TranslatableProperty/TranslatablePropertyNamesFactory.php @@ -35,6 +35,12 @@ class TranslatablePropertyNamesFactory */ protected $objectManager; + /** + * @var bool + * @Flow\InjectConfiguration(path="nodeTranslation.translateRepeatableFields") + */ + protected $translateRepeatableFields; + /** * @var array */ @@ -53,6 +59,30 @@ public function createForNodeType(NodeType $nodeType): TranslatablePropertyNames continue; } + // Handle repeatable properties + if ($this->translateRepeatableFields && $type === 'repeatable') { + $subProperties = $propertyDefinition['ui']['inspector']['editorOptions']['properties'] ?? []; + $translatableSubProperties = []; + + foreach ($subProperties as $subPropertyName => $subPropertyDefinition) { + $subPropertyType = $subPropertyDefinition['type'] ?? 'string'; + if ($subPropertyType !== 'string') { + continue; + } + if (isset($subPropertyDefinition['options']['automaticTranslation']) && !$subPropertyDefinition['options']['automaticTranslation']) { + continue; + } + if ($subPropertyDefinition['options']['automaticTranslation'] ?? false) { + $translatableSubProperties[] = $subPropertyName; + } + } + + if (!empty($translatableSubProperties)) { + $translateProperties[] = new TranslatableRepeatablePropertyName($propertyName, $translatableSubProperties); + } + continue; + } + // @deprecated Fallback for renamed setting translateOnAdoption -> automaticTranslation $automaticTranslationIsEnabled = $propertyDefinition[ 'options' ][ 'automaticTranslation' ] ?? ($propertyDefinition[ 'options' ][ 'translateOnAdoption' ] ?? null); diff --git a/Classes/Domain/TranslatableProperty/TranslatableRepeatablePropertyName.php b/Classes/Domain/TranslatableProperty/TranslatableRepeatablePropertyName.php new file mode 100644 index 0000000..c1b61f7 --- /dev/null +++ b/Classes/Domain/TranslatableProperty/TranslatableRepeatablePropertyName.php @@ -0,0 +1,39 @@ + + */ + protected array $translatableSubProperties; + + /** + * @param string $name + * @param array $translatableSubProperties + */ + public function __construct(string $name, array $translatableSubProperties) + { + parent::__construct($name); + $this->translatableSubProperties = $translatableSubProperties; + } + + /** + * @return array + */ + public function getTranslatableSubProperties(): array + { + return $this->translatableSubProperties; + } + + public function isRepeatable(): bool + { + return true; + } +} diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 01fb769..e13c531 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -70,6 +70,13 @@ Sitegeist: # translateInlineEditables: true + # + # Enable translation of repeatable field sub-properties. + # Sub-properties within repeatable fields must have options.automaticTranslation: true + # to be translated. This feature requires the Mireo.RepeatableFields package. + # + translateRepeatableFields: true + # # The name of the language dimension. Usually needs no modification #