Skip to content

Commit

Permalink
Merge pull request #35 from sitegeist/feature/contentCollectionCompar…
Browse files Browse the repository at this point in the history
…ison

FEATURE: Optional overview to compare translation on collection and document level
  • Loading branch information
mficzel authored Jan 15, 2024
2 parents 50a0ec5 + 699ea7d commit 6a0d0f2
Show file tree
Hide file tree
Showing 33 changed files with 5,606 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php-versions: ['7.4']
php-versions: ['8.0']
neos-versions: ['7.3']
include:
- php-versions: '8.1'
Expand Down
42 changes: 20 additions & 22 deletions Classes/ContentRepository/NodeTranslationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Neos\Flow\Annotations as Flow;
use Neos\Neos\Service\PublishingService;
use Neos\Neos\Utility\NodeUriPathSegmentGenerator;
use Sitegeist\LostInTranslation\Domain\TranslatableProperty\TranslatablePropertyNamesFactory;
use Sitegeist\LostInTranslation\Domain\TranslationServiceInterface;

/**
Expand Down Expand Up @@ -60,7 +61,7 @@ class NodeTranslationService

/**
* @Flow\InjectConfiguration(package="Neos.ContentRepository", path="contentDimensions")
* @var array
* @var array<string,array{'default': string, 'defaultPreset': string, 'presets': array<string,mixed> }>
*/
protected $contentDimensionConfiguration;

Expand All @@ -87,13 +88,19 @@ class NodeTranslationService
*/
protected $nodeUriPathSegmentGenerator;

/**
* @Flow\Inject
* @var TranslatablePropertyNamesFactory
*/
protected $translatablePropertiesFactory;

/**
* @param NodeInterface $node
* @param Context $context
* @param $recursive
* @param bool $recursive
* @return void
*/
public function afterAdoptNode(NodeInterface $node, Context $context, $recursive): void
public function afterAdoptNode(NodeInterface $node, Context $context, bool $recursive): void
{
if (!$this->enabled) {
return;
Expand All @@ -111,8 +118,10 @@ public function afterAdoptNode(NodeInterface $node, Context $context, $recursive
return;
}

$adoptedNode = $context->getNodeByIdentifier((string)$node->getNodeAggregateIdentifier());
$this->translateNode($node, $adoptedNode, $context);
$adoptedNode = $context->getNodeByIdentifier((string)$node->getIdentifier());
if ($adoptedNode instanceof NodeInterface) {
$this->translateNode($node, $adoptedNode, $context);
}
}

/**
Expand Down Expand Up @@ -150,7 +159,7 @@ public function afterNodePublish(NodeInterface $node, Workspace $workspace): voi
*/
public function translateNode(NodeInterface $sourceNode, NodeInterface $targetNode, Context $context): void
{
$propertyDefinitions = $sourceNode->getNodeType()->getProperties();
$translatableProperties = $this->translatablePropertiesFactory->createForNodeType($sourceNode->getNodeType());

$sourceDimensionValue = $sourceNode->getContext()->getTargetDimensions()[$this->languageDimensionName];
$targetDimensionValue = $context->getTargetDimensions()[$this->languageDimensionName];
Expand All @@ -172,31 +181,20 @@ public function translateNode(NodeInterface $sourceNode, NodeInterface $targetNo
return;
}

$properties = (array)$sourceNode->getProperties(true);
$properties = (array)$sourceNode->getProperties();
$propertiesToTranslate = [];
foreach ($properties as $propertyName => $propertyValue) {
if (empty($propertyValue)) {
if (empty($propertyValue) || !is_string($propertyValue)) {
continue;
}
if (!array_key_exists($propertyName, $propertyDefinitions)) {
continue;
}
if (!isset($propertyDefinitions[$propertyName]['type']) || $propertyDefinitions[$propertyName]['type'] != 'string' || !is_string($propertyValue)) {
if (!$translatableProperties->isTranslatable($propertyName)) {
continue;
}
if ((trim(strip_tags($propertyValue))) == "") {
continue;
}

$isInlineEditable = $propertyDefinitions[$propertyName]['ui']['inlineEditable'] ?? false;
// @deprecated Fallback for renamed setting translateOnAdoption -> automaticTranslation
$isTranslateEnabledForProperty = $propertyDefinitions[$propertyName]['options']['automaticTranslation'] ?? ($propertyDefinitions[$propertyName]['options']['translateOnAdoption'] ?? null);
$translateProperty = $isTranslateEnabledForProperty == true || (is_null($isTranslateEnabledForProperty) && $this->translateRichtextProperties && $isInlineEditable == true);

if ($translateProperty) {
$propertiesToTranslate[$propertyName] = $propertyValue;
unset($properties[$propertyName]);
}
$propertiesToTranslate[$propertyName] = $propertyValue;
unset($properties[$propertyName]);
}

if (count($propertiesToTranslate) > 0) {
Expand Down
8 changes: 4 additions & 4 deletions Classes/Controller/LostInTranslationModuleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,23 @@ class LostInTranslationModuleController extends AbstractModuleController
protected $view;


public function indexAction()
public function indexAction(): void
{
$status = $this->translationService->getStatus();
$this->view->assign('status', $status);
}

public function setCustomKeyAction()
public function setCustomKeyAction(): void
{
}

public function storeCustomKeyAction(string $key)
public function storeCustomKeyAction(string $key): void
{
$this->apiKeyCache->set(Package::API_KEY_CACHE_ID, $key);
$this->forward('index');
}

public function removeCustomKeyAction()
public function removeCustomKeyAction(): void
{
$this->apiKeyCache->remove(Package::API_KEY_CACHE_ID);
$this->forward('index');
Expand Down
164 changes: 164 additions & 0 deletions Classes/Domain/CollectionComparison/Comparator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

declare(strict_types=1);

namespace Sitegeist\LostInTranslation\Domain\CollectionComparison;

use Neos\ContentRepository\Domain\Service\Context;
use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Neos\Domain\Service\ContentContextFactory;

class Comparator
{
/**
* @var ContentContextFactory
* @Flow\Inject
*/
protected $contextFactory;

public function compareCollectionNode(NodeInterface $currentNode, NodeInterface $referenceNode): Result
{
if ($currentNode->getNodeType()->isOfType('Neos.Neos:ContentCollection') === false) {
throw new \InvalidArgumentException($currentNode->getNodeType()->getName() . " is not of type Neos.Neos:ContentCollection");
}

$result = Result::createEmpty();

// ensure deleted but not yet published nodes are found aswell so we will not try to translate those
$currentContextProperties = $currentNode->getContext()->getProperties();
$currentContextProperties['removedContentShown'] = true;
$currentContextIncludingRemovedItems = $this->contextFactory->create($currentContextProperties);

$result = $this->traverseContentCollectionForAlteredNodes(
$currentNode,
$referenceNode,
$currentContextIncludingRemovedItems,
$result
);

return $result;
}

public function compareDocumentNode(NodeInterface $currentNode, NodeInterface $referenceNode): Result
{
if ($currentNode->getNodeType()->isOfType('Neos.Neos:Document') === false) {
throw new \InvalidArgumentException($currentNode->getNodeType()->getName() . " is not of type Neos.Neos:Document");
}

$result = Result::createEmpty();

// ensure deleted but not yet published nodes are found as well, so we will not try to translate those
$currentContextProperties = $currentNode->getContext()->getProperties();
$currentContextProperties['removedContentShown'] = true;
$currentContextIncludingRemovedItems = $this->contextFactory->create($currentContextProperties);

$referenceContentContext = $referenceNode->getContext();

if ($referenceNode->getNodeData()->getLastModificationDateTime() > $currentNode->getNodeData()->getLastModificationDateTime()) {
$result = $result->withOutdatedNodes(new OutdatedNodeReference(
$currentNode,
$referenceNode
));
}

foreach ($currentNode->getChildNodes('Neos.Neos:ContentCollection') as $currentCollectionChild) {
if ($currentCollectionChild->isAutoCreated() === false) {
// skip nodes that are not autocreated
continue;
}
$referenceCollectionChild = $referenceContentContext->getNodeByIdentifier($currentCollectionChild->getIdentifier());
if ($referenceCollectionChild) {
$result = $this->traverseContentCollectionForAlteredNodes(
$currentCollectionChild,
$referenceCollectionChild,
$currentContextIncludingRemovedItems,
$result
);
}
}

return $result;
}

private function traverseContentCollectionForAlteredNodes(
NodeInterface $currentNode,
NodeInterface $referenceNode,
Context $currentContextIncludingRemovedItems,
Result $result,
): Result {

$missing = [];
$outdated = [];

$reduceToArrayWithIdentifier = function (array $carry, NodeInterface $item) {
$carry[$item->getIdentifier()] = $item;
return $carry;
};

$currentNodeInContextShowingRemovedItems = $currentContextIncludingRemovedItems->getNodeByIdentifier($currentNode->getIdentifier());
if (is_null($currentNodeInContextShowingRemovedItems)) {
return $result;
}

/**
* @var array<string,NodeInterface> $currentCollectionChildren
*/
$currentCollectionChildren = array_reduce($currentNodeInContextShowingRemovedItems->getChildNodes(), $reduceToArrayWithIdentifier, []);

/**
* @var array<string,NodeInterface> $referenceCollectionChildren
*/
$referenceCollectionChildren = array_reduce($referenceNode->getChildNodes(), $reduceToArrayWithIdentifier, []);
$referenceCollectionChildrenIdentifiers = array_keys($referenceCollectionChildren);

foreach ($referenceCollectionChildren as $identifier => $referenceCollectionChild) {
if (!array_key_exists($identifier, $currentCollectionChildren)) {
$position = array_search($identifier, $referenceCollectionChildrenIdentifiers);
$previousIdentifier = ($position !== false && array_key_exists($position - 1, $referenceCollectionChildrenIdentifiers)) ? $referenceCollectionChildrenIdentifiers[$position - 1] : null;
$nextIdentifier = ($position !== false && array_key_exists($position + 1, $referenceCollectionChildrenIdentifiers)) ? $referenceCollectionChildrenIdentifiers[$position + 1] : null;

$missing[] = new MissingNodeReference(
$referenceCollectionChild,
$currentNode,
$previousIdentifier,
$nextIdentifier
);
}
}

foreach ($currentCollectionChildren as $identifier => $currentCollectionCollectionChild) {
if (
array_key_exists($identifier, $referenceCollectionChildren)
&& $referenceCollectionChildren[$identifier]->getNodeData()->getLastModificationDateTime() > $currentCollectionCollectionChild->getNodeData()->getLastModificationDateTime()
) {
$outdated[] = new OutdatedNodeReference(
$currentCollectionCollectionChild,
$referenceCollectionChildren[$identifier]
);
}

if (
($currentCollectionCollectionChild->hasChildNodes() || $currentCollectionCollectionChild->getNodeType()->isOfType('Neos.Neos:ContentCollection'))
&& array_key_exists($identifier, $referenceCollectionChildren)
) {
$result = $this->traverseContentCollectionForAlteredNodes(
$currentCollectionCollectionChild,
$referenceCollectionChildren[$identifier],
$currentContextIncludingRemovedItems,
$result
);
}
}

if ($missing) {
$result = $result->withMissingNodes(...$missing);
}

if ($outdated) {
$result = $result->withOutdatedNodes(...$outdated);
}

return $result;
}
}
49 changes: 49 additions & 0 deletions Classes/Domain/CollectionComparison/MissingNodeReference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Sitegeist\LostInTranslation\Domain\CollectionComparison;

use Neos\ContentRepository\Domain\Model\Node;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Flow\Annotations as Flow;

/**
* @Flow\Proxy(false)
*/
final class MissingNodeReference
{
protected NodeInterface $node;

protected NodeInterface $referenceNode;
protected ?string $previousIdentifier;
protected ?string $nextIdentifier;

public function __construct(NodeInterface $node, NodeInterface $referenceNode, ?string $previousIdentifier, ?string $nextIdentifier)
{
$this->node = $node;
$this->referenceNode = $referenceNode;
$this->previousIdentifier = $previousIdentifier;
$this->nextIdentifier = $nextIdentifier;
}

public function getNode(): NodeInterface
{
return $this->node;
}

public function getReferenceNode(): NodeInterface
{
return $this->referenceNode;
}

public function getPreviousIdentifier(): ?string
{
return $this->previousIdentifier;
}

public function getNextIdentifier(): ?string
{
return $this->nextIdentifier;
}
}
34 changes: 34 additions & 0 deletions Classes/Domain/CollectionComparison/OutdatedNodeReference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Sitegeist\LostInTranslation\Domain\CollectionComparison;

use Neos\ContentRepository\Domain\Model\Node;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Flow\Annotations as Flow;

/**
* @Flow\Proxy(false)
*/
final class OutdatedNodeReference
{
protected NodeInterface $node;
protected NodeInterface $referenceNode;

public function __construct(NodeInterface $node, NodeInterface $referenceNode)
{
$this->node = $node;
$this->referenceNode = $referenceNode;
}

public function getNode(): NodeInterface
{
return $this->node;
}

public function getReferenceNode(): NodeInterface
{
return $this->referenceNode;
}
}
Loading

0 comments on commit 6a0d0f2

Please sign in to comment.