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
This commit is contained in:
29
.github/workflows/coverage.yaml
vendored
29
.github/workflows/coverage.yaml
vendored
@@ -23,16 +23,36 @@ jobs:
|
|||||||
|
|
||||||
name: Coverage (${{ matrix.php }})
|
name: Coverage (${{ matrix.php }})
|
||||||
steps:
|
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:
|
with:
|
||||||
php-version: ${{ matrix.php }}
|
php-version: ${{ matrix.php }}
|
||||||
coverage: pcov
|
coverage: pcov
|
||||||
extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap
|
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
|
- name: Setup problem matchers for PHPUnit
|
||||||
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml
|
run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml
|
||||||
env:
|
env:
|
||||||
@@ -40,6 +60,7 @@ jobs:
|
|||||||
APP_ENV: dev
|
APP_ENV: dev
|
||||||
MAILER_URL: null://localhost
|
MAILER_URL: null://localhost
|
||||||
TEST_WITH_BUNDLES: 1
|
TEST_WITH_BUNDLES: 1
|
||||||
|
|
||||||
- name: Upload code coverage
|
- name: Upload code coverage
|
||||||
uses: codecov/codecov-action@v2
|
uses: codecov/codecov-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/lockfiles.yaml
vendored
2
.github/workflows/lockfiles.yaml
vendored
@@ -9,8 +9,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Verify lock file integrity
|
name: Verify lock file integrity
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Clone Kimai
|
- name: Clone Kimai
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Prevent file change
|
- name: Prevent file change
|
||||||
uses: xalvarez/prevent-file-change-action@v1
|
uses: xalvarez/prevent-file-change-action@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
12
.github/workflows/testing.yaml
vendored
12
.github/workflows/testing.yaml
vendored
@@ -35,8 +35,18 @@ jobs:
|
|||||||
extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap
|
extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap
|
||||||
tools: cs2pr:1.1.0
|
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
|
- name: Install dependencies
|
||||||
uses: ramsey/composer-install@v1
|
run: composer install
|
||||||
|
|
||||||
- name: Validate Composer
|
- name: Validate Composer
|
||||||
run: composer validate --strict
|
run: composer validate --strict
|
||||||
|
|||||||
@@ -10,11 +10,14 @@
|
|||||||
namespace App\Command;
|
namespace App\Command;
|
||||||
|
|
||||||
use App\Kernel;
|
use App\Kernel;
|
||||||
|
use App\Utils\LanguageService;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Helper\ProgressBar;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
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.
|
* 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 $projectDirectory;
|
||||||
private $environment;
|
private $environment;
|
||||||
|
private $languageService;
|
||||||
|
|
||||||
public function __construct(string $projectDirectory, string $kernelEnvironment)
|
public function __construct(string $projectDirectory, string $kernelEnvironment, LanguageService $languageService)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->projectDirectory = $projectDirectory;
|
$this->projectDirectory = $projectDirectory;
|
||||||
$this->environment = $kernelEnvironment;
|
$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('duplicates', null, InputOption::VALUE_NONE, 'Find duplicate translation keys')
|
||||||
->addOption('delete-resname', null, InputOption::VALUE_REQUIRED, 'Deletes the translation by resname')
|
->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('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;
|
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(
|
||||||
|
'<trans-unit id="%s" resname="%s"><source>%s</source><target>%s</target></trans-unit>',
|
||||||
|
$id,
|
||||||
|
$values['resname'],
|
||||||
|
$values['source'],
|
||||||
|
$values['target'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fragment = $xmlDocument->createDocumentFragment();
|
||||||
|
$fragment->appendXml('<body>' . $xmlContent . '</body>');
|
||||||
|
|
||||||
|
/** @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
|
private function fixXlfFile(string $file): void
|
||||||
{
|
{
|
||||||
$xml = simplexml_load_file($file);
|
$xml = simplexml_load_file($file);
|
||||||
|
|||||||
@@ -58,30 +58,35 @@ trait StringAccessibleConfigTrait
|
|||||||
// especially the pointers could be a problem in the future
|
// especially the pointers could be a problem in the future
|
||||||
foreach ($this->getConfigurations($this->repository) as $configuration) {
|
foreach ($this->getConfigurations($this->repository) as $configuration) {
|
||||||
$temp = explode('.', $configuration->getName());
|
$temp = explode('.', $configuration->getName());
|
||||||
$array = &$this->settings;
|
$this->setConfiguration($temp, $configuration->getValue());
|
||||||
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->initialized = true;
|
$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
|
* @return string
|
||||||
*/
|
*/
|
||||||
@@ -181,7 +186,7 @@ trait StringAccessibleConfigTrait
|
|||||||
*/
|
*/
|
||||||
public function offsetSet($offset, $value)
|
public function offsetSet($offset, $value)
|
||||||
{
|
{
|
||||||
throw new \BadMethodCallException('SystemBundleConfiguration does not support offsetSet()');
|
$this->setConfiguration(explode('.', $offset), $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '1.16.8';
|
public const VERSION = '1.16.9';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* 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"
|
* The current release status, either "stable" or "dev"
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class UserSubscriber extends AbstractActionsSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($viewOther && $user->isEnabled()) {
|
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)) {
|
if ($event->isIndexView() && $this->isGranted('delete', $user)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user