Skip to content
Merged
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
4 changes: 2 additions & 2 deletions Classes/Command/LostInTranslationCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

namespace Sitegeist\LostInTranslation\Command;

use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentSubgraph;
use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint;
use Neos\ContentRepository\Core\Feature\NodeVariation\Command\CreateNodeVariant;
use Neos\ContentRepository\Core\Feature\Security\Exception\AccessDenied;
use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
Expand Down Expand Up @@ -69,7 +69,7 @@ public function translateCommand(string $source, string $target, string $content
$this->translateNodeRecursive($cr, $start, $originSubgraph, $targetSubgraph);
}

public function translateNodeRecursive(ContentRepository $cr, Node $originNode, ContentSubgraph $originSubgraph, ContentSubgraph $targetSubgraph): void
public function translateNodeRecursive(ContentRepository $cr, Node $originNode, ContentSubgraphInterface $originSubgraph, ContentSubgraphInterface $targetSubgraph): void
{
$targetNode = $targetSubgraph->findNodeById($originNode->aggregateId);
if ($targetNode === null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Sitegeist\LostInTranslation\ContentRepository\AuthProvider;

use Neos\ContentRepository\Core\CommandHandler\CommandInterface;
use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface;
use Neos\ContentRepository\Core\Feature\Security\Dto\Privilege;
use Neos\ContentRepository\Core\Feature\Security\Dto\UserId;
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use Neos\Flow\Annotations as Flow;

#[Flow\Proxy(false)]
final class AIAwareContentRepositoryAuthProvider implements AuthProviderInterface
{
public function __construct(
private readonly AuthProviderInterface $baseAuthProvider,
private readonly AISystemTranslationRuntimeState $aiSystemTranslationRuntimeState,
) {
}

public function getAuthenticatedUserId(): ?UserId
{
return $this->aiSystemTranslationRuntimeState->getActiveAIServiceId()
?: $this->baseAuthProvider->getAuthenticatedUserId();
}

public function canReadNodesFromWorkspace(WorkspaceName $workspaceName): Privilege
{
return $this->baseAuthProvider->canReadNodesFromWorkspace($workspaceName);
}

public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints
{
return $this->baseAuthProvider->getVisibilityConstraints($workspaceName);
}

public function canExecuteCommand(CommandInterface $command): Privilege
{
return $this->baseAuthProvider->canExecuteCommand($command);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Sitegeist\LostInTranslation\ContentRepository\AuthProvider;

use Neos\ContentRepository\Core\Factory\AuthProviderFactoryInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface;
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Security\Context as SecurityContext;
use Neos\Neos\Domain\Service\UserService;
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService;
use Neos\Neos\Security\ContentRepositoryAuthProvider\ContentRepositoryAuthProvider;

/**
* Implementation of the {@see AuthProviderFactoryInterface} in order to provide authentication and authorization for Content Repositories
* and distinguish between human and AI editors
*
* @api
*/
#[Flow\Scope('singleton')]
final readonly class AIAwareContentRepositoryAuthProviderFactory implements AuthProviderFactoryInterface
{
public function __construct(
private UserService $userService,
private ContentRepositoryAuthorizationService $contentRepositoryAuthorizationService,
private SecurityContext $securityContext,
private AISystemTranslationRuntimeState $aiSystemTranslationRuntimeState,
) {
}

public function build(
ContentRepositoryId $contentRepositoryId,
ContentGraphReadModelInterface $contentGraphReadModel
): AIAwareContentRepositoryAuthProvider {
return new AIAwareContentRepositoryAuthProvider(
baseAuthProvider: new ContentRepositoryAuthProvider(
$contentRepositoryId,
$this->userService,
$contentGraphReadModel,
$this->contentRepositoryAuthorizationService,
$this->securityContext
),
aiSystemTranslationRuntimeState: $this->aiSystemTranslationRuntimeState,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Sitegeist\LostInTranslation\ContentRepository\AuthProvider;

use Neos\ContentRepository\Core\Factory\AuthProviderFactoryInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface;
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
use Neos\ContentRepository\TestSuite\Fakes\FakeAuthProvider;
use Neos\Flow\Annotations as Flow;

/**
* Implementation of the {@see AuthProviderFactoryInterface} in order to provide authentication and authorization for Content Repositories
* and distinguish between human and AI editors
*
* @api
*/
#[Flow\Scope('singleton')]
final readonly class AIAwareFakeAuthProviderFactory implements AuthProviderFactoryInterface
{
public function __construct(
private AISystemTranslationRuntimeState $aiSystemTranslationRuntimeState,
) {
}

public function build(
ContentRepositoryId $contentRepositoryId,
ContentGraphReadModelInterface $contentGraphReadModel
): AIAwareContentRepositoryAuthProvider {
return new AIAwareContentRepositoryAuthProvider(
/** @phpstan-ignore class.notFound (requires dev dependencies) */
baseAuthProvider: new FakeAuthProvider(),
aiSystemTranslationRuntimeState: $this->aiSystemTranslationRuntimeState,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Sitegeist\LostInTranslation\ContentRepository\AuthProvider;

use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Core\Feature\Security\Dto\UserId;

/**
* The state tracking if an AI system - and which one - is currently running a translation
*/
#[Flow\Scope('singleton')]
final class AISystemTranslationRuntimeState
{
public function __construct(
private ?UserId $activeAIServiceId = null
) {
}

public function setActiveAIServiceId(UserId $aiServiceId): void
{
$this->activeAIServiceId = $aiServiceId;
}

public function getActiveAIServiceId(): ?UserId
{
return $this->activeAIServiceId;
}

public function resetActiveAIServiceId(): void
{
$this->activeAIServiceId = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
use Sitegeist\LostInTranslation\ContentRepository\AuthProvider\AISystemTranslationRuntimeState;
use Sitegeist\LostInTranslation\Domain\Directive\DimensionValueDirectiveFactory;
use Sitegeist\LostInTranslation\Domain\Directive\NodeTypeTranslationDirectiveFactory;
use Sitegeist\LostInTranslation\Domain\TranslationServiceInterface;
Expand All @@ -29,6 +30,7 @@ public function __construct(
private readonly DimensionValueDirectiveFactory $dimensionValueDirectiveFactory,
private readonly TranslationServiceInterface $translationService,
private readonly ContentDimension $languageDimension,
private readonly AISystemTranslationRuntimeState $aiSystemTranslationRuntimeState,
) {
}

Expand All @@ -43,11 +45,13 @@ public function onBeforeHandle(CommandInterface $command): CommandInterface

public function onAfterHandle(CommandInterface $command, PublishedEvents $events): Commands
{
$this->aiSystemTranslationRuntimeState->resetActiveAIServiceId();
if ($this->enabled === false) {
return Commands::createEmpty();
}

if ($command instanceof CreateNodeVariant) {
$this->aiSystemTranslationRuntimeState->setActiveAIServiceId($this->translationService->getAIServiceId());
return $this->createNodeVariantCommandWasHandled($command);
} else {
return Commands::createEmpty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Neos\ContentRepository\Core\Factory\CommandHooksFactoryDependencies;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\Flow\Annotations as Flow;
use Sitegeist\LostInTranslation\ContentRepository\AuthProvider\AISystemTranslationRuntimeState;
use Sitegeist\LostInTranslation\Domain\Directive\DimensionValueDirectiveFactory;
use Sitegeist\LostInTranslation\Domain\Directive\NodeTypeTranslationDirectiveFactory;
use Sitegeist\LostInTranslation\Domain\TranslationServiceInterface;
Expand All @@ -27,6 +28,7 @@ public function __construct(
protected readonly ContentRepositoryRegistry $contentRepositoryRegistry,
protected readonly NodeTypeTranslationDirectiveFactory $translatablePropertyNamesFactory,
protected readonly TranslationServiceInterface $translationService,
protected readonly AISystemTranslationRuntimeState $aiSystemTranslationRuntimeState,
) {
}

Expand All @@ -44,7 +46,8 @@ public function build(CommandHooksFactoryDependencies $commandHooksFactoryDepend
$this->translatablePropertyNamesFactory,
new DimensionValueDirectiveFactory(),
$this->translationService,
$languageDimension
$languageDimension,
$this->aiSystemTranslationRuntimeState,
);
} else {
throw new \Exception(sprintf('Lamguage dimension %s was nou found in content repository %s', $this->languageDimensionName, $commandHooksFactoryDependencies->contentRepositoryId->value));
Expand Down
4 changes: 4 additions & 0 deletions Classes/Domain/TranslationServiceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Sitegeist\LostInTranslation\Domain;

use Neos\ContentRepository\Core\Feature\Security\Dto\UserId;

interface TranslationServiceInterface
{
/**
Expand All @@ -15,4 +17,6 @@ interface TranslationServiceInterface
public function translate(array $texts, string $targetLanguage, ?string $sourceLanguage = null): array;

public function getStatus(): ApiStatus;

public function getAIServiceId(): UserId;
}
6 changes: 6 additions & 0 deletions Classes/Infrastructure/DeepL/DeepLTranslationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use DeepL\TranslatorOptions;
use DeepL\Usage;
use Neos\Cache\Frontend\StringFrontend;
use Neos\ContentRepository\Core\Feature\Security\Dto\UserId;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Http\Client\Browser;
use Neos\Flow\Http\Client\CurlEngine;
Expand Down Expand Up @@ -209,6 +210,11 @@ public function getStatus(): ApiStatus
}
}

public function getAIServiceId(): UserId
{
return UserId::fromString('AI:DeepL:DeepL');
}

protected function getDeeplAuthenticationKey(): DeepLAuthenticationKey
{
return $this->authenticationKeyFactory->create();
Expand Down
32 changes: 32 additions & 0 deletions Classes/Infrastructure/Dummy/DummyTranslationService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Sitegeist\LostInTranslation\Infrastructure\Dummy;

use Neos\ContentRepository\Core\Feature\Security\Dto\UserId;
use Neos\Flow\Annotations as Flow;
use Sitegeist\LostInTranslation\Domain\ApiStatus;
use Sitegeist\LostInTranslation\Domain\TranslationServiceInterface;

#[Flow\Scope('singleton')]
class DummyTranslationService implements TranslationServiceInterface
{
public function translate(array $texts, string $targetLanguage, ?string $sourceLanguage = null): array
{
return array_map(
fn (string $text): string => $text . ' translated',
$texts,
);
}

public function getStatus(): ApiStatus
{
return new ApiStatus(true);
}

public function getAIServiceId(): UserId
{
return UserId::fromString('AI:dummy:my-dummy');
}
}
3 changes: 3 additions & 0 deletions Configuration/Objects.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
Sitegeist\LostInTranslation\Domain\TranslationServiceInterface:
className: 'Sitegeist\LostInTranslation\Infrastructure\DeepL\DeepLTranslationService'

Sitegeist\LostInTranslation\Infrastructure\DeepL\DeepLTranslationService:
properties:
translationCache:
Expand Down
7 changes: 3 additions & 4 deletions Configuration/Settings.Neos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ Neos:
ContentRepositoryRegistry:
presets:
'default':
# contentGraphProjection:
# catchUpHooks:
# 'Sitegeist.LostInTranslation:TranslationCatchupHook':
# factoryObjectName: Sitegeist\LostInTranslation\ContentRepository\CatchUpHook\TranslationCatchupHookFactory
commandHooks:
'Sitegeist.LostInTranslation:TranslationCommandHook':
factoryObjectName: Sitegeist\LostInTranslation\ContentRepository\CommandHook\TranslationCommandHookFactory
# Activate for content governance mode:
#authProvider:
# factoryObjectName: Sitegeist\LostInTranslation\ContentRepository\AuthProvider\AIAwareContentRepositoryAuthProviderFactory
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Neos:
ContentRepositoryRegistry:
presets:
default:
authProvider:
factoryObjectName: Sitegeist\LostInTranslation\ContentRepository\AuthProvider\AIAwareFakeAuthProviderFactory
clock:
factoryObjectName: 'Neos\ContentRepository\TestSuite\Fakes\FakeClockFactory'
nodeTypeManager:
factoryObjectName: 'Neos\ContentRepository\TestSuite\Fakes\FakeNodeTypeManagerFactory'
contentDimensionSource:
factoryObjectName: 'Neos\ContentRepository\TestSuite\Fakes\FakeContentDimensionSourceFactory'
2 changes: 2 additions & 0 deletions Configuration/Testing/Objects.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Sitegeist\LostInTranslation\Domain\TranslationServiceInterface:
className: 'Sitegeist\LostInTranslation\Infrastructure\Dummy\DummyTranslationService'
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,22 @@ Sitegeist:
enableCache: false
```

### Content Governance Mode

To exactly track what write operations have been performed by human editors or their translation assistant,
you can enable content governance mode by enabling the respective AuthProvider:


```yaml
Neos:
ContentRepositoryRegistry:
presets:
# or whatever preset you use
default:
authProvider:
factoryObjectName: Sitegeist\LostInTranslation\ContentRepository\AuthProvider\AIAwareContentRepositoryAuthProviderFactory
```

## Performance

For every translated node, a single request is made to the DeepL API.
Expand Down
Loading