diff --git a/config/services.yaml b/config/services.yaml
index a16b2f9b..8e1c2af5 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -81,6 +81,11 @@ services:
arguments:
$renderer: !tagged widget.renderer
+ App\Utils\Translator:
+ decorates: translator
+ arguments:
+ $localDomains: '%kimai.i18n_domains%'
+
# ================================================================================
# DATABASE
# ================================================================================
diff --git a/src/DependencyInjection/AppExtension.php b/src/DependencyInjection/AppExtension.php
index 757af2af..7777635b 100644
--- a/src/DependencyInjection/AppExtension.php
+++ b/src/DependencyInjection/AppExtension.php
@@ -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)
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 5907f912..423ec761 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -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');
diff --git a/src/Entity/User.php b/src/Entity/User.php
index 68c63bae..60d644bd 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -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();
}
}
diff --git a/src/Form/Type/UserType.php b/src/Form/Type/UserType.php
index 39569513..e3cffb4b 100644
--- a/src/Form/Type/UserType.php
+++ b/src/Form/Type/UserType.php
@@ -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();
},
]);
diff --git a/src/Utils/Translator.php b/src/Utils/Translator.php
new file mode 100644
index 00000000..81d27111
--- /dev/null
+++ b/src/Utils/Translator.php
@@ -0,0 +1,128 @@
+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);
+ }
+}
diff --git a/templates/macros/widgets.html.twig b/templates/macros/widgets.html.twig
index 902f6290..2493cc18 100644
--- a/templates/macros/widgets.html.twig
+++ b/templates/macros/widgets.html.twig
@@ -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) %}
diff --git a/tests/DependencyInjection/AppExtensionTest.php b/tests/DependencyInjection/AppExtensionTest.php
index 3b0c6fd8..2f548acc 100644
--- a/tests/DependencyInjection/AppExtensionTest.php
+++ b/tests/DependencyInjection/AppExtensionTest.php
@@ -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.
diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php
index b28ff9f2..1dfa7b24 100644
--- a/tests/DependencyInjection/ConfigurationTest.php
+++ b/tests/DependencyInjection/ConfigurationTest.php
@@ -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);
+ }
}
diff --git a/tests/Entity/UserTest.php b/tests/Entity/UserTest.php
index 3f1a1517..3e3348c4 100644
--- a/tests/Entity/UserTest.php
+++ b/tests/Entity/UserTest.php
@@ -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()
diff --git a/translations/messages.de.xliff b/translations/messages.de.xliff
index 8b5c3077..4a104416 100644
--- a/translations/messages.de.xliff
+++ b/translations/messages.de.xliff
@@ -190,7 +190,7 @@
label.username
- Benutzername
+ Benutzer
label.description
diff --git a/translations/messages.en.xliff b/translations/messages.en.xliff
index 94f6d3fe..1523cca3 100644
--- a/translations/messages.en.xliff
+++ b/translations/messages.en.xliff
@@ -190,7 +190,7 @@
label.username
- Username
+ User
label.description