support installation specific translations (#1099)

This commit is contained in:
Kevin Papst
2019-09-11 23:18:35 +02:00
committed by GitHub
parent 8c503a226e
commit 7f1a206a52
12 changed files with 349 additions and 16 deletions

View File

@@ -81,6 +81,11 @@ services:
arguments: arguments:
$renderer: !tagged widget.renderer $renderer: !tagged widget.renderer
App\Utils\Translator:
decorates: translator
arguments:
$localDomains: '%kimai.i18n_domains%'
# ================================================================================ # ================================================================================
# DATABASE # DATABASE
# ================================================================================ # ================================================================================

View File

@@ -62,6 +62,16 @@ class AppExtension extends Extension
$container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']); $container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']);
$this->setLdapParameter($config['ldap'], $container); $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) protected function setLdapParameter(array $config, ContainerBuilder $container)

View File

@@ -58,6 +58,7 @@ class Configuration implements ConfigurationInterface
->append($this->getLanguagesNode()) ->append($this->getLanguagesNode())
->append($this->getCalendarNode()) ->append($this->getCalendarNode())
->append($this->getThemeNode()) ->append($this->getThemeNode())
->append($this->getIndustryNode())
->append($this->getDashboardNode()) ->append($this->getDashboardNode())
->append($this->getWidgetsNode()) ->append($this->getWidgetsNode())
->append($this->getDefaultsNode()) ->append($this->getDefaultsNode())
@@ -344,6 +345,9 @@ class Configuration implements ConfigurationInterface
->scalarNode('title') ->scalarNode('title')
->defaultNull() ->defaultNull()
->end() ->end()
->scalarNode('translation')
->defaultNull()
->end()
->end() ->end()
->end() ->end()
->integerNode('autocomplete_chars') ->integerNode('autocomplete_chars')
@@ -355,6 +359,22 @@ class Configuration implements ConfigurationInterface
return $node; 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() protected function getUserNode()
{ {
$builder = new TreeBuilder('user'); $builder = new TreeBuilder('user');

View File

@@ -340,11 +340,20 @@ class User extends BaseUser implements UserInterface
return $this->hasRole(static::ROLE_ADMIN); return $this->hasRole(static::ROLE_ADMIN);
} }
public function getDisplayName(): ?string
{
if (!empty($this->getAlias())) {
return $this->getAlias();
}
return $this->getUsername();
}
/** /**
* @return string * @return string
*/ */
public function __toString() public function __toString()
{ {
return $this->getAlias() ?: $this->getUsername(); return $this->getDisplayName();
} }
} }

View File

@@ -31,11 +31,7 @@ class UserType extends AbstractType
'class' => User::class, 'class' => User::class,
'label' => 'label.user', 'label' => 'label.user',
'choice_label' => function (User $user) { 'choice_label' => function (User $user) {
if (!empty($user->getAlias())) { return $user->getDisplayName();
return $user->getAlias() . ' (' . $user->getUsername() . ')';
}
return $user->getUsername();
}, },
]); ]);

128
src/Utils/Translator.php Normal file
View 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);
}
}

View File

@@ -50,12 +50,12 @@
{% endmacro %} {% endmacro %}
{% macro username(user) %} {% macro username(user) %}
{{ user.alias|default(user.username) }} {{- user.displayName -}}
{% endmacro %} {% endmacro %}
{% macro label_user(user) %} {% macro label_user(user) %}
{% import _self as macro %} {% import _self as macro %}
{{ macro.label(macro.username(user), 'primary') }} {{ macro.label(user.displayName, 'primary') }}
{% endmacro %} {% endmacro %}
{% macro label_activity(activity, url) %} {% macro label_activity(activity, url) %}

View File

@@ -98,7 +98,6 @@ class AppExtensionTest extends TestCase
'currency' => 'EUR', 'currency' => 'EUR',
] ]
], ],
'kimai.theme' => [ 'kimai.theme' => [
'active_warning' => 3, 'active_warning' => 3,
'box_color' => 'green', 'box_color' => 'green',
@@ -115,18 +114,17 @@ class AppExtensionTest extends TestCase
'mini' => null, 'mini' => null,
'company' => null, 'company' => null,
'title' => null, 'title' => null,
'translation' => null,
], ],
'auto_reload_datatable' => false, 'auto_reload_datatable' => false,
'autocomplete_chars' => 3, 'autocomplete_chars' => 3,
], ],
'kimai.theme.select_type' => null, 'kimai.theme.select_type' => null,
'kimai.theme.show_about' => true, 'kimai.theme.show_about' => true,
'kimai.fosuser' => [ 'kimai.fosuser' => [
'registration' => true, 'registration' => true,
'password_reset' => true, 'password_reset' => true,
], ],
'kimai.timesheet' => [ 'kimai.timesheet' => [
'mode' => 'default', 'mode' => 'default',
'markdown_content' => false, 'markdown_content' => false,
@@ -174,6 +172,7 @@ class AppExtensionTest extends TestCase
'ROLE_ADMIN' => [], 'ROLE_ADMIN' => [],
'ROLE_SUPER_ADMIN' => [], 'ROLE_SUPER_ADMIN' => [],
], ],
'kimai.i18n_domains' => []
]; ];
// nasty parameter, should be removed!!! // nasty parameter, should be removed!!!
@@ -322,6 +321,34 @@ class AppExtensionTest extends TestCase
$this->assertEquals('(&(objectClass=inetOrgPerson))', $ldapConfig['user']['filter']); $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 * @expectedException \PHPUnit\Framework\Error\Notice
* @expectedExceptionMessage Found invalid "kimai" configuration: The child node "data_dir" at path "kimai" must be configured. * @expectedExceptionMessage Found invalid "kimai" configuration: The child node "data_dir" at path "kimai" must be configured.

View File

@@ -219,4 +219,138 @@ class ConfigurationTest extends TestCase
]; ];
self::assertEquals($expected, $finalizedConfig['ldap']); 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);
}
} }

View File

@@ -25,6 +25,7 @@ class UserTest extends TestCase
$user = new User(); $user = new User();
$this->assertInstanceOf(ArrayCollection::class, $user->getPreferences()); $this->assertInstanceOf(ArrayCollection::class, $user->getPreferences());
$this->assertNull($user->getTitle()); $this->assertNull($user->getTitle());
$this->assertNull($user->getDisplayName());
$this->assertNull($user->getAvatar()); $this->assertNull($user->getAvatar());
$this->assertNull($user->getAlias()); $this->assertNull($user->getAlias());
$this->assertNull($user->getId()); $this->assertNull($user->getId());
@@ -73,17 +74,20 @@ class UserTest extends TestCase
$this->assertEquals('I like rain', $user->getPreferenceValue('test2')); $this->assertEquals('I like rain', $user->getPreferenceValue('test2'));
} }
public function testToString() public function testDisplayName()
{ {
$user = new User(); $user = new User();
$user->setUsername('bar'); $user->setUsername('bar');
$this->assertEquals('bar', (string) $user); $this->assertEquals('bar', $user->getDisplayName());
$this->assertEquals('bar', $user->getUsername()); $this->assertEquals('bar', $user->getUsername());
$this->assertEquals('bar', (string) $user);
$user->setAlias('foo'); $user->setAlias('foo');
$this->assertEquals('foo', (string) $user);
$this->assertEquals('foo', $user->getAlias()); $this->assertEquals('foo', $user->getAlias());
$this->assertEquals('bar', $user->getUsername());
$this->assertEquals('foo', $user->getDisplayName());
$this->assertEquals('foo', (string) $user);
} }
public function testGetLocale() public function testGetLocale()

View File

@@ -190,7 +190,7 @@
</trans-unit> </trans-unit>
<trans-unit id="label.username"> <trans-unit id="label.username">
<source>label.username</source> <source>label.username</source>
<target>Benutzername</target> <target>Benutzer</target>
</trans-unit> </trans-unit>
<trans-unit id="label.description"> <trans-unit id="label.description">
<source>label.description</source> <source>label.description</source>

View File

@@ -190,7 +190,7 @@
</trans-unit> </trans-unit>
<trans-unit id="label.username"> <trans-unit id="label.username">
<source>label.username</source> <source>label.username</source>
<target>Username</target> <target>User</target>
</trans-unit> </trans-unit>
<trans-unit id="label.description"> <trans-unit id="label.description">
<source>label.description</source> <source>label.description</source>