support installation specific translations (#1099)
This commit is contained in:
@@ -81,6 +81,11 @@ services:
|
||||
arguments:
|
||||
$renderer: !tagged widget.renderer
|
||||
|
||||
App\Utils\Translator:
|
||||
decorates: translator
|
||||
arguments:
|
||||
$localDomains: '%kimai.i18n_domains%'
|
||||
|
||||
# ================================================================================
|
||||
# DATABASE
|
||||
# ================================================================================
|
||||
|
||||
@@ -62,6 +62,16 @@ class AppExtension extends Extension
|
||||
$container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']);
|
||||
|
||||
$this->setLdapParameter($config['ldap'], $container);
|
||||
|
||||
// translation files, which can overwrite the default kimai translations
|
||||
$localTranslations = [];
|
||||
if (null !== $config['theme']['branding']['translation']) {
|
||||
$localTranslations[] = $config['theme']['branding']['translation'];
|
||||
}
|
||||
if (null !== $config['industry']['translation']) {
|
||||
$localTranslations[] = $config['industry']['translation'];
|
||||
}
|
||||
$container->setParameter('kimai.i18n_domains', $localTranslations);
|
||||
}
|
||||
|
||||
protected function setLdapParameter(array $config, ContainerBuilder $container)
|
||||
|
||||
@@ -58,6 +58,7 @@ class Configuration implements ConfigurationInterface
|
||||
->append($this->getLanguagesNode())
|
||||
->append($this->getCalendarNode())
|
||||
->append($this->getThemeNode())
|
||||
->append($this->getIndustryNode())
|
||||
->append($this->getDashboardNode())
|
||||
->append($this->getWidgetsNode())
|
||||
->append($this->getDefaultsNode())
|
||||
@@ -344,6 +345,9 @@ class Configuration implements ConfigurationInterface
|
||||
->scalarNode('title')
|
||||
->defaultNull()
|
||||
->end()
|
||||
->scalarNode('translation')
|
||||
->defaultNull()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->integerNode('autocomplete_chars')
|
||||
@@ -355,6 +359,22 @@ class Configuration implements ConfigurationInterface
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getIndustryNode()
|
||||
{
|
||||
$builder = new TreeBuilder('industry');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('translation')->defaultNull()->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getUserNode()
|
||||
{
|
||||
$builder = new TreeBuilder('user');
|
||||
|
||||
@@ -340,11 +340,20 @@ class User extends BaseUser implements UserInterface
|
||||
return $this->hasRole(static::ROLE_ADMIN);
|
||||
}
|
||||
|
||||
public function getDisplayName(): ?string
|
||||
{
|
||||
if (!empty($this->getAlias())) {
|
||||
return $this->getAlias();
|
||||
}
|
||||
|
||||
return $this->getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getAlias() ?: $this->getUsername();
|
||||
return $this->getDisplayName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,7 @@ class UserType extends AbstractType
|
||||
'class' => User::class,
|
||||
'label' => 'label.user',
|
||||
'choice_label' => function (User $user) {
|
||||
if (!empty($user->getAlias())) {
|
||||
return $user->getAlias() . ' (' . $user->getUsername() . ')';
|
||||
}
|
||||
|
||||
return $user->getUsername();
|
||||
return $user->getDisplayName();
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
128
src/Utils/Translator.php
Normal file
128
src/Utils/Translator.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Utils;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Translation\Translator as BaseTranslator;
|
||||
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Translation\MessageCatalogueInterface;
|
||||
use Symfony\Component\Translation\TranslatorBagInterface;
|
||||
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Should be:
|
||||
* class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface
|
||||
*
|
||||
* But this is not compatible with PagerFanta yet.
|
||||
*/
|
||||
class Translator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface
|
||||
{
|
||||
/**
|
||||
* @var BaseTranslator
|
||||
*/
|
||||
private $translator;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $localDomains = [];
|
||||
|
||||
public function __construct(BaseTranslator $translator, array $localDomains = [])
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->localDomains = $localDomains;
|
||||
}
|
||||
|
||||
public function trans($id, array $parameters = [], $domain = 'messages', $locale = null)
|
||||
{
|
||||
if (null === $domain) {
|
||||
$domain = 'messages';
|
||||
}
|
||||
|
||||
foreach ($this->localDomains as $localDomain) {
|
||||
if (false !== $this->hasLocalOverwrite($id, $localDomain, $locale)) {
|
||||
$domain = $localDomain;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->translator->trans($id, $parameters, $domain, $locale);
|
||||
}
|
||||
|
||||
protected function hasLocalOverwrite($id, $domain, $locale = null): bool
|
||||
{
|
||||
$found = false;
|
||||
|
||||
$catalogue = $this->getCatalogue($locale);
|
||||
while (false === ($found = $catalogue->defines($id, $domain))) {
|
||||
if ($cat = $catalogue->getFallbackCatalogue()) {
|
||||
$catalogue = $cat;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the catalogue by locale.
|
||||
*
|
||||
* @param string|null $locale The locale or null to use the default
|
||||
*
|
||||
* @return MessageCatalogueInterface
|
||||
*
|
||||
* @throws InvalidArgumentException If the locale contains invalid characters
|
||||
*/
|
||||
public function getCatalogue($locale = null)
|
||||
{
|
||||
return $this->translator->getCatalogue($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current locale.
|
||||
*
|
||||
* @param string $locale The locale
|
||||
*
|
||||
* @throws \InvalidArgumentException If the locale contains invalid characters
|
||||
*/
|
||||
public function setLocale($locale)
|
||||
{
|
||||
$this->translator->setLocale($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current locale.
|
||||
*
|
||||
* @return string The locale
|
||||
*/
|
||||
public function getLocale()
|
||||
{
|
||||
return $this->translator->getLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given choice message by choosing a translation according to a number.
|
||||
*
|
||||
* @param string $id The message id (may also be an object that can be cast to string)
|
||||
* @param int $number The number to use to find the index of the message
|
||||
* @param array $parameters An array of parameters for the message
|
||||
* @param string|null $domain The domain for the message or null to use the default
|
||||
* @param string|null $locale The locale or null to use the default
|
||||
*
|
||||
* @return string The translated string
|
||||
*
|
||||
* @throws InvalidArgumentException If the locale contains invalid characters
|
||||
*/
|
||||
public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null)
|
||||
{
|
||||
return $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
|
||||
}
|
||||
}
|
||||
@@ -50,12 +50,12 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro username(user) %}
|
||||
{{ user.alias|default(user.username) }}
|
||||
{{- user.displayName -}}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_user(user) %}
|
||||
{% import _self as macro %}
|
||||
{{ macro.label(macro.username(user), 'primary') }}
|
||||
{{ macro.label(user.displayName, 'primary') }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_activity(activity, url) %}
|
||||
|
||||
@@ -98,7 +98,6 @@ class AppExtensionTest extends TestCase
|
||||
'currency' => 'EUR',
|
||||
]
|
||||
],
|
||||
|
||||
'kimai.theme' => [
|
||||
'active_warning' => 3,
|
||||
'box_color' => 'green',
|
||||
@@ -115,18 +114,17 @@ class AppExtensionTest extends TestCase
|
||||
'mini' => null,
|
||||
'company' => null,
|
||||
'title' => null,
|
||||
'translation' => null,
|
||||
],
|
||||
'auto_reload_datatable' => false,
|
||||
'autocomplete_chars' => 3,
|
||||
],
|
||||
'kimai.theme.select_type' => null,
|
||||
'kimai.theme.show_about' => true,
|
||||
|
||||
'kimai.fosuser' => [
|
||||
'registration' => true,
|
||||
'password_reset' => true,
|
||||
],
|
||||
|
||||
'kimai.timesheet' => [
|
||||
'mode' => 'default',
|
||||
'markdown_content' => false,
|
||||
@@ -174,6 +172,7 @@ class AppExtensionTest extends TestCase
|
||||
'ROLE_ADMIN' => [],
|
||||
'ROLE_SUPER_ADMIN' => [],
|
||||
],
|
||||
'kimai.i18n_domains' => []
|
||||
];
|
||||
|
||||
// nasty parameter, should be removed!!!
|
||||
@@ -322,6 +321,34 @@ class AppExtensionTest extends TestCase
|
||||
$this->assertEquals('(&(objectClass=inetOrgPerson))', $ldapConfig['user']['filter']);
|
||||
}
|
||||
|
||||
public function testTranslationOverwritesEmpty()
|
||||
{
|
||||
$minConfig = $this->getMinConfig();
|
||||
$this->extension->load($minConfig, $container = $this->getContainer());
|
||||
|
||||
$config = $container->getParameter('kimai.i18n_domains');
|
||||
$this->assertEquals([], $config);
|
||||
}
|
||||
|
||||
public function testTranslationOverwrites()
|
||||
{
|
||||
$minConfig = $this->getMinConfig();
|
||||
$minConfig['kimai']['industry'] = [
|
||||
'translation' => 'xxxx',
|
||||
];
|
||||
$minConfig['kimai']['theme'] = [
|
||||
'branding' => [
|
||||
'translation' => 'yyyy',
|
||||
]
|
||||
];
|
||||
|
||||
$this->extension->load($minConfig, $container = $this->getContainer());
|
||||
|
||||
$config = $container->getParameter('kimai.i18n_domains');
|
||||
// oder is important, theme/installation specific translations win
|
||||
$this->assertEquals(['yyyy', 'xxxx'], $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \PHPUnit\Framework\Error\Notice
|
||||
* @expectedExceptionMessage Found invalid "kimai" configuration: The child node "data_dir" at path "kimai" must be configured.
|
||||
|
||||
@@ -219,4 +219,138 @@ class ConfigurationTest extends TestCase
|
||||
];
|
||||
self::assertEquals($expected, $finalizedConfig['ldap']);
|
||||
}
|
||||
|
||||
public function testFullDefaultConfig()
|
||||
{
|
||||
$fullDefaultConfig = [
|
||||
'data_dir' => '/tmp/',
|
||||
'plugin_dir' => '/tmp/',
|
||||
'timesheet' => [
|
||||
'default_begin' => 'now',
|
||||
'mode' => 'default',
|
||||
'markdown_content' => false,
|
||||
'rounding' => [],
|
||||
'rates' => [],
|
||||
'active_entries' => [
|
||||
'soft_limit' => 1,
|
||||
'hard_limit' => 1,
|
||||
],
|
||||
'rules' => [
|
||||
'allow_future_times' => true,
|
||||
],
|
||||
],
|
||||
'user' => [
|
||||
'registration' => true,
|
||||
'password_reset' => true,
|
||||
],
|
||||
'invoice' => [
|
||||
'documents' => [
|
||||
0 => 'var/invoices/',
|
||||
1 => 'templates/invoice/renderer/',
|
||||
],
|
||||
],
|
||||
'languages' => [],
|
||||
'calendar' => [
|
||||
'week_numbers' => true,
|
||||
'day_limit' => 4,
|
||||
'slot_duration' => '00:30:00',
|
||||
'businessHours' => [
|
||||
'days' => [
|
||||
0 => 1,
|
||||
1 => 2,
|
||||
2 => 3,
|
||||
3 => 4,
|
||||
4 => 5,
|
||||
],
|
||||
'begin' => '08:00',
|
||||
'end' => '20:00',
|
||||
],
|
||||
'visibleHours' => [
|
||||
'begin' => '00:00',
|
||||
'end' => '23:59',
|
||||
],
|
||||
'google' => [
|
||||
'api_key' => null,
|
||||
'sources' => [
|
||||
],
|
||||
],
|
||||
'weekends' => true,
|
||||
],
|
||||
'theme' => [
|
||||
'active_warning' => 3,
|
||||
'box_color' => 'green',
|
||||
'select_type' => null,
|
||||
'auto_reload_datatable' => false,
|
||||
'show_about' => true,
|
||||
'chart' => [
|
||||
'background_color' => 'rgba(0,115,183,0.7)',
|
||||
'border_color' => '#3b8bba',
|
||||
'grid_color' => 'rgba(0,0,0,.05)',
|
||||
'height' => '200',
|
||||
],
|
||||
'branding' => [
|
||||
'logo' => null,
|
||||
'mini' => null,
|
||||
'company' => null,
|
||||
'title' => null,
|
||||
'translation' => null,
|
||||
],
|
||||
'autocomplete_chars' => 3,
|
||||
],
|
||||
'industry' => [
|
||||
'translation' => null,
|
||||
],
|
||||
'dashboard' => [],
|
||||
'widgets' => [],
|
||||
'defaults' => [
|
||||
'customer' => [
|
||||
'timezone' => null,
|
||||
'country' => 'DE',
|
||||
'currency' => 'EUR',
|
||||
],
|
||||
'user' => [
|
||||
'timezone' => null,
|
||||
'language' => 'en',
|
||||
'theme' => null,
|
||||
'currency' => 'EUR',
|
||||
],
|
||||
],
|
||||
'permissions' => [
|
||||
'sets' => [],
|
||||
'maps' => [],
|
||||
'roles' => [
|
||||
'ROLE_USER' => [],
|
||||
'ROLE_TEAMLEAD' => [],
|
||||
'ROLE_ADMIN' => [],
|
||||
'ROLE_SUPER_ADMIN' => [],
|
||||
],
|
||||
],
|
||||
'ldap' => [
|
||||
'connection' => [
|
||||
'host' => null,
|
||||
'port' => 389,
|
||||
'useStartTls' => false,
|
||||
'useSsl' => false,
|
||||
'bindRequiresDn' => true,
|
||||
'accountFilterFormat' => null,
|
||||
],
|
||||
'user' => [
|
||||
'baseDn' => null,
|
||||
'filter' => '',
|
||||
'attributesFilter' => '(objectClass=*)',
|
||||
'usernameAttribute' => 'uid',
|
||||
'attributes' => [],
|
||||
],
|
||||
'role' => [
|
||||
'baseDn' => null,
|
||||
'usernameAttribute' => 'dn',
|
||||
'nameAttribute' => 'cn',
|
||||
'userDnAttribute' => 'member',
|
||||
'groups' => [],
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertConfig($this->getMinConfig(), $fullDefaultConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class UserTest extends TestCase
|
||||
$user = new User();
|
||||
$this->assertInstanceOf(ArrayCollection::class, $user->getPreferences());
|
||||
$this->assertNull($user->getTitle());
|
||||
$this->assertNull($user->getDisplayName());
|
||||
$this->assertNull($user->getAvatar());
|
||||
$this->assertNull($user->getAlias());
|
||||
$this->assertNull($user->getId());
|
||||
@@ -73,17 +74,20 @@ class UserTest extends TestCase
|
||||
$this->assertEquals('I like rain', $user->getPreferenceValue('test2'));
|
||||
}
|
||||
|
||||
public function testToString()
|
||||
public function testDisplayName()
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
$user->setUsername('bar');
|
||||
$this->assertEquals('bar', (string) $user);
|
||||
$this->assertEquals('bar', $user->getDisplayName());
|
||||
$this->assertEquals('bar', $user->getUsername());
|
||||
$this->assertEquals('bar', (string) $user);
|
||||
|
||||
$user->setAlias('foo');
|
||||
$this->assertEquals('foo', (string) $user);
|
||||
$this->assertEquals('foo', $user->getAlias());
|
||||
$this->assertEquals('bar', $user->getUsername());
|
||||
$this->assertEquals('foo', $user->getDisplayName());
|
||||
$this->assertEquals('foo', (string) $user);
|
||||
}
|
||||
|
||||
public function testGetLocale()
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="label.username">
|
||||
<source>label.username</source>
|
||||
<target>Benutzername</target>
|
||||
<target>Benutzer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.description">
|
||||
<source>label.description</source>
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="label.username">
|
||||
<source>label.username</source>
|
||||
<target>Username</target>
|
||||
<target>User</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.description">
|
||||
<source>label.description</source>
|
||||
|
||||
Reference in New Issue
Block a user