Skip to content

Commit

Permalink
Merge pull request #39 from sitegeist/documentRecursiveComparison
Browse files Browse the repository at this point in the history
Document recursive comparison
  • Loading branch information
mficzel authored Jan 15, 2024
2 parents af7124d + 3b5e8d4 commit 699ea7d
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 39 deletions.
106 changes: 90 additions & 16 deletions Classes/Domain/CollectionComparison/Comparator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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;
Expand All @@ -18,17 +19,83 @@ class Comparator

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;
};

// 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);
$currentNodeInContextShowingRemovedItems = $currentContextIncludingRemovedItems->getNodeByIdentifier($currentNode->getIdentifier());
if (is_null($currentNodeInContextShowingRemovedItems)) {
return $result;
Expand All @@ -45,10 +112,6 @@ public function compareCollectionNode(NodeInterface $currentNode, NodeInterface
$referenceCollectionChildren = array_reduce($referenceNode->getChildNodes(), $reduceToArrayWithIdentifier, []);
$referenceCollectionChildrenIdentifiers = array_keys($referenceCollectionChildren);

/**
* @var MissingNodeReference[] $missing
*/
$missing = [];
foreach ($referenceCollectionChildren as $identifier => $referenceCollectionChild) {
if (!array_key_exists($identifier, $currentCollectionChildren)) {
$position = array_search($identifier, $referenceCollectionChildrenIdentifiers);
Expand All @@ -57,19 +120,13 @@ public function compareCollectionNode(NodeInterface $currentNode, NodeInterface

$missing[] = new MissingNodeReference(
$referenceCollectionChild,
$currentNode,
$previousIdentifier,
$nextIdentifier
);
}
}
if (count($missing) > 0) {
$result = $result->withMissingNodes(...$missing);
}

/**
* @var OutdatedNodeReference[] $outdated
*/
$outdated = [];
foreach ($currentCollectionChildren as $identifier => $currentCollectionCollectionChild) {
if (
array_key_exists($identifier, $referenceCollectionChildren)
Expand All @@ -80,8 +137,25 @@ public function compareCollectionNode(NodeInterface $currentNode, NodeInterface
$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 (count($outdated) > 0) {

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

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

Expand Down
10 changes: 9 additions & 1 deletion Classes/Domain/CollectionComparison/MissingNodeReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@
final class MissingNodeReference
{
protected NodeInterface $node;

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

public function __construct(NodeInterface $node, ?string $previousIdentifier, ?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;
}
Expand All @@ -29,6 +32,11 @@ public function getNode(): NodeInterface
return $this->node;
}

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

public function getPreviousIdentifier(): ?string
{
return $this->previousIdentifier;
Expand Down
8 changes: 4 additions & 4 deletions Classes/Domain/CollectionComparison/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ final class Result
public array $outdated;

/**
* @param array<int|string,MissingNodeReference> $missing
* @param array<int|string,OutdatedNodeReference> $outdated
* @param array<int,MissingNodeReference> $missing
* @param array<int,OutdatedNodeReference> $outdated
*/
private function __construct(array $missing, array $outdated)
{
Expand All @@ -38,12 +38,12 @@ public static function createEmpty(): static

public function withMissingNodes(MissingNodeReference ...$missingNodes): static
{
return new static($missingNodes, $this->outdated);
return new static([...$this->missing, ...$missingNodes], $this->outdated);
}

public function withOutdatedNodes(OutdatedNodeReference ...$outdatedNodes): static
{
return new static($this->missing, $outdatedNodes);
return new static($this->missing, [...$this->outdated, ...$outdatedNodes]);
}

public function getHasDifferences(): bool
Expand Down
15 changes: 15 additions & 0 deletions Classes/Eel/TranslationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ public function compareCollectionWithDimension(NodeInterface $currentCollectionN
return $this->comparator->compareCollectionNode($currentCollectionNode, $referenceCollectionNode);
}

/**
* @param NodeInterface $currentDocumentNode
* @param string $referenceLanguage
* @return Result
*/
public function compareDocumentWithDimension(NodeInterface $currentDocumentNode, string $referenceLanguage): Result
{
$contentContext = $this->createContentContext($currentDocumentNode->getContext()->getWorkspaceName(), [$this->languageDimensionName => [$referenceLanguage]]);
$referenceDocumentNode = $contentContext->getNodeByIdentifier($currentDocumentNode->getIdentifier());
if ($referenceDocumentNode === null) {
return Result::createEmpty();
}
return $this->comparator->compareDocumentNode($currentDocumentNode, $referenceDocumentNode);
}

/**
* @inheritDoc
*/
Expand Down
19 changes: 12 additions & 7 deletions Classes/Ui/Changes/AbstractCollectionTranslationChange.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,24 @@ public function setReferenceLanguage(string $referenceLanguage): void

public function canApply()
{
return $this->subject->getNodeType()->isOfType('Neos.Neos:ContentCollection');
return $this->subject->getNodeType()->isOfType('Neos.Neos:ContentCollection') || $this->subject->getNodeType()->isOfType('Neos.Neos:Document') ;
}

protected function getComparisonResult(): ?Result
protected function getComparisonResult(NodeInterface $node): ?Result
{
$referenceDimensionValues = [$this->languageDimensionName => [$this->referenceLanguage]];
$currentCollection = $this->subject;
$referenceContentContext = $this->createContentContext($currentCollection->getContext()->getWorkspaceName(), $referenceDimensionValues);
$referenceCollectionNode = $referenceContentContext->getNodeByIdentifier($currentCollection->getIdentifier());

$referenceContentContext = $this->createContentContext($node->getContext()->getWorkspaceName(), $referenceDimensionValues);
$referenceCollectionNode = $referenceContentContext->getNodeByIdentifier($node->getIdentifier());
if ($referenceCollectionNode === null) {
return null;
}
$comparisonResult = $this->comparator->compareCollectionNode($currentCollection, $referenceCollectionNode);
return $comparisonResult;
if ($node->getNodeType()->isOfType('Neos.Neos:Document')) {
return $this->comparator->compareDocumentNode($node, $referenceCollectionNode);
} elseif ($node->getNodeType()->isOfType('Neos.Neos:ContentCollection')) {
return $this->comparator->compareCollectionNode($node, $referenceCollectionNode);
} else {
return null;
}
}
}
5 changes: 3 additions & 2 deletions Classes/Ui/Changes/AddMissingTranslations.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ class AddMissingTranslations extends AbstractCollectionTranslationChange
public function apply()
{
$collection = $this->subject;
$comparisonResult = $this->getComparisonResult();
$comparisonResult = $this->getComparisonResult($collection);

if (is_null($comparisonResult)) {
return;
}

$count = 0;
foreach ($comparisonResult->getMissing() as $missingNodeDifference) {
$adoptedNode = $collection->getContext()->adoptNode($missingNodeDifference->getNode(), true);
$adoptedNode = ($missingNodeDifference->getReferenceNode())->getContext()->adoptNode($missingNodeDifference->getNode(), true);
if ($missingNodeDifference->getPreviousIdentifier()) {
$previousNode = $collection->getContext()->getNodeByIdentifier($missingNodeDifference->getPreviousIdentifier());
if ($previousNode && $previousNode->getParent() === $adoptedNode->getParent()) {
Expand Down
5 changes: 4 additions & 1 deletion Classes/Ui/Changes/UpdateOutdatedTranslations.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ class UpdateOutdatedTranslations extends AbstractCollectionTranslationChange

public function apply()
{

$collection = $this->subject;
$comparisonResult = $this->getComparisonResult();

$comparisonResult = $this->getComparisonResult($collection);

if (is_null($comparisonResult)) {
return;
}
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,43 @@ ${Sitegeist.LostInTranslation.translate(['Hello world!', 'My name is...'], 'de',
# Output: ['Hallo Welt!', 'Mein Name ist...']
```

### Compare and update translations

The lost in translation package contains two prototypes that visualize differences between the current and the `default`
translation.

To show the information in the backend you can render the `Sitegeist.LostInTranslation:Collection.TranslationInformation` adjacent to a ContentCollection.

```
content = Neos.Fusion:Join {
info = Sitegeist.LostInTranslation:Collection.TranslationInformation {
nodePath = 'content'
}
content = Neos.Neos:ContentCollection {
nodePath = 'content'
}
}
```

![DDEV__WebPage_test](https://github.com/sitegeist/Sitegeist.LostInTranslation/assets/1309380/7d268e18-5a2a-4292-8844-4800020b0ddb)

### `Sitegeist.LostInTranslation:Document.TranslationInformation`

Show informations about missing and outdated translations on document level. Allows to "translate missing" and "update outdated" nodes.
The prototype is only showing in backend + edit mode.

- `node`: (Node, default `documentNode` from fusion context) The document node that shall be compared
- `referenceLanguage`: (string, default language preset) The preset used to compare against

### `Sitegeist.LostInTranslation:Collection.TranslationInformation`

Show informations about missing and outdated translations on content collection level. Allows to "translate missing" and "update outdated" nodes.
The prototype is only showing in backend + edit mode.

- `nodePath`: (string, default null)
- `node`: (Node, default `node` from fusion context)
- `referenceLanguage`: (string, default language preset) The preset used to compare against

### Translation Cache

The plugin includes a translation cache for the DeepL API that stores the individual text parts
Expand Down
10 changes: 5 additions & 5 deletions Resources/Private/Fusion/Frontend/Collection/InfoRenderer.fusion
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ prototype(Sitegeist.LostInTranslation:Collection.TranslationInformation) < proto
<Neos.Fusion:Fragment @if={props.status.missing || props.status.outdated}>
<script src={props.scriptHref} ></script>
<lost-in-translation-info
collectionNode={props.collectionNode.contextPath}
referenceLanguage={props.referenceLanguage}
showAddMissingButton={props.status.missing ? true : false}
showUpdateOutdatedButton={props.status.outdated ? true : false}
icon={props.icon}
node={props.collectionNode.contextPath}
referenceLanguage={props.referenceLanguage}
showAddMissingButton={props.status.missing ? true : false}
showUpdateOutdatedButton={props.status.outdated ? true : false}
icon={props.icon}
>
<Neos.Fusion:Fragment @path="attributes.text">
<p @if={props.status.missing}>
Expand Down
40 changes: 40 additions & 0 deletions Resources/Private/Fusion/Frontend/Document/InfoRenderer.fusion
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
prototype(Sitegeist.LostInTranslation:Document.TranslationInformation) < prototype(Neos.Fusion:Component) {
node = ${documentNode}
referenceLanguage = ${Configuration.setting('Neos.ContentRepository.contentDimensions.' + Configuration.setting('Sitegeist.LostInTranslation.nodeTranslation.languageDimensionName')+ '.defaultPreset')}

renderer = Neos.Fusion:Component {
@if.inBackend = ${node && node.context.inBackend && node.context.currentRenderingMode.edit}

status = ${Sitegeist.LostInTranslation.compareDocumentWithDimension(props.node, props.referenceLanguage)}
documentNode = ${props.node}
referenceLanguage = ${props.referenceLanguage}

scriptHref = Neos.Fusion:ResourceUri {
path = 'resource://Sitegeist.LostInTranslation/Public/Scripts/beinfo.js'
}

icon = ${File.readFile('resource://Sitegeist.LostInTranslation/Public/Icons/language.svg')}

renderer = afx`
<Neos.Fusion:Fragment @if={props.status.missing || props.status.outdated}>
<script src={props.scriptHref} ></script>
<lost-in-translation-info
node={props.documentNode.contextPath}
referenceLanguage={props.referenceLanguage}
showAddMissingButton={props.status.missing ? true : false}
showUpdateOutdatedButton={props.status.outdated ? true : false}
icon={props.icon}
>
<Neos.Fusion:Fragment @path="attributes.text">
<p @if={props.status.missing}>
Missing Contents: <Neos.Fusion:Loop items={props.status.missing} @glue=", ">{item.node.label}</Neos.Fusion:Loop>
</p>
<p @if={props.status.outdated}>
Outdated Contents: <Neos.Fusion:Loop items={props.status.outdated}>{item.node.label}</Neos.Fusion:Loop>
</p>
</Neos.Fusion:Fragment>
</lost-in-translation-info>
</Neos.Fusion:Fragment>
`
}
}
Loading

0 comments on commit 699ea7d

Please sign in to comment.