From 7f1a206a526a782ed56236d4c6d7cc020f60c702 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Wed, 11 Sep 2019 23:18:35 +0200 Subject: [PATCH] support installation specific translations (#1099) --- config/services.yaml | 5 + src/DependencyInjection/AppExtension.php | 10 ++ src/DependencyInjection/Configuration.php | 20 +++ src/Entity/User.php | 11 +- src/Form/Type/UserType.php | 6 +- src/Utils/Translator.php | 128 +++++++++++++++++ templates/macros/widgets.html.twig | 4 +- .../DependencyInjection/AppExtensionTest.php | 33 ++++- .../DependencyInjection/ConfigurationTest.php | 134 ++++++++++++++++++ tests/Entity/UserTest.php | 10 +- translations/messages.de.xliff | 2 +- translations/messages.en.xliff | 2 +- 12 files changed, 349 insertions(+), 16 deletions(-) create mode 100644 src/Utils/Translator.php 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