Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions Classes/ContentRepository/NodeTranslationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;
Expand Down Expand Up @@ -455,4 +487,124 @@ public function resetContextCache(): void
{
$this->contextFirstLevelCache = [];
}

/**
* Normalize a repeatable property value to an array format
*
* @param mixed $propertyValue
* @return array<mixed>|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<mixed> $repeatableValue
* @param array<string> $translatableSubProperties
* @param string $targetLanguage
* @param string $sourceLanguage
* @return array<mixed>
*/
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ public function getTranslationConnector(): ?TranslationConnectorInterface
{
return $this->translationConnector;
}

public function isRepeatable(): bool
{
return false;
}
}
32 changes: 32 additions & 0 deletions Classes/Domain/TranslatableProperty/TranslatablePropertyNames.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, TranslatableRepeatablePropertyName>
*/
public function getRepeatableProperties(): array
{
/** @var array<int, TranslatableRepeatablePropertyName> $result */
$result = array_values(array_filter(
$this->translatableProperties,
fn($prop) => $prop->isRepeatable()
));
return $result;
}

/**
* @return \ArrayIterator<int, TranslatablePropertyName>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ class TranslatablePropertyNamesFactory
*/
protected $objectManager;

/**
* @var bool
* @Flow\InjectConfiguration(path="nodeTranslation.translateRepeatableFields")
*/
protected $translateRepeatableFields;

/**
* @var array<string, TranslatablePropertyNames>
*/
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Sitegeist\LostInTranslation\Domain\TranslatableProperty;

/**
* Represents a repeatable property that contains translatable sub-properties
*/
class TranslatableRepeatablePropertyName extends TranslatablePropertyName
{
/**
* @var array<string>
*/
protected array $translatableSubProperties;

/**
* @param string $name
* @param array<string> $translatableSubProperties
*/
public function __construct(string $name, array $translatableSubProperties)
{
parent::__construct($name);
$this->translatableSubProperties = $translatableSubProperties;
}

/**
* @return array<string>
*/
public function getTranslatableSubProperties(): array
{
return $this->translatableSubProperties;
}

public function isRepeatable(): bool
{
return true;
}
}
7 changes: 7 additions & 0 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down