Skip to content

Commit 38c541e

Browse files
authored
Merge pull request #63 from sitegeist/feature/glossarySupport
FEATURE: Support for glossaries
2 parents d0f545c + 3a76337 commit 38c541e

23 files changed

+1454
-50
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace Sitegeist\LostInTranslation\Command;
4+
5+
use Neos\Flow\Annotations as Flow;
6+
use Neos\Flow\Cli\CommandController;
7+
use Sitegeist\LostInTranslation\Domain\Model\Glossary;
8+
use Sitegeist\LostInTranslation\Domain\Repository\GlossaryRepository;
9+
use Sitegeist\LostInTranslation\Infrastructure\DeepL\DeepLCacheService;
10+
use Sitegeist\LostInTranslation\Infrastructure\DeepL\DeepLGlossaryService;
11+
12+
class GlossaryCommandController extends CommandController
13+
{
14+
#[Flow\Inject]
15+
protected DeepLGlossaryService $deepLGlossaryService;
16+
17+
#[Flow\Inject]
18+
protected DeepLCacheService $deepLCacheService;
19+
20+
#[Flow\Inject]
21+
protected GlossaryRepository $glossaryRepository;
22+
23+
public function uploadAllCommand(): void
24+
{
25+
foreach ($this->glossaryRepository->findAll() as $glossary) {
26+
/**
27+
* @var Glossary $glossary
28+
*/
29+
$id = $this->deepLGlossaryService->uploadRemoteGlossary($glossary);
30+
if ($id) {
31+
$this->output->outputLine(sprintf('Glossary %s was uploaded with id %s', $glossary->getLabel(), $id));
32+
}
33+
}
34+
$this->deepLCacheService->flush();
35+
}
36+
37+
public function cleanupAllCommand(): void
38+
{
39+
$deleted = $this->deepLGlossaryService->cleanupRemoteGlossaries();
40+
$this->output->outputLine(sprintf('Removed %s outdated glossaries', count($deleted)));
41+
}
42+
}

Classes/Controller/LostInTranslationModuleController.php

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@
44

55
namespace Sitegeist\LostInTranslation\Controller;
66

7+
use DeepL\GlossaryInfo;
8+
use Neos\Error\Messages\Message;
79
use Neos\Fusion\View\FusionView;
810
use Neos\Neos\Controller\Module\AbstractModuleController;
911
use Neos\Flow\Annotations as Flow;
12+
use Sitegeist\LostInTranslation\Domain\Model\Glossary;
13+
use Sitegeist\LostInTranslation\Domain\Model\GlossaryEntry;
14+
use Sitegeist\LostInTranslation\Domain\Repository\GlossaryRepository;
15+
use Sitegeist\LostInTranslation\Infrastructure\DeepL\DeepLCacheService;
1016
use Sitegeist\LostInTranslation\Infrastructure\DeepL\DeepLCustomAuthenticationKeyService;
17+
use Sitegeist\LostInTranslation\Infrastructure\DeepL\DeepLGlossaryService;
1118
use Sitegeist\LostInTranslation\Infrastructure\DeepL\DeepLTranslationService;
1219

1320
class LostInTranslationModuleController extends AbstractModuleController
@@ -18,19 +25,56 @@ class LostInTranslationModuleController extends AbstractModuleController
1825
*/
1926
protected $translationService;
2027

28+
/**
29+
* @var DeepLCacheService
30+
* @Flow\Inject
31+
*/
32+
protected $cacheService;
33+
34+
/**
35+
* @var DeepLGlossaryService
36+
* @Flow\Inject
37+
*/
38+
protected $glossaryService;
39+
40+
/**
41+
* @var GlossaryRepository
42+
* @Flow\Inject
43+
*/
44+
protected $glossaryRepository;
45+
2146
/**
2247
* @Flow\Inject
2348
* @var DeepLCustomAuthenticationKeyService
2449
*/
2550
protected $customAuthenticationKeyService;
2651

52+
/**
53+
* @Flow\InjectConfiguration(path="nodeTranslation.languageDimensionName")
54+
* @var string
55+
*/
56+
protected $languageDimensionName;
57+
58+
/**
59+
* @Flow\InjectConfiguration(package="Neos.ContentRepository", path="contentDimensions")
60+
* @var array<string,array{'default': string, 'defaultPreset': string, 'presets': array<string,mixed> }>
61+
*/
62+
protected $contentDimensionConfiguration;
63+
2764
/**
2865
* @var FusionView
2966
*/
3067
protected $view;
3168

3269

3370
public function indexAction(): void
71+
{
72+
$status = $this->translationService->getStatus();
73+
$this->view->assign('status', $status);
74+
$this->view->assign('glossaries', $this->glossaryRepository->findAll()->toArray());
75+
}
76+
77+
public function showStatusAction(): void
3478
{
3579
$status = $this->translationService->getStatus();
3680
$this->view->assign('status', $status);
@@ -51,4 +95,134 @@ public function removeCustomKeyAction(): void
5195
$this->customAuthenticationKeyService->remove();
5296
$this->forward('index');
5397
}
98+
99+
/**
100+
* The list of supported language pairs is narrowed by only including exising language dimension
101+
* values and removing already existing glossaries. We only look into the first segments as this
102+
* deepl glossaries currently do not support country identifiers
103+
*/
104+
public function createGlossaryAction(): void
105+
{
106+
$languageKeysOfInterest = [];
107+
$languagePresets = $this->contentDimensionConfiguration[$this->languageDimensionName]['presets'];
108+
foreach ($languagePresets as $key => $languagePreset) {
109+
$deeplLanguage = $languagePreset['options']['deeplLanguage'] ?? null;
110+
if ($deeplLanguage) {
111+
$deeplLanguagesParts = explode(':', $deeplLanguage);
112+
foreach ($deeplLanguagesParts as $deeplLanguagesPart) {
113+
$keyParts = explode('-', $deeplLanguagesPart);
114+
$languageKeysOfInterest[] = strtolower($keyParts[0]);
115+
}
116+
} else {
117+
$keyParts = explode('-', $key);
118+
$languageKeysOfInterest[] = strtolower($keyParts[0]);
119+
}
120+
}
121+
122+
$languageKeysOfInterest = array_unique($languageKeysOfInterest);
123+
$languagePairs = $this->glossaryService->getGlossaryLanguagePairs();
124+
$sourceTargetCombinations = [];
125+
foreach ($languagePairs as $languagePair) {
126+
if (in_array($languagePair->sourceLang, $languageKeysOfInterest) && in_array($languagePair->targetLang, $languageKeysOfInterest)) {
127+
$existingGlossary = $this->glossaryRepository->findOneBySourceAndTargetLanguageKey($languagePair->sourceLang, $languagePair->targetLang);
128+
if ($existingGlossary) {
129+
continue;
130+
}
131+
$sourceTargetCombinations[] = $languagePair->sourceLang . ' -> ' . $languagePair->targetLang;
132+
}
133+
}
134+
135+
$this->view->assign('sourceTargetCombinations', $sourceTargetCombinations);
136+
}
137+
138+
public function addGlossaryAction(string $sourceAndTarget): void
139+
{
140+
list ($source, $target) = explode(' -> ', $sourceAndTarget);
141+
$existingGlossary = $this->glossaryRepository->findOneBySourceAndTargetLanguageKey($source, $target);
142+
if ($existingGlossary instanceof Glossary) {
143+
$this->addFlashMessage('Glossary already exists!', '', Message::SEVERITY_WARNING);
144+
$this->forward(actionName: 'showGlossary', arguments: ['glossary' => $existingGlossary]);
145+
}
146+
$glossary = Glossary::create($source, $target);
147+
$this->glossaryRepository->add($glossary);
148+
$this->forward('index');
149+
}
150+
151+
public function showGlossaryAction(Glossary $glossary): void
152+
{
153+
$this->view->assign('glossary', $glossary);
154+
}
155+
156+
public function uploadGlossaryAction(Glossary $glossary, bool $toIndex = false): void
157+
{
158+
$identifier = $this->glossaryService->uploadRemoteGlossary($glossary);
159+
160+
if (is_string($identifier)) {
161+
$glossary->updateSynchronizationIdentifier($identifier);
162+
$this->glossaryRepository->update($glossary);
163+
$this->cacheService->flush();
164+
$deleted = $this->glossaryService->cleanupRemoteGlossaries();
165+
$removedNumber = count($deleted);
166+
if ($removedNumber == 0) {
167+
$this->addFlashMessage("Glossary was uploaded", "");
168+
} elseif ($removedNumber == 1) {
169+
$this->addFlashMessage(sprintf("Glossary was uploaded, %s outdated glossary was removed", $removedNumber), "");
170+
} else {
171+
$this->addFlashMessage(sprintf("Glossary was uploaded, %s outdated glossaries were removed", $removedNumber), "");
172+
}
173+
} else {
174+
$this->addFlashMessage("Upload failed", "", Message::SEVERITY_ERROR);
175+
}
176+
177+
if ($toIndex === true) {
178+
$this->forward(actionName: 'index');
179+
} else {
180+
$this->forward(actionName: 'showGlossary', arguments: ['glossary' => $glossary]);
181+
}
182+
}
183+
184+
public function deleteGlossaryAction(Glossary $glossary): void
185+
{
186+
$this->glossaryRepository->remove($glossary);
187+
$this->addFlashMessage('Glossary deleted');
188+
$this->forward('index');
189+
}
190+
191+
public function createGlossaryEntryAction(Glossary $glossary): void
192+
{
193+
$this->view->assign('glossary', $glossary);
194+
}
195+
196+
public function addGlossaryEntryAction(Glossary $glossary, string $sourceText, string $targetText): void
197+
{
198+
$entry = new GlossaryEntry();
199+
$entry->glossary = $glossary;
200+
$entry->sourceText = $sourceText;
201+
$entry->targetText = $targetText;
202+
$glossary->addEntry($entry);
203+
$this->glossaryRepository->update($glossary);
204+
$this->forward(actionName: 'showGlossary', arguments: ['glossary' => $glossary]);
205+
}
206+
public function editGlossaryEntryAction(GlossaryEntry $entry): void
207+
{
208+
$this->view->assign('entry', $entry);
209+
$this->view->assign('glossary', $entry->glossary);
210+
}
211+
212+
public function updateGlossaryEntryAction(GlossaryEntry $entry, string $sourceText, string $targetText): void
213+
{
214+
$entry->sourceText = $sourceText;
215+
$entry->targetText = $targetText;
216+
$entry->glossary->updateModificationDate();
217+
$this->glossaryRepository->update($entry->glossary);
218+
$this->forward(actionName: 'showGlossary', arguments: ['glossary' => $entry->glossary]);
219+
}
220+
221+
public function deleteGlossaryEntryAction(GlossaryEntry $entry): void
222+
{
223+
$glossary = $entry->glossary;
224+
$glossary->removeEntry($entry);
225+
$this->glossaryRepository->update($glossary);
226+
$this->forward(actionName: 'showGlossary', arguments: ['glossary' => $glossary]);
227+
}
54228
}

Classes/Domain/Model/Glossary.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sitegeist\LostInTranslation\Domain\Model;
6+
7+
use Doctrine\Common\Collections\ArrayCollection;
8+
use Doctrine\Common\Collections\Collection;
9+
use Doctrine\ORM\Mapping as ORM;
10+
use Neos\Flow\Annotations as Flow;
11+
use League\Csv\Writer;
12+
13+
/**
14+
* @Flow\Entity
15+
* @ORM\Table(uniqueConstraints={@ORM\UniqueConstraint(name="languageCombination", columns={"sourceLanguageKey", "targetLanguageKey"})})
16+
*/
17+
class Glossary
18+
{
19+
/**
20+
* @var string
21+
*/
22+
public string $sourceLanguageKey;
23+
24+
/**
25+
* @var string
26+
*/
27+
public string $targetLanguageKey;
28+
29+
/**
30+
* @var \DateTimeImmutable|null
31+
* @ORM\Column(nullable=true)
32+
*/
33+
public $synchronizationDate;
34+
35+
/**
36+
* @var string|null
37+
* @ORM\Column(nullable=true)
38+
*/
39+
public $synchronizationIdentifier;
40+
41+
/**
42+
* @var \DateTimeImmutable
43+
*/
44+
public $modificationDate;
45+
46+
/**
47+
* @phpstan-var Collection<int, GlossaryEntry>
48+
* @var Collection<GlossaryEntry>
49+
* @ORM\OneToMany(targetEntity="Sitegeist\LostInTranslation\Domain\Model\GlossaryEntry", mappedBy="glossary", cascade={"persist"})
50+
*/
51+
public $entries;
52+
53+
public static function create(string $source, string $target): Glossary
54+
{
55+
$subject = new Glossary();
56+
$subject->entries = new ArrayCollection();
57+
$subject->sourceLanguageKey = strtoupper($source);
58+
$subject->targetLanguageKey = strtoupper($target);
59+
$subject->modificationDate = new \DateTimeImmutable();
60+
return $subject;
61+
}
62+
63+
public function getLabel(): string
64+
{
65+
return $this->sourceLanguageKey . ' -> ' . $this->targetLanguageKey;
66+
}
67+
68+
public function isUpToDate(): bool
69+
{
70+
return $this->modificationDate <= $this->synchronizationDate;
71+
}
72+
73+
public function addEntry(GlossaryEntry $entry): void
74+
{
75+
$this->modificationDate = new \DateTimeImmutable();
76+
$this->entries->add($entry);
77+
}
78+
79+
public function updateModificationDate(): void
80+
{
81+
$this->modificationDate = new \DateTimeImmutable();
82+
}
83+
84+
public function updateSynchronizationIdentifier(string $id): void
85+
{
86+
$this->synchronizationDate = new \DateTimeImmutable();
87+
$this->synchronizationIdentifier = $id;
88+
}
89+
90+
public function removeEntry(GlossaryEntry $entry): void
91+
{
92+
$this->modificationDate = new \DateTimeImmutable();
93+
$this->entries->removeElement($entry);
94+
}
95+
96+
/**
97+
* @return array<string, string>
98+
*/
99+
public function getEntriesAsAssociativeArray(): array
100+
{
101+
$entries = [];
102+
foreach ($this->entries as $entry) {
103+
$entries[$entry->sourceText] = $entry->targetText;
104+
}
105+
return $entries;
106+
}
107+
}
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\Domain\Model;
6+
7+
use Doctrine\ORM\Mapping as ORM;
8+
use Neos\Flow\Annotations as Flow;
9+
10+
/**
11+
* @Flow\Entity
12+
*/
13+
class GlossaryEntry
14+
{
15+
/**
16+
* @var Glossary
17+
* @ORM\ManyToOne()
18+
*/
19+
public $glossary;
20+
21+
/**
22+
* @var string
23+
* @ORM\Column(type="text")
24+
*/
25+
public $sourceText;
26+
27+
/**
28+
* @var string
29+
* @ORM\Column(type="text")
30+
*/
31+
public $targetText;
32+
}

0 commit comments

Comments
 (0)