Skip to content

Commit f6e2268

Browse files
authored
Declare automated translations as AI generated
2 parents 2b1c39f + 6fd0e8c commit f6e2268

19 files changed

+426
-10
lines changed

Classes/Command/LostInTranslationCommandController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
namespace Sitegeist\LostInTranslation\Command;
66

7-
use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentSubgraph;
87
use Neos\ContentRepository\Core\ContentRepository;
98
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
109
use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint;
1110
use Neos\ContentRepository\Core\Feature\NodeVariation\Command\CreateNodeVariant;
1211
use Neos\ContentRepository\Core\Feature\Security\Exception\AccessDenied;
1312
use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath;
13+
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface;
1414
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter;
1515
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
1616
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
@@ -69,7 +69,7 @@ public function translateCommand(string $source, string $target, string $content
6969
$this->translateNodeRecursive($cr, $start, $originSubgraph, $targetSubgraph);
7070
}
7171

72-
public function translateNodeRecursive(ContentRepository $cr, Node $originNode, ContentSubgraph $originSubgraph, ContentSubgraph $targetSubgraph): void
72+
public function translateNodeRecursive(ContentRepository $cr, Node $originNode, ContentSubgraphInterface $originSubgraph, ContentSubgraphInterface $targetSubgraph): void
7373
{
7474
$targetNode = $targetSubgraph->findNodeById($originNode->aggregateId);
7575
if ($targetNode === null) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sitegeist\LostInTranslation\ContentRepository\AuthProvider;
6+
7+
use Neos\ContentRepository\Core\CommandHandler\CommandInterface;
8+
use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface;
9+
use Neos\ContentRepository\Core\Feature\Security\Dto\Privilege;
10+
use Neos\ContentRepository\Core\Feature\Security\Dto\UserId;
11+
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
12+
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
13+
use Neos\Flow\Annotations as Flow;
14+
15+
#[Flow\Proxy(false)]
16+
final class AIAwareContentRepositoryAuthProvider implements AuthProviderInterface
17+
{
18+
public function __construct(
19+
private readonly AuthProviderInterface $baseAuthProvider,
20+
private readonly AISystemTranslationRuntimeState $aiSystemTranslationRuntimeState,
21+
) {
22+
}
23+
24+
public function getAuthenticatedUserId(): ?UserId
25+
{
26+
return $this->aiSystemTranslationRuntimeState->getActiveAIServiceId()
27+
?: $this->baseAuthProvider->getAuthenticatedUserId();
28+
}
29+
30+
public function canReadNodesFromWorkspace(WorkspaceName $workspaceName): Privilege
31+
{
32+
return $this->baseAuthProvider->canReadNodesFromWorkspace($workspaceName);
33+
}
34+
35+
public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints
36+
{
37+
return $this->baseAuthProvider->getVisibilityConstraints($workspaceName);
38+
}
39+
40+
public function canExecuteCommand(CommandInterface $command): Privilege
41+
{
42+
return $this->baseAuthProvider->canExecuteCommand($command);
43+
}
44+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sitegeist\LostInTranslation\ContentRepository\AuthProvider;
6+
7+
use Neos\ContentRepository\Core\Factory\AuthProviderFactoryInterface;
8+
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface;
9+
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
10+
use Neos\Flow\Annotations as Flow;
11+
use Neos\Flow\Security\Context as SecurityContext;
12+
use Neos\Neos\Domain\Service\UserService;
13+
use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService;
14+
use Neos\Neos\Security\ContentRepositoryAuthProvider\ContentRepositoryAuthProvider;
15+
16+
/**
17+
* Implementation of the {@see AuthProviderFactoryInterface} in order to provide authentication and authorization for Content Repositories
18+
* and distinguish between human and AI editors
19+
*
20+
* @api
21+
*/
22+
#[Flow\Scope('singleton')]
23+
final readonly class AIAwareContentRepositoryAuthProviderFactory implements AuthProviderFactoryInterface
24+
{
25+
public function __construct(
26+
private UserService $userService,
27+
private ContentRepositoryAuthorizationService $contentRepositoryAuthorizationService,
28+
private SecurityContext $securityContext,
29+
private AISystemTranslationRuntimeState $aiSystemTranslationRuntimeState,
30+
) {
31+
}
32+
33+
public function build(
34+
ContentRepositoryId $contentRepositoryId,
35+
ContentGraphReadModelInterface $contentGraphReadModel
36+
): AIAwareContentRepositoryAuthProvider {
37+
return new AIAwareContentRepositoryAuthProvider(
38+
baseAuthProvider: new ContentRepositoryAuthProvider(
39+
$contentRepositoryId,
40+
$this->userService,
41+
$contentGraphReadModel,
42+
$this->contentRepositoryAuthorizationService,
43+
$this->securityContext
44+
),
45+
aiSystemTranslationRuntimeState: $this->aiSystemTranslationRuntimeState,
46+
);
47+
}
48+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sitegeist\LostInTranslation\ContentRepository\AuthProvider;
6+
7+
use Neos\ContentRepository\Core\Factory\AuthProviderFactoryInterface;
8+
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface;
9+
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
10+
use Neos\ContentRepository\TestSuite\Fakes\FakeAuthProvider;
11+
use Neos\Flow\Annotations as Flow;
12+
13+
/**
14+
* Implementation of the {@see AuthProviderFactoryInterface} in order to provide authentication and authorization for Content Repositories
15+
* and distinguish between human and AI editors
16+
*
17+
* @api
18+
*/
19+
#[Flow\Scope('singleton')]
20+
final readonly class AIAwareFakeAuthProviderFactory implements AuthProviderFactoryInterface
21+
{
22+
public function __construct(
23+
private AISystemTranslationRuntimeState $aiSystemTranslationRuntimeState,
24+
) {
25+
}
26+
27+
public function build(
28+
ContentRepositoryId $contentRepositoryId,
29+
ContentGraphReadModelInterface $contentGraphReadModel
30+
): AIAwareContentRepositoryAuthProvider {
31+
return new AIAwareContentRepositoryAuthProvider(
32+
/** @phpstan-ignore class.notFound (requires dev dependencies) */
33+
baseAuthProvider: new FakeAuthProvider(),
34+
aiSystemTranslationRuntimeState: $this->aiSystemTranslationRuntimeState,
35+
);
36+
}
37+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sitegeist\LostInTranslation\ContentRepository\AuthProvider;
6+
7+
use Neos\Flow\Annotations as Flow;
8+
use Neos\ContentRepository\Core\Feature\Security\Dto\UserId;
9+
10+
/**
11+
* The state tracking if an AI system - and which one - is currently running a translation
12+
*/
13+
#[Flow\Scope('singleton')]
14+
final class AISystemTranslationRuntimeState
15+
{
16+
public function __construct(
17+
private ?UserId $activeAIServiceId = null
18+
) {
19+
}
20+
21+
public function setActiveAIServiceId(UserId $aiServiceId): void
22+
{
23+
$this->activeAIServiceId = $aiServiceId;
24+
}
25+
26+
public function getActiveAIServiceId(): ?UserId
27+
{
28+
return $this->activeAIServiceId;
29+
}
30+
31+
public function resetActiveAIServiceId(): void
32+
{
33+
$this->activeAIServiceId = null;
34+
}
35+
}

Classes/ContentRepository/CommandHook/TranslationCommandHook.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
1616
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface;
1717
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
18+
use Sitegeist\LostInTranslation\ContentRepository\AuthProvider\AISystemTranslationRuntimeState;
1819
use Sitegeist\LostInTranslation\Domain\Directive\DimensionValueDirectiveFactory;
1920
use Sitegeist\LostInTranslation\Domain\Directive\NodeTypeTranslationDirectiveFactory;
2021
use Sitegeist\LostInTranslation\Domain\TranslationServiceInterface;
@@ -29,6 +30,7 @@ public function __construct(
2930
private readonly DimensionValueDirectiveFactory $dimensionValueDirectiveFactory,
3031
private readonly TranslationServiceInterface $translationService,
3132
private readonly ContentDimension $languageDimension,
33+
private readonly AISystemTranslationRuntimeState $aiSystemTranslationRuntimeState,
3234
) {
3335
}
3436

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

4446
public function onAfterHandle(CommandInterface $command, PublishedEvents $events): Commands
4547
{
48+
$this->aiSystemTranslationRuntimeState->resetActiveAIServiceId();
4649
if ($this->enabled === false) {
4750
return Commands::createEmpty();
4851
}
4952

5053
if ($command instanceof CreateNodeVariant) {
54+
$this->aiSystemTranslationRuntimeState->setActiveAIServiceId($this->translationService->getAIServiceId());
5155
return $this->createNodeVariantCommandWasHandled($command);
5256
} else {
5357
return Commands::createEmpty();

Classes/ContentRepository/CommandHook/TranslationCommandHookFactory.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Neos\ContentRepository\Core\Factory\CommandHooksFactoryDependencies;
1212
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
1313
use Neos\Flow\Annotations as Flow;
14+
use Sitegeist\LostInTranslation\ContentRepository\AuthProvider\AISystemTranslationRuntimeState;
1415
use Sitegeist\LostInTranslation\Domain\Directive\DimensionValueDirectiveFactory;
1516
use Sitegeist\LostInTranslation\Domain\Directive\NodeTypeTranslationDirectiveFactory;
1617
use Sitegeist\LostInTranslation\Domain\TranslationServiceInterface;
@@ -27,6 +28,7 @@ public function __construct(
2728
protected readonly ContentRepositoryRegistry $contentRepositoryRegistry,
2829
protected readonly NodeTypeTranslationDirectiveFactory $translatablePropertyNamesFactory,
2930
protected readonly TranslationServiceInterface $translationService,
31+
protected readonly AISystemTranslationRuntimeState $aiSystemTranslationRuntimeState,
3032
) {
3133
}
3234

@@ -44,7 +46,8 @@ public function build(CommandHooksFactoryDependencies $commandHooksFactoryDepend
4446
$this->translatablePropertyNamesFactory,
4547
new DimensionValueDirectiveFactory(),
4648
$this->translationService,
47-
$languageDimension
49+
$languageDimension,
50+
$this->aiSystemTranslationRuntimeState,
4851
);
4952
} else {
5053
throw new \Exception(sprintf('Lamguage dimension %s was nou found in content repository %s', $this->languageDimensionName, $commandHooksFactoryDependencies->contentRepositoryId->value));

Classes/Domain/TranslationServiceInterface.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Sitegeist\LostInTranslation\Domain;
66

7+
use Neos\ContentRepository\Core\Feature\Security\Dto\UserId;
8+
79
interface TranslationServiceInterface
810
{
911
/**
@@ -15,4 +17,6 @@ interface TranslationServiceInterface
1517
public function translate(array $texts, string $targetLanguage, ?string $sourceLanguage = null): array;
1618

1719
public function getStatus(): ApiStatus;
20+
21+
public function getAIServiceId(): UserId;
1822
}

Classes/Infrastructure/DeepL/DeepLTranslationService.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use DeepL\TranslatorOptions;
1111
use DeepL\Usage;
1212
use Neos\Cache\Frontend\StringFrontend;
13+
use Neos\ContentRepository\Core\Feature\Security\Dto\UserId;
1314
use Neos\Flow\Annotations as Flow;
1415
use Neos\Flow\Http\Client\Browser;
1516
use Neos\Flow\Http\Client\CurlEngine;
@@ -209,6 +210,11 @@ public function getStatus(): ApiStatus
209210
}
210211
}
211212

213+
public function getAIServiceId(): UserId
214+
{
215+
return UserId::fromString('AI:DeepL:DeepL');
216+
}
217+
212218
protected function getDeeplAuthenticationKey(): DeepLAuthenticationKey
213219
{
214220
return $this->authenticationKeyFactory->create();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sitegeist\LostInTranslation\Infrastructure\Dummy;
6+
7+
use Neos\ContentRepository\Core\Feature\Security\Dto\UserId;
8+
use Neos\Flow\Annotations as Flow;
9+
use Sitegeist\LostInTranslation\Domain\ApiStatus;
10+
use Sitegeist\LostInTranslation\Domain\TranslationServiceInterface;
11+
12+
#[Flow\Scope('singleton')]
13+
class DummyTranslationService implements TranslationServiceInterface
14+
{
15+
public function translate(array $texts, string $targetLanguage, ?string $sourceLanguage = null): array
16+
{
17+
return array_map(
18+
fn (string $text): string => $text . ' translated',
19+
$texts,
20+
);
21+
}
22+
23+
public function getStatus(): ApiStatus
24+
{
25+
return new ApiStatus(true);
26+
}
27+
28+
public function getAIServiceId(): UserId
29+
{
30+
return UserId::fromString('AI:dummy:my-dummy');
31+
}
32+
}

0 commit comments

Comments
 (0)