Skip to content

Commit 6c2be14

Browse files
author
Andrew Zhdanovskih
committed
Init
0 parents  commit 6c2be14

File tree

18 files changed

+964
-0
lines changed

18 files changed

+964
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor
2+
composer.lock
3+
.phpunit.result.cache

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Sometimes You have to give the visual interface of i18n message CRUD for a customer. To do this, You need to have storage, which is not under version control and allowed from a form.
2+
3+
# I18n messages stored in database
4+
5+
With this bundle i18n messages stored in a database instead of files, then, you can implement web-interface to manage it.
6+
7+
## Some rules:
8+
9+
- you application service container must have aa array `locales` parameter with possible application locales. For example:
10+
```yaml
11+
# config/services.yaml
12+
parameters:
13+
locales: [ 'ru', 'en', 'de' ]
14+
```
15+
- implementation of `Symfony\Contracts\Translation\TranslatorInterface` must have a `getCatalogue` method (usually, it have) for import messages from translation files to database.
16+
- You must define the default messages domain as `db_messages` in you views to use messages from database. For example:
17+
```yaml
18+
# templates/main.html.twig
19+
{% trans_default_domain 'db_messages' %}
20+
```
21+
- update you database schema after install this bundle — use `bin/console doctrine:schema:update` command or make migration for this.
22+
23+
So, now you can load messages from old translation files to the database. Command
24+
25+
```bash
26+
bin/console creative:db-i18n:migrate translations/messages.en.yaml
27+
```
28+
29+
will import all messages from `[project root]/translations/messages.en.yaml`. You can set absolute path instead, nevermind, but file name must be compatible with Symfony localization files agreement — `<domain>.<locale>.<format>`.
30+
31+
After (or instead of) that, make your forms/interfaces and add, change and so on with your messages.

composer.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "creative/symfony-db-i18n-bundle",
3+
"type": "symfony-bundle",
4+
"description": "Allow store i18n-messages in database",
5+
"keywords": ["symfony", "i18n", "translations"],
6+
"license": "MIT",
7+
"version": "0.1",
8+
"authors": [
9+
{
10+
"name": "Andrew Zhdanovskih",
11+
"email": "azh@crtweb.ru"
12+
}
13+
],
14+
"require": {
15+
"php": "^7.2",
16+
"doctrine/common": "^2.8",
17+
"doctrine/doctrine-bundle": "^1.8",
18+
"doctrine/orm": "^2.6",
19+
"doctrine/persistence": "^1.0",
20+
"symfony/config": "^4.1",
21+
"symfony/dependency-injection": "^4.1",
22+
"symfony/doctrine-bridge": "^4.1",
23+
"symfony/finder": "^4.1",
24+
"symfony/framework-bundle": "^4.1",
25+
"symfony/polyfill-mbstring": "^1.7",
26+
"symfony/translation": "^4.1",
27+
"symfony/twig-bridge": "^4.1",
28+
"symfony/twig-bundle": "^4.1",
29+
"twig/extensions": "^1.5",
30+
"twig/twig": "^2.4",
31+
"symfony/yaml": "^4.1"
32+
},
33+
"require-dev": {
34+
"phpunit/phpunit": "^8.1",
35+
"symfony/console": "^4.1",
36+
"symfony/css-selector": "^4.1",
37+
"symfony/dom-crawler": "^4.1",
38+
"symfony/phpunit-bridge": "^4.1",
39+
"symfony/var-dumper": "^4.1"
40+
},
41+
"config": {
42+
"sort-packages": true
43+
},
44+
"autoload": {
45+
"psr-4": {
46+
"Creative\\DbI18nBundle\\": "src"
47+
}
48+
},
49+
"autoload-dev": {
50+
"Creative\\DbI18nBundle\\Tests\\": "tests/"
51+
}
52+
}

phpunit.xml.dist

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.5/phpunit.xsd"
5+
backupGlobals="false"
6+
colors="true"
7+
>
8+
<php>
9+
<ini name="error_reporting" value="-1" />
10+
<env name="APP_ENV" value="test" />
11+
<env name="SHELL_VERBOSITY" value="-1" />
12+
</php>
13+
14+
<testsuites>
15+
<testsuite name="Project Test Suite">
16+
<directory>tests</directory>
17+
</testsuite>
18+
</testsuites>
19+
20+
<filter>
21+
<whitelist>
22+
<directory>src</directory>
23+
</whitelist>
24+
</filter>
25+
</phpunit>
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
<?php
2+
/**
3+
* 2019-04-20.
4+
*/
5+
6+
declare(strict_types=1);
7+
8+
namespace Creative\DbI18nBundle\Command;
9+
10+
use Creative\DbI18nBundle\Interfaces\EntityInterface;
11+
use Creative\DbI18nBundle\Interfaces\TranslationRepositoryInterface;
12+
use Symfony\Bridge\Doctrine\RegistryInterface;
13+
use Symfony\Component\Console\Command\Command;
14+
use Symfony\Component\Console\Exception\RuntimeException;
15+
use Symfony\Component\Console\Input\InputArgument;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Component\Console\Style\SymfonyStyle;
19+
use Symfony\Component\DependencyInjection\ContainerInterface;
20+
use Symfony\Component\Translation\Translator;
21+
use Symfony\Contracts\Translation\TranslatorInterface;
22+
23+
/**
24+
* Class MigrateToDatabaseCommand.
25+
*
26+
* @package Creative\DbI18nBundle\Command
27+
*/
28+
class MigrateToDatabaseCommand extends Command
29+
{
30+
private const HELP = <<<EOL
31+
You can load all messages, stored in translation (yaml / xml) files,
32+
and save it to database to use in future with db-i18n module
33+
34+
Application container must have a 'locales' parameter, and this parameter must be an array.
35+
36+
Filename, passed as argument, must be compatible with Symfony localization files agreement.
37+
For example: <info>messages.ru.yaml</info>
38+
<info>messages.ru.xlf</info>
39+
<info>my_awesome_translations.en.xlf</info>
40+
EOL;
41+
42+
private const BATCH_SIZE = 100;
43+
44+
/**
45+
* @var string
46+
*/
47+
protected static $defaultName = 'creative:db-i18n:migrate';
48+
49+
/**
50+
* @var ContainerInterface
51+
*/
52+
private $container;
53+
54+
/**
55+
* @var TranslatorInterface|Translator
56+
*/
57+
private $translator;
58+
59+
/**
60+
* @var string
61+
*/
62+
private $entityClass;
63+
64+
/**
65+
* @var RegistryInterface
66+
*/
67+
private $doctrine;
68+
69+
/**
70+
* @var TranslationRepositoryInterface
71+
*/
72+
private $translationEntityRepository;
73+
74+
/**
75+
* MigrateToDatabaseCommand constructor.
76+
*
77+
* @param ContainerInterface $container
78+
* @param TranslatorInterface $translator
79+
* @param RegistryInterface $doctrine
80+
* @param string|null $name
81+
*/
82+
public function __construct(ContainerInterface $container, TranslatorInterface $translator, RegistryInterface $doctrine, string $name = null)
83+
{
84+
parent::__construct($name);
85+
$this->container = $container;
86+
$this->translator = $translator;
87+
$this->entityClass = $this->container->getParameter('db_i18n.entity');
88+
$this->doctrine = $doctrine;
89+
}
90+
91+
/**
92+
* Configure options.
93+
*/
94+
protected function configure(): void
95+
{
96+
$this->setDescription('Load data from translation file and pass it to database')
97+
->addArgument('source-file', InputArgument::REQUIRED, 'File to import')
98+
->setHelp(self::HELP)
99+
;
100+
}
101+
102+
/**
103+
* @param InputInterface $input
104+
* @param OutputInterface $output
105+
*
106+
* @return int|void|null
107+
*/
108+
protected function execute(InputInterface $input, OutputInterface $output)
109+
{
110+
$this->translationEntityRepository = $this->doctrine->getRepository($this->entityClass);
111+
112+
if (!method_exists($this->translator, 'getCatalogue')) {
113+
throw new RuntimeException('Translator service of application has no \'getCatalogue\' method');
114+
}
115+
116+
if (!$this->container->hasParameter('locales') || !is_array($this->container->getParameter('locales'))) {
117+
throw new RuntimeException('Application container must have a \'locales\' parameter, and this parameter must be an array');
118+
}
119+
120+
$io = new SymfonyStyle($input, $output);
121+
$filePath = $this->locateFile($input->getArgument('source-file'));
122+
123+
$locale = $this->getLocale(pathinfo($filePath, PATHINFO_FILENAME));
124+
$domain = trim(str_replace($locale, '', pathinfo($filePath, PATHINFO_FILENAME)), '.');
125+
$catalogue = $this->translator->getCatalogue($locale);
126+
127+
$forExport = $catalogue->all($domain);
128+
$exported = $this->exportToDatabase($forExport, $locale, $this->container->getParameter('db_i18n.domain'));
129+
130+
$io->writeln(sprintf(
131+
'Loaded form %s: %u messages, exported to database: %s',
132+
$filePath,
133+
count($forExport),
134+
$exported
135+
));
136+
137+
return 0;
138+
}
139+
140+
/**
141+
* @param array $messages
142+
* @param string $locale
143+
* @param string $domain
144+
*
145+
* @return int
146+
*/
147+
protected function exportToDatabase(array $messages, string $locale, string $domain): int
148+
{
149+
$count = 0;
150+
$i = 0;
151+
$em = $this->doctrine->getManager();
152+
foreach ($messages as $key => $value) {
153+
++$count;
154+
++$i;
155+
$em->persist($this->makeEntity($key, $value, $locale, $domain));
156+
if ($i > self::BATCH_SIZE) {
157+
$i = 0;
158+
$em->flush();
159+
}
160+
}
161+
162+
return $count;
163+
}
164+
165+
/**
166+
* @param string $key
167+
* @param string $translation
168+
* @param string $locale
169+
*
170+
* @return EntityInterface
171+
*/
172+
protected function makeEntity(string $key, string $translation, string $locale, string $domain): EntityInterface
173+
{
174+
$entity = $this->checkEntityExists($locale, $key);
175+
$entity->load([
176+
'domain' => $domain,
177+
'locale' => $locale,
178+
'key' => $key,
179+
'translation' => $translation,
180+
]);
181+
182+
return $entity;
183+
}
184+
185+
/**
186+
* @param string $locale
187+
* @param string $key
188+
*
189+
* @return EntityInterface|object
190+
*/
191+
protected function checkEntityExists(string $locale, string $key): EntityInterface
192+
{
193+
$entity = $this->translationEntityRepository->findOneBy([
194+
'locale' => $locale,
195+
'key' => $key,
196+
]);
197+
198+
if ($entity === null) {
199+
$entity = new $this->entityClass();
200+
}
201+
202+
return $entity;
203+
}
204+
205+
/**
206+
* @param string $filename
207+
*
208+
* @return string
209+
*/
210+
protected function getLocale(string $filename): ?string
211+
{
212+
$locales = $this->container->getParameter('locales');
213+
$locale = null;
214+
foreach ($locales as $localeParam) {
215+
if (strpos($filename, $localeParam) !== false) {
216+
$locale = $localeParam;
217+
}
218+
}
219+
220+
if ($locale === null) {
221+
throw new RuntimeException(sprintf('No one %s found in \'%s\'', implode(', ', $locales), $filename));
222+
}
223+
224+
return $locale;
225+
}
226+
227+
/**
228+
* @param string $path
229+
*
230+
* @return string
231+
*/
232+
protected function locateFile(string $path): string
233+
{
234+
$realPath = null;
235+
if (strpos($path, '/') === 0) {
236+
$realPath = $path;
237+
} else {
238+
$realPath = $this->container->getParameter('kernel.root_dir') . '/../' . $path;
239+
}
240+
241+
if (!is_file($realPath) || !is_readable($realPath)) {
242+
throw new RuntimeException(sprintf('Unable to load %s file', $realPath));
243+
}
244+
245+
return $realPath;
246+
}
247+
}

src/DbI18nBundle.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
/**
3+
* 2019-04-20.
4+
*/
5+
6+
declare(strict_types=1);
7+
8+
namespace Creative\DbI18nBundle;
9+
10+
use Symfony\Component\DependencyInjection\ContainerBuilder;
11+
use Symfony\Component\HttpKernel\Bundle\Bundle;
12+
13+
class DbI18nBundle extends Bundle
14+
{
15+
public function build(ContainerBuilder $container): void
16+
{
17+
}
18+
}

0 commit comments

Comments
 (0)