diff --git a/src/Storage/ArrayStorage.php b/src/Storage/ArrayStorage.php new file mode 100644 index 0000000..9056966 --- /dev/null +++ b/src/Storage/ArrayStorage.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Common\Storage; + +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageCatalogueInterface; +use Translation\Common\Model\Message; +use Translation\Common\Model\MessageInterface; + +/** + * An in-memory storage. + */ +final class ArrayStorage implements StorageInterface +{ + /** + * @var MessageCatalogue[] + */ + private $catalogues; + + /** + * {@inheritdoc} + */ + public function get(string $locale, string $domain, string $key): ?MessageInterface + { + $translation = $this->getCatalogue($locale)->get($key, $domain); + + return new Message($key, $domain, $locale, $translation); + } + + /** + * {@inheritdoc} + */ + public function create(MessageInterface $message): void + { + $catalogue = $this->getCatalogue($message->getLocale()); + if (!$catalogue->defines($message->getKey(), $message->getDomain())) { + $catalogue->set($message->getKey(), $message->getTranslation(), $message->getDomain()); + } + } + + /** + * {@inheritdoc} + */ + public function update(MessageInterface $message): void + { + $catalogue = $this->getCatalogue($message->getLocale()); + $catalogue->set($message->getKey(), $message->getTranslation(), $message->getDomain()); + } + + /** + * {@inheritdoc} + */ + public function delete(string $locale, string $domain, string $key): void + { + $catalogue = $this->getCatalogue($locale); + $messages = $catalogue->all($domain); + unset($messages[$key]); + + $catalogue->replace($messages, $domain); + } + + /** + * {@inheritdoc} + */ + public function export(MessageCatalogueInterface $catalogue, array $options = []): void + { + $catalogue->addCatalogue($this->getCatalogue($catalogue->getLocale())); + } + + /** + * {@inheritdoc} + */ + public function import(MessageCatalogueInterface $catalogue, array $options = []): void + { + $this->getCatalogue($catalogue->getLocale())->addCatalogue($catalogue); + } + + private function getCatalogue(string $locale): MessageCatalogue + { + if (empty($this->catalogues[$locale])) { + $this->catalogues[$locale] = new MessageCatalogue($locale); + } + + return $this->catalogues[$locale]; + } +} diff --git a/src/Storage/ChainStorage.php b/src/Storage/ChainStorage.php index 16c754c..5ea5fbc 100644 --- a/src/Storage/ChainStorage.php +++ b/src/Storage/ChainStorage.php @@ -19,6 +19,9 @@ */ class ChainStorage implements StorageInterface { + const DIRECTION_UP = 'up'; + const DIRECTION_DOWN = 'down'; + private $storages = []; /** @@ -83,7 +86,14 @@ public function delete(string $locale, string $domain, string $key): void */ public function export(MessageCatalogueInterface $catalogue, array $options = []): void { - foreach ($this->storages as $storage) { + $options['direction'] = $options['direction'] ?? self::DIRECTION_DOWN; + + $storages = $this->storages; + if (isset($options['direction']) && self::DIRECTION_UP === $options['direction']) { + $storages = array_reverse($storages); + } + + foreach ($storages as $storage) { $storage->export($catalogue, $options); } } diff --git a/src/Storage/StorageInterface.php b/src/Storage/StorageInterface.php index f8b05e7..c25cadf 100644 --- a/src/Storage/StorageInterface.php +++ b/src/Storage/StorageInterface.php @@ -48,6 +48,9 @@ public function delete(string $locale, string $domain, string $key): void; /** * Get messages from the storage into the $catalogue. * + * This action should be considered as a "force merge". Existing messages + * in the storage will be overwritten but no message will be removed. + * * @var array a list of arbitrary options that could be used. The array SHOULD * use a format of array. * Example: ['foo' => ['bar', 'baz]] @@ -55,9 +58,10 @@ public function delete(string $locale, string $domain, string $key): void; public function export(MessageCatalogueInterface $catalogue, array $options = []): void; /** - * Populate the storage with all the messages in $catalogue. This action - * should be considered as a "force merge". Existing messages in the storage - * will be overwritten but no message will be removed. + * Populate the storage with all the messages in $catalogue. + * + * This action should be considered as a "force merge". Existing messages + * in the storage will be overwritten but no message will be removed. * * @var array a list of arbitrary options that could be used. The array SHOULD * use a format of array. diff --git a/tests/Unit/Storage/ArrayStorageTest.php b/tests/Unit/Storage/ArrayStorageTest.php new file mode 100644 index 0000000..091875d --- /dev/null +++ b/tests/Unit/Storage/ArrayStorageTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\common\tests\Unit\Storage; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Translation\MessageCatalogue; +use Translation\Common\Model\Message; +use Translation\Common\Storage\ArrayStorage; + +class ArrayStorageTest extends TestCase +{ + private $messages; + private $storage; + + public function setUp(): void + { + $this->messages = [ + 'messages_en_foo' => new Message('foo', 'messages', 'en', 'I am the "foo" translation for English in the "messages" domain.'), + 'messages_fr_foo' => new Message('foo', 'messages', 'fr', 'Je suis la traduction de la clé "foo" en français dans le domain "messages".'), + 'messages_en_bar' => new Message('bar', 'messages', 'en', 'I am the "bar" translation for English in the "messages" domain.'), + 'messages_fr_bar' => new Message('bar', 'messages', 'fr', 'Je suis la traduction de la clé "bar" en français dans le domain "messages".'), + 'validators_en_foo' => new Message('foo', 'validators', 'en', 'I am the "foo" translation for English in the "validators" domain.'), + 'validators_fr_foo' => new Message('foo', 'validators', 'fr', 'Je suis la traduction de la clé "foo" en français dans le domain "validators".'), + 'validators_en_bar' => new Message('bar', 'validators', 'en', 'I am the "bar" translation for English in the "validators" domain.'), + 'validators_fr_bar' => new Message('bar', 'validators', 'fr', 'Je suis la traduction de la clé "bar" en français dans le domain "validators".'), + ]; + + $this->storage = new ArrayStorage(); + foreach ($this->messages as $message) { + $this->storage->create($message); + } + } + + public function testDelete() + { + $this->assertEquals($this->messages['messages_en_foo'], $this->storage->get('en', 'messages', 'foo')); + $this->storage->delete('en', 'messages', 'foo'); + $this->assertEquals(new Message('foo', 'messages', 'en', 'foo'), $this->storage->get('en', 'messages', 'foo')); + } + + public function testUpdate() + { + $this->assertEquals($this->messages['messages_en_foo'], $this->storage->get('en', 'messages', 'foo')); + $updatedMessage = new Message('foo', 'messages', 'en', 'Updated translation'); + $this->assertEquals($this->messages['messages_en_foo'], $this->storage->get('en', 'messages', 'foo')); + } + + public function testExport() + { + $messageCatalogue = new MessageCatalogue('en'); + $messageCatalogue->set('foo', 'I will be overrided.', 'messages'); + + $this->storage->export($messageCatalogue); + + $this->assertEquals($this->messages['messages_en_foo'], $this->storage->get('en', 'messages', 'foo')); + } + + public function testImport() + { + $messageCatalogue = new MessageCatalogue('en'); + $messageCatalogue->set('foo', 'I will override the existing key.', 'messages'); + + $this->storage->import($messageCatalogue); + + $this->assertEquals(new Message('foo', 'messages', 'en', 'I will override the existing key.'), $this->storage->get('en', 'messages', 'foo')); + } +} diff --git a/tests/Unit/Storage/ChainStorageTest.php b/tests/Unit/Storage/ChainStorageTest.php index d7cc35c..ce2cc10 100644 --- a/tests/Unit/Storage/ChainStorageTest.php +++ b/tests/Unit/Storage/ChainStorageTest.php @@ -12,8 +12,9 @@ namespace Translation\common\tests\Unit\Storage; use PHPUnit\Framework\TestCase; -use Symfony\Component\Translation\MessageCatalogueInterface; +use Symfony\Component\Translation\MessageCatalogue; use Translation\Common\Model\Message; +use Translation\Common\Storage\ArrayStorage; use Translation\Common\Storage\ChainStorage; use Translation\Common\Storage\StorageInterface; @@ -37,7 +38,7 @@ public function testGetWithMessageInFirstStorage() { $expectedMessage = new Message('PHP Translation IS awesome!'); - $this->childStorage1->get('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledtimes(1)->willReturn($expectedMessage); + $this->childStorage1->get('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledTimes(1)->willReturn($expectedMessage); $this->childStorage2->get('en', 'messages', 'php_translation_is_awesome')->shouldNotBeCalled(); $message = $this->storage->get('en', 'messages', 'php_translation_is_awesome'); @@ -48,8 +49,8 @@ public function testGetWithMessageInSecondStorage() { $expectedMessage = new Message('PHP Translation IS awesome!'); - $this->childStorage1->get('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledtimes(1)->willReturn(null); - $this->childStorage2->get('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledtimes(1)->willReturn($expectedMessage); + $this->childStorage1->get('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledTimes(1)->willReturn(null); + $this->childStorage2->get('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledTimes(1)->willReturn($expectedMessage); $message = $this->storage->get('en', 'messages', 'php_translation_is_awesome'); $this->assertSame($expectedMessage, $message); @@ -57,8 +58,8 @@ public function testGetWithMessageInSecondStorage() public function testGetWithMessageNotFound() { - $this->childStorage1->get('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledtimes(1)->willReturn(null); - $this->childStorage2->get('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledtimes(1)->willReturn(null); + $this->childStorage1->get('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledTimes(1)->willReturn(null); + $this->childStorage2->get('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledTimes(1)->willReturn(null); $message = $this->storage->get('en', 'messages', 'php_translation_is_awesome'); $this->assertNull($message); @@ -68,8 +69,8 @@ public function testCreateCallAllStorages() { $message = new Message('PHP Translation IS awesome!'); - $this->childStorage1->create($message)->shouldBeCalledtimes(1); - $this->childStorage2->create($message)->shouldBeCalledtimes(1); + $this->childStorage1->create($message)->shouldBeCalledTimes(1); + $this->childStorage2->create($message)->shouldBeCalledTimes(1); $this->storage->create($message); } @@ -78,35 +79,144 @@ public function testUpdateCallAllStorages() { $message = new Message('PHP Translation IS awesome!'); - $this->childStorage1->update($message)->shouldBeCalledtimes(1); - $this->childStorage2->update($message)->shouldBeCalledtimes(1); + $this->childStorage1->update($message)->shouldBeCalledTimes(1); + $this->childStorage2->update($message)->shouldBeCalledTimes(1); $this->storage->update($message); } public function testDeleteCallAllStorages() { - $this->childStorage1->delete('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledtimes(1); - $this->childStorage2->delete('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledtimes(1); + $this->childStorage1->delete('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledTimes(1); + $this->childStorage2->delete('en', 'messages', 'php_translation_is_awesome')->shouldBeCalledTimes(1); $this->storage->delete('en', 'messages', 'php_translation_is_awesome'); } - public function testExportCallOnlyTransferrableStorage() + public function testExport() { - $messageCatalogue = $this->prophesize(MessageCatalogueInterface::class)->reveal(); - - $this->childStorage2->export($messageCatalogue, [])->shouldBeCalledtimes(1); - - $this->storage->export($messageCatalogue, []); + $firstStorage = new ArrayStorage(); + $firstStorage->create(new Message('common', 'messages', 'en', 'Translation from first storage')); + $firstStorage->create(new Message('foo', 'messages', 'en', 'Only in first storage')); + $firstStorage->create(new Message('baz', 'domain_1', 'en', 'Baz in domain 1')); + $secondStorage = new ArrayStorage(); + $secondStorage->create(new Message('common', 'messages', 'en', 'Translation from second storage')); + $secondStorage->create(new Message('bar', 'messages', 'en', 'Only in second storage')); + $secondStorage->create(new Message('baz', 'domain_2', 'en', 'Baz in domain 2')); + + $messageCatalogue = new MessageCatalogue('en'); + $messageCatalogue->set('common', 'Translation from the existing catalogue.', 'messages'); + $messageCatalogue->set('baz', 'Baz in domain 3', 'domain_3'); + + $storage = new ChainStorage([$firstStorage, $secondStorage]); + $storage->export($messageCatalogue, []); + + $expectedMessages = [ + 'messages' => [ + 'common' => 'Translation from second storage', + 'foo' => 'Only in first storage', + 'bar' => 'Only in second storage', + ], + 'domain_3' => [ + 'baz' => 'Baz in domain 3', + ], + 'domain_1' => [ + 'baz' => 'Baz in domain 1', + ], + 'domain_2' => [ + 'baz' => 'Baz in domain 2', + ], + ]; + + $this->assertSame($expectedMessages, $messageCatalogue->all()); } - public function testImportCallOnlyTransferrableStorage() + public function testExportDown() { - $messageCatalogue = $this->prophesize(MessageCatalogueInterface::class)->reveal(); - - $this->childStorage2->import($messageCatalogue, [])->shouldBeCalledtimes(1); + $firstStorage = new ArrayStorage(); + $firstStorage->create(new Message('common', 'messages', 'en', 'Translation from first storage')); + $firstStorage->create(new Message('foo', 'messages', 'en', 'Only in first storage')); + $firstStorage->create(new Message('baz', 'domain_1', 'en', 'Baz in domain 1')); + $secondStorage = new ArrayStorage(); + $secondStorage->create(new Message('common', 'messages', 'en', 'Translation from second storage')); + $secondStorage->create(new Message('bar', 'messages', 'en', 'Only in second storage')); + $secondStorage->create(new Message('baz', 'domain_2', 'en', 'Baz in domain 2')); + + $messageCatalogue = new MessageCatalogue('en'); + $messageCatalogue->set('common', 'Translation from the existing catalogue.', 'messages'); + $messageCatalogue->set('baz', 'Baz in domain 3', 'domain_3'); + + $storage = new ChainStorage([$firstStorage, $secondStorage]); + $storage->export($messageCatalogue, ['direction' => ChainStorage::DIRECTION_UP]); + + $expectedMessages = [ + 'messages' => [ + 'common' => 'Translation from first storage', + 'bar' => 'Only in second storage', + 'foo' => 'Only in first storage', + ], + 'domain_3' => [ + 'baz' => 'Baz in domain 3', + ], + 'domain_2' => [ + 'baz' => 'Baz in domain 2', + ], + 'domain_1' => [ + 'baz' => 'Baz in domain 1', + ], + ]; + + $this->assertSame($expectedMessages, $messageCatalogue->all()); + } - $this->storage->import($messageCatalogue, []); + public function testImport() + { + $firstStorage = new ArrayStorage(); + $firstStorage->create(new Message('common', 'messages', 'en', 'Translation from first storage')); + $firstStorage->create(new Message('foo', 'messages', 'en', 'Only in first storage')); + $firstStorage->create(new Message('baz', 'domain_1', 'en', 'Baz in domain 1')); + $secondStorage = new ArrayStorage(); + $secondStorage->create(new Message('common', 'messages', 'en', 'Translation from second storage')); + $secondStorage->create(new Message('bar', 'messages', 'en', 'Only in second storage')); + $secondStorage->create(new Message('baz', 'domain_2', 'en', 'Baz in domain 2')); + + $messageCatalogue = new MessageCatalogue('en'); + $messageCatalogue->set('common', 'Translation from the existing catalogue.', 'messages'); + $messageCatalogue->set('baz', 'Baz in domain 3', 'domain_3'); + + $storage = new ChainStorage([$firstStorage, $secondStorage]); + $storage->import($messageCatalogue, []); + + $expectedMessagesInFirstStorage = [ + 'messages' => [ + 'common' => 'Translation from the existing catalogue.', + 'foo' => 'Only in first storage', + ], + 'domain_1' => [ + 'baz' => 'Baz in domain 1', + ], + 'domain_3' => [ + 'baz' => 'Baz in domain 3', + ], + ]; + + $firstStorage->export($messagesInFirstStorage = new MessageCatalogue('en'), []); + $this->assertSame($expectedMessagesInFirstStorage, $messagesInFirstStorage->all()); + + $expectedMessagesInSecondStorage = [ + 'messages' => [ + 'common' => 'Translation from the existing catalogue.', + 'bar' => 'Only in second storage', + ], + 'domain_2' => [ + 'baz' => 'Baz in domain 2', + ], + 'domain_3' => [ + 'baz' => 'Baz in domain 3', + ], + ]; + + $secondStorage->export($messagesInSecondStorage = new MessageCatalogue('en'), []); + $this->assertSame($expectedMessagesInSecondStorage, $messagesInSecondStorage->all()); } }