From 358959522debe1f22ec1a6b69d9089171f036881 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Tue, 14 Dec 2021 02:29:06 +0100 Subject: [PATCH] release 1.16.9 (#3018) * fix filter action display * allow to set system configuration during runtime * bump version * use deepl pro free to translate missing keys * replacing broken Github action --- .github/workflows/coverage.yaml | 29 ++- .github/workflows/lockfiles.yaml | 2 + .github/workflows/testing.yaml | 12 +- src/Command/TranslationCommand.php | 184 +++++++++++++++++- .../StringAccessibleConfigTrait.php | 45 +++-- src/Constants.php | 4 +- .../Actions/UserSubscriber.php | 2 +- 7 files changed, 249 insertions(+), 29 deletions(-) diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 9ddd7ec5..9d3db37a 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -23,16 +23,36 @@ jobs: name: Coverage (${{ matrix.php }}) steps: - - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v2 + + - name: Clone Kimai + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: pcov extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap - - uses: ramsey/composer-install@v1 - - run: composer require laminas/laminas-ldap + + - name: Determine composer cache directory + id: composer-cache + run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" + + - name: Cache Composer dependencies + uses: actions/cache@v2 + with: + path: "${{ steps.composer-cache.outputs.directory }}" + key: ${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} + + - name: Install dependencies + run: composer install + + - name: Install LDAP package + run: composer require laminas/laminas-ldap + - name: Setup problem matchers for PHPUnit run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Run tests run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml env: @@ -40,6 +60,7 @@ jobs: APP_ENV: dev MAILER_URL: null://localhost TEST_WITH_BUNDLES: 1 + - name: Upload code coverage uses: codecov/codecov-action@v2 with: diff --git a/.github/workflows/lockfiles.yaml b/.github/workflows/lockfiles.yaml index 66de9e94..22ed9e71 100644 --- a/.github/workflows/lockfiles.yaml +++ b/.github/workflows/lockfiles.yaml @@ -9,8 +9,10 @@ jobs: runs-on: ubuntu-latest name: Verify lock file integrity steps: + - name: Clone Kimai uses: actions/checkout@v2 + - name: Prevent file change uses: xalvarez/prevent-file-change-action@v1 with: diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 4b148a00..4dbcc519 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -35,8 +35,18 @@ jobs: extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap tools: cs2pr:1.1.0 + - name: Determine composer cache directory + id: composer-cache + run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" + + - name: Cache Composer dependencies + uses: actions/cache@v2 + with: + path: "${{ steps.composer-cache.outputs.directory }}" + key: ${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} + - name: Install dependencies - uses: ramsey/composer-install@v1 + run: composer install - name: Validate Composer run: composer validate --strict diff --git a/src/Command/TranslationCommand.php b/src/Command/TranslationCommand.php index 2ce70ee7..e54384cb 100644 --- a/src/Command/TranslationCommand.php +++ b/src/Command/TranslationCommand.php @@ -10,11 +10,14 @@ namespace App\Command; use App\Kernel; +use App\Utils\LanguageService; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\HttpClient\HttpClient; /** * Command used to execute all the basic application bootstrapping AFTER "composer install" was executed. @@ -25,12 +28,14 @@ class TranslationCommand extends Command { private $projectDirectory; private $environment; + private $languageService; - public function __construct(string $projectDirectory, string $kernelEnvironment) + public function __construct(string $projectDirectory, string $kernelEnvironment, LanguageService $languageService) { parent::__construct(); $this->projectDirectory = $projectDirectory; $this->environment = $kernelEnvironment; + $this->languageService = $languageService; } /** @@ -45,6 +50,9 @@ class TranslationCommand extends Command ->addOption('duplicates', null, InputOption::VALUE_NONE, 'Find duplicate translation keys') ->addOption('delete-resname', null, InputOption::VALUE_REQUIRED, 'Deletes the translation by resname') ->addOption('extension', null, InputOption::VALUE_NONE, 'Find translation files with wrong extensions') + ->addOption('translate-locale', null, InputOption::VALUE_REQUIRED, 'Translate into the given locale') + // @see https://www.deepl.com/de/pro#developer + ->addOption('translate-deepl', null, InputOption::VALUE_REQUIRED, 'Translate using the "DeepL API Free" auth-key') ; } @@ -115,9 +123,183 @@ class TranslationCommand extends Command } } + $locale = $input->getOption('translate-locale'); + $deepl = $input->getOption('translate-deepl'); + + if ($locale !== null && $deepl === null) { + $io->error('Missing "DeepL API Free" auth-key'); + + return 1; + } + + if ($locale === null && $deepl !== null) { + $io->error('Missing translation locale'); + + return 1; + } + + if ($locale !== null && $deepl !== null) { + // see https://github.com/octfx/DeepLy/blob/master/src/DeepLy.php + $deeplySupportedLanguages = [ + 'de' => 'DE', + 'en' => 'EN-US', + 'fr' => 'FR', + 'it' => 'IT', + 'ja' => 'JA', + 'es' => 'ES', + 'nl' => 'NL', + 'pl' => 'PL', + 'pt' => 'PT-PT', // ??? + 'pt_BR' => 'PT-BR', // ??? + 'ru' => 'RU', + 'zh_CN' => 'ZH', + ]; + + $locale = strtolower($locale); + if (!$this->languageService->isKnownLanguage($locale)) { + $io->error('Unknown locale given: ' . $locale); + + return 1; + } + + if (!\array_key_exists($locale, $deeplySupportedLanguages)) { + $io->error('Locale not supported by Deeply: ' . $locale); + + return 1; + } + + $allKeys = 0; + $enFiles = glob($bases['core'] . '/*.en.xlf'); + + $baseUrl = 'https://api-free.deepl.com/v2/translate'; + $client = HttpClient::create([]); + + foreach ($enFiles as $file) { + $enTrans = []; + $domain = explode('.', basename($file))[0]; + + $xml = simplexml_load_file($file); + + foreach ($xml->file->body->{'trans-unit'} as $unit) { + $id = (string) $unit['id']; + $enTrans[$id] = [ + 'resname' => (string) $unit['resname'], + 'source' => (string) $unit->source, + 'target' => (string) $unit->target + ]; + $allKeys++; + } + + $localeFile = $bases['core'] . '/' . $domain . '.' . $locale . '.xlf'; + + $translated = []; + + if (file_exists($localeFile)) { + $xml2 = simplexml_load_file($localeFile); + foreach ($xml2->file->body->{'trans-unit'} as $unit) { + $id = (string) $unit['id']; + $translated[$id] = [ + 'resname' => (string) $unit['resname'], + 'source' => (string) $unit->source, + 'target' => (string) $unit->target + ]; + } + } + + $missingIds = array_diff(array_keys($enTrans), array_keys($translated)); + if (\count($missingIds) === 0) { + continue; + } + + $io->title('Translating ' . $domain); + $progress = new ProgressBar($output, \count($missingIds)); + + foreach ($missingIds as $id) { + $progress->advance(); + + $values = $enTrans[$id]; + $translated[$id] = $values; + + $params = [ + 'auth_key' => $deepl, + //'split_sentences' => '1', + //'preserve_formatting' => '0', + 'formality' => 'default', + 'text' => $values['target'], + 'source_lang' => 'en', + 'target_lang' => $deeplySupportedLanguages[$locale], + ]; + + $rawResponseData = null; + try { + $rawResponseData = $client->request('POST', $baseUrl, ['body' => $params]); + } catch (\Exception $exception) { + $io->error($exception->getMessage()); + + return 1; + } + + $json = json_decode($rawResponseData->getContent(), true); + $translation = $json['translations'][0]['text']; + + $translated[$id]['target'] = $translation; + } + + $progress->finish(); + $io->writeln(PHP_EOL); + + $this->writeXliffFile($bases['core'], $domain, $locale, $translated); + } + } + return 0; } + private function writeXliffFile(string $base, string $domain, string $locale, array $translations = []): void + { + $from = $base . '/' . $domain . '.en.xlf'; + $to = $base . '/' . $domain . '.' . $locale . '.xlf'; + + copy($from, $to); + + $xml = simplexml_load_file($to); + + /** @var \SimpleXMLElement $fileNode */ + $fileNode = $xml->file; + $fileNode->attributes()->{'target-language'} = $locale; + $fileNode->attributes()->{'original'} = $domain . '.en.xlf'; + + unset($xml->file->body); + + $xmlDocument = new \DOMDocument('1.0', 'UTF-8'); + $xmlDocument->preserveWhiteSpace = false; + $xmlDocument->formatOutput = true; + $xmlDocument->loadXML($xml->asXML()); + + $xpath = new \DOMXpath($xmlDocument); + $xpath->registerNamespace('ns', $xmlDocument->documentElement->namespaceURI); + + $xmlContent = ''; + foreach ($translations as $id => $values) { + $xmlContent .= sprintf( + '%s%s', + $id, + $values['resname'], + $values['source'], + $values['target'], + ); + } + + $fragment = $xmlDocument->createDocumentFragment(); + $fragment->appendXml('' . $xmlContent . ''); + + /** @var \DOMElement $element */ + $element = $xpath->evaluate('/ns:xliff/ns:file')->item(0); + $element->appendChild($fragment); + + file_put_contents($to, $xmlDocument->saveXML()); + } + private function fixXlfFile(string $file): void { $xml = simplexml_load_file($file); diff --git a/src/Configuration/StringAccessibleConfigTrait.php b/src/Configuration/StringAccessibleConfigTrait.php index 6852bf63..0e35b9ca 100644 --- a/src/Configuration/StringAccessibleConfigTrait.php +++ b/src/Configuration/StringAccessibleConfigTrait.php @@ -58,30 +58,35 @@ trait StringAccessibleConfigTrait // especially the pointers could be a problem in the future foreach ($this->getConfigurations($this->repository) as $configuration) { $temp = explode('.', $configuration->getName()); - $array = &$this->settings; - if ($temp[0] === $this->getPrefix()) { - $temp = \array_slice($temp, 1); - } - foreach ($temp as $key2) { - if (!\array_key_exists($key2, $array)) { - $array[$key2] = $configuration->getValue(); - continue; - } - if (\is_array($array[$key2])) { - $array = &$array[$key2]; - } elseif (\is_bool($array[$key2])) { - $array[$key2] = (bool) $configuration->getValue(); - } elseif (\is_int($array[$key2])) { - $array[$key2] = (int) $configuration->getValue(); - } else { - $array[$key2] = $configuration->getValue(); - } - } + $this->setConfiguration($temp, $configuration->getValue()); } $this->initialized = true; } + private function setConfiguration(array $keys, ?string $value): void + { + $array = &$this->settings; + if ($keys[0] === $this->getPrefix()) { + $keys = \array_slice($keys, 1); + } + foreach ($keys as $key2) { + if (!\array_key_exists($key2, $array)) { + $array[$key2] = $value; + continue; + } + if (\is_array($array[$key2])) { + $array = &$array[$key2]; + } elseif (\is_bool($array[$key2])) { + $array[$key2] = (bool) $value; + } elseif (\is_int($array[$key2])) { + $array[$key2] = (int) $value; + } else { + $array[$key2] = $value; + } + } + } + /** * @return string */ @@ -181,7 +186,7 @@ trait StringAccessibleConfigTrait */ public function offsetSet($offset, $value) { - throw new \BadMethodCallException('SystemBundleConfiguration does not support offsetSet()'); + $this->setConfiguration(explode('.', $offset), $value); } /** diff --git a/src/Constants.php b/src/Constants.php index 1098424d..75fb83fe 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ class Constants /** * The current release version */ - public const VERSION = '1.16.8'; + public const VERSION = '1.16.9'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 11608; + public const VERSION_ID = 11609; /** * The current release status, either "stable" or "dev" */ diff --git a/src/EventSubscriber/Actions/UserSubscriber.php b/src/EventSubscriber/Actions/UserSubscriber.php index e48afba7..e70df666 100644 --- a/src/EventSubscriber/Actions/UserSubscriber.php +++ b/src/EventSubscriber/Actions/UserSubscriber.php @@ -67,7 +67,7 @@ class UserSubscriber extends AbstractActionsSubscriber } if ($viewOther && $user->isEnabled()) { - $event->addAction('timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', ['users[]' => $user->getId()])]); + $event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', ['users[]' => $user->getId()])]); } if ($event->isIndexView() && $this->isGranted('delete', $user)) {