Skip to content

Commit

Permalink
Merge pull request #3 from jdreesen/rewrite
Browse files Browse the repository at this point in the history
Rewrite
  • Loading branch information
lukadschaak authored Nov 29, 2022
2 parents 004f5d4 + 90aac2c commit bb7171e
Show file tree
Hide file tree
Showing 24 changed files with 496 additions and 402 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

This bundle combines the advantages of Symfony translation files and translations in the Pimcore admin backend.

This bundle reads standard symfony translation files in yaml format and migrates them to Pimcore translations. Changed Pimcore translations are not overwritten.
This bundle reads standard symfony translation files and migrates them to Pimcore translations. Changed Pimcore translations are not overwritten.

## Installation

Expand All @@ -16,10 +16,9 @@ Require via Composer
composer require teamneusta/pimcore-translation-migration-bundle
```

As this is a Pimcore bundle, enable and install it
Enable it
```shell
bin/console pimcore:bundle:enable NeustaPimcoreTranslationMigrationBundle
bin/console pimcore:bundle:install NeustaPimcoreTranslationMigrationBundle
```

## Usage
Expand Down
38 changes: 38 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types=1);

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Neusta\Pimcore\TranslationMigrationBundle\Command\TranslationsMigrateCommand;
use Neusta\Pimcore\TranslationMigrationBundle\Source\SourceFinder;
use Neusta\Pimcore\TranslationMigrationBundle\Source\SourceProvider;
use Neusta\Pimcore\TranslationMigrationBundle\Source\SymfonySourceFinder;
use Neusta\Pimcore\TranslationMigrationBundle\Source\SymfonySourceProvider;
use Neusta\Pimcore\TranslationMigrationBundle\Target\PimcoreTargetRepository;
use Neusta\Pimcore\TranslationMigrationBundle\Target\TargetRepository;

return static function (ContainerConfigurator $container) {
$container->services()
->set(SymfonySourceFinder::class, SymfonySourceFinder::class)
->alias(SourceFinder::class, SymfonySourceFinder::class)

->set(SymfonySourceProvider::class, SymfonySourceProvider::class)
->args([
service('event_dispatcher'),
service(SourceFinder::class),
[], // Resource directories
param('kernel.enabled_locales'),
])
->alias(SourceProvider::class, SymfonySourceProvider::class)

->set(PimcoreTargetRepository::class, PimcoreTargetRepository::class)
->alias(TargetRepository::class, PimcoreTargetRepository::class)

->set(TranslationsMigrateCommand::class, TranslationsMigrateCommand::class)
->args([
service(SourceProvider::class),
service(TargetRepository::class),
service('event_dispatcher'),
])
->tag('console.command')
;
};
5 changes: 0 additions & 5 deletions config/services.yaml

This file was deleted.

10 changes: 0 additions & 10 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
parameters:
ignoreErrors:
-
message: "#^Property Neusta\\\\Pimcore\\\\TranslationMigrationBundle\\\\Command\\\\TranslationsMigrateCommand\\:\\:\\$editableLanguages \\(array\\) does not accept array\\|null\\.$#"
count: 1
path: src/Command/TranslationsMigrateCommand.php

-
message: "#^Property Neusta\\\\Pimcore\\\\TranslationMigrationBundle\\\\Command\\\\TranslationsMigrateCommand\\:\\:\\$editableLanguages type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/TranslationsMigrateCommand.php

-
message: "#^Argument of an invalid type array\\|bool\\|float\\|int\\|string\\|UnitEnum\\|null supplied for foreach, only iterables are supported\\.$#"
count: 1
Expand Down
261 changes: 58 additions & 203 deletions src/Command/TranslationsMigrateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,30 @@

namespace Neusta\Pimcore\TranslationMigrationBundle\Command;

use Neusta\Pimcore\TranslationMigrationBundle\Service\ArrayTransformationService;
use Neusta\Pimcore\TranslationMigrationBundle\Event\FileCannotBeLoaded;
use Neusta\Pimcore\TranslationMigrationBundle\Event\FileWasLoaded;
use Neusta\Pimcore\TranslationMigrationBundle\Source\SourceProvider;
use Neusta\Pimcore\TranslationMigrationBundle\Target\TargetRepository;
use Pimcore\Console\AbstractCommand;
use Pimcore\Model\Translation;
use Pimcore\Model\User;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

#[AsCommand(
name: 'neusta:translations:migrate',
description: 'Creates Pimcore translations for every Symfony translation file.',
)]
final class TranslationsMigrateCommand extends AbstractCommand
{
protected static $defaultName = 'neusta:translations:migrate';
protected static $defaultDescription = 'Creates Pimcore translations for every YAML translation file.';
private array $editableLanguages = [];
private const DOMAIN = 'messages';
private const PROJECT_ROOT = PIMCORE_PROJECT_ROOT . '/';

/**
* @param string[] $translationFilePaths
*/
public function __construct(
private array $translationFilePaths,
private SourceProvider $sourceProvider,
private TargetRepository $targetRepository,
private EventDispatcherInterface $eventDispatcher,
) {
parent::__construct();
}
Expand All @@ -40,214 +42,67 @@ protected function configure(): void
<info>php %command.full_name%</info>
<info>php %command.full_name% -v</info>
By default, this command uses YAML translation files from %kernel.project_dir%/translations.
This can be changed via Symfony service definitions (services.yaml).
EOF
);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->io->comment('Start migrating YAML translations to Pimcore translations');

// User may be not allowed to store all languages
// Idea from \Pimcore\Model\Translation\Dao
$userId = \Pimcore\Tool\Admin::getCurrentUser()?->getId() ?? 0;
$user = User::getById($userId);
$this->editableLanguages = $user instanceof User ? $user->getAllowedLanguagesForEditingWebsiteTranslations() : [];

$this->writelnVerbose('Read from directories:');
if (OutputInterface::VERBOSITY_VERBOSE === $this->io->getVerbosity()) {
$basePaths = \array_map(fn ($path) => $this->stripProjectPrefix($path), $this->translationFilePaths);
$this->io->listing($basePaths);
$isVerbose = OutputInterface::VERBOSITY_VERBOSE === $this->io->getVerbosity();

$this->io->comment('Start migrating translations to Pimcore translations');

if ($isVerbose) {
$output->writeln('Reading from directories:');
$this->io->listing(\array_map(
fn (string $path): string => $this->stripProjectPrefix($path),
$this->sourceProvider->getDirectories(),
));

$ioTableRows = [];
$this->eventDispatcher->addListener(
FileWasLoaded::class,
function (FileWasLoaded $event) use (&$ioTableRows): void {
$ioTableRows[] = [
$this->stripProjectPrefix($event->file->file()->getRealPath()),
'PARSED',
];
},
);
$this->eventDispatcher->addListener(
FileCannotBeLoaded::class,
function (FileCannotBeLoaded $event) use (&$ioTableRows): void {
$ioTableRows[] = [
$this->stripProjectPrefix($event->file->file()->getRealPath()),
sprintf('IGNORED: %s', $event->exception->getMessage()),
];
},
);
}

$finder = $this->createFinder();
$translationsFromFiles = $this->readTranslationsFromFiles($finder);

$translationsGroupedByKey = $this->regroupTranslationArray($translationsFromFiles);
$currentPimcoreTranslations = $this->loadPimcoreTranslations();
$translations = $this->sourceProvider->getTranslations(self::DOMAIN);

$translationsGroupedByKey = $this->filterTranslationsFromFiles(
$translationsGroupedByKey,
$currentPimcoreTranslations,
);
if ($isVerbose) {
$this->io->table(['Realpath', 'Result'], $ioTableRows);
$output->writeln(sprintf('Found %s translation keys in translation files', \count($translations)));
$output->writeln('');
$output->writeln('Loading Pimcore translations from database');
$output->writeln(sprintf('Found %s Pimcore translation keys in database', $this->targetRepository->count()));
}

$this->createPimcoreTranslations($translationsGroupedByKey);
$this->io->info(\count($translationsGroupedByKey) . ' translation keys were added to Pimcore.');
$translationsToUpdate = $translations->withoutIds(...$this->targetRepository->getModifiedIds());
$this->targetRepository->save($translationsToUpdate);

$this->io->info(sprintf('%s translation keys were added to Pimcore.', \count($translationsToUpdate)));
$this->io->success('Pimcore translations updated successfully');

return Command::SUCCESS;
}

private function writelnVerbose(string $output): void
{
$this->output->writeln($output, OutputInterface::VERBOSITY_VERBOSE);
}

private function stripProjectPrefix(string $string): string
{
$prefix = PIMCORE_PROJECT_ROOT . '/';

return str_starts_with($string, $prefix)
? substr($string, strlen($prefix))
return str_starts_with($string, self::PROJECT_ROOT)
? substr($string, strlen(self::PROJECT_ROOT))
: $string;
}

private function createFinder(): Finder
{
$finder = new Finder();
$finder->in($this->translationFilePaths);

return $finder;
}

/**
* @return array<string, array<string, string>>
*/
private function readTranslationsFromFiles(Finder $finder): array
{
$ioTableRows = [];
$translationsFromFiles = [];
foreach ($finder->files() as $translationYaml) {
$filename = $translationYaml->getFilename();
$basePath = $this->stripProjectPrefix($translationYaml->getRealPath());

if (!$this->isYamlFile($filename)) {
$ioTableRows[] = [$basePath, 'IGNORED: Not a \'messages\' Yaml file'];
continue;
}

$locale = $this->getLocale($filename);

// User may be not allowed to store all languages
if (!\in_array($locale, $this->editableLanguages, true)) {
$ioTableRows[] = [$basePath, 'IGNORED: User has no permission for editing this language'];
continue;
}

// Parse can throw ParseException. Bubble it up.
$newTranslations = Yaml::parse($translationYaml->getContents());
$alreadyCollectedTranslations = $translationsFromFiles[$locale] ?? [];
$translationsFromFiles[$locale] = array_merge($newTranslations, $alreadyCollectedTranslations);

$ioTableRows[] = [$basePath, 'PARSED'];
}

$translationsFromFiles = $this->flattenTheYaml($translationsFromFiles);

if (OutputInterface::VERBOSITY_VERBOSE === $this->io->getVerbosity()) {
$headers = ['Realpath', 'Result'];
$this->io->table($headers, $ioTableRows);
}

return $translationsFromFiles;
}

private function isYamlFile(string $filename): bool
{
return null !== $this->getLocale($filename);
}

private function getLocale(string $filename): ?string
{
// Matches 'messages.de.yaml' as well as 'messages+intl-icu.de_DE.yml'
$result = preg_match('/messages.*\.(\w{2,5})\.(yml|yaml)$/', $filename, $matches);

return 1 === $result || \count($matches) > 1 ? $matches[1] : null;
}

/**
* If the Yaml contains hierarchical data, this method uses Parts from Symfony Translation
* Component to flatten all keys by imploding them with dots.
*
* @param array<string, array<string, string>> $translationsFromFiles
*
* @return array<string, array<string, string>>
*/
private function flattenTheYaml(array $translationsFromFiles): array
{
$locales = \array_keys($translationsFromFiles);

foreach ($locales as $locale) {
$messageCatalogue = (new ArrayLoader())->load($translationsFromFiles[$locale], $locale);
$translationsFromFiles[$locale] = $messageCatalogue->all()['messages'];
}

return $translationsFromFiles;
}

/**
* @param array<string, array<string, string>> $translationsFromFiles
*
* @return array<string, array<string, string>>
*/
private function regroupTranslationArray(array $translationsFromFiles): array
{
$mergedTranslations = (new ArrayTransformationService())->groupByTranslationKey($translationsFromFiles);

$this->writelnVerbose(
sprintf('Found %s translation keys in translation files', \count($mergedTranslations)),
);

return $mergedTranslations;
}

/**
* @return array<Translation>
*/
private function loadPimcoreTranslations(): array
{
$this->output->writeln('');
$this->writelnVerbose('Loading Pimcore translations from database');

$databaseTranslations = (new Translation\Listing())->load();

$this->writelnVerbose(
sprintf('Found %s Pimcore translation keys in database', \count($databaseTranslations)),
);

return $databaseTranslations;
}

/**
* @param array<string, array<string, string>> $translationsFromFiles
* @param array<Translation> $currentPimcoreTranslations
*
* @return array<string, array<string, string>>
*/
private function filterTranslationsFromFiles(
array $translationsFromFiles,
array $currentPimcoreTranslations,
): array {
foreach ($currentPimcoreTranslations as $databaseTranslation) {
if ($this->wasModifiedAfterMigration($databaseTranslation)) {
// Do not overwrite modified values
$key = $databaseTranslation->getKey();
unset($translationsFromFiles[$key]);
}
}

return $translationsFromFiles;
}

private function wasModifiedAfterMigration(Translation $databaseTranslation): bool
{
return $databaseTranslation->getCreationDate() !== $databaseTranslation->getModificationDate();
}

/**
* @param array<string, array<string, string>> $translationsGroupedByKey
*/
private function createPimcoreTranslations(array $translationsGroupedByKey): void
{
foreach ($translationsGroupedByKey as $translationKey => $translations) {
$newTranslation = new Translation();
$newTranslation->setKey($translationKey);
$newTranslation->setTranslations($translations);
$newTranslation->save();
}
}
}
Loading

0 comments on commit bb7171e

Please sign in to comment.